Files
boss/docs/superpowers/plans/2026-04-03-claw-backend-adapter.md
2026-04-03 00:48:51 +08:00

21 KiB
Raw Permalink Blame History

Boss ClawBackendAdapter Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 在不改变当前生产主链默认行为的前提下,为 Boss 新增一个默认关闭、可显式启用的 ClawBackendAdapter,让 claw-code 能作为单次执行后端候选接入 master_agent_reply / thread_reply

Architecture: 先在 src/lib/execution/backends/ 下增加 claw-config / claw-runner / claw-backend 三个 focused 模块,再把 ExecutionBackendSelector 扩成支持 claw 候选但默认不参与。整个过程坚持“先最小单次执行,不改群聊审批和设备导入行为”,并通过环境变量驱动启用状态与外部命令配置。

Tech Stack: Next.js App Router、TypeScript、Node.js child_process、现有 src/lib/execution/* 抽象层、tsx --test


Task 1: 定义 Claw backend 配置与最小运行时可用性判断

Files:

  • Create: /Users/kris/code/boss/src/lib/execution/backends/claw-config.ts

  • Test: /Users/kris/code/boss/tests/claw-backend-config.test.ts

  • Step 1: 写失败测试,固定默认关闭与配置解析行为

import test from "node:test";
import assert from "node:assert/strict";
import {
  getClawBackendConfigForTesting,
  isClawBackendConfiguredForTesting,
} from "../src/lib/execution/backends/claw-config.ts";

test("Claw backend 在未配置时默认关闭", () => {
  const previous = { ...process.env };
  delete process.env.BOSS_CLAW_ENABLED;
  delete process.env.BOSS_CLAW_COMMAND;

  const config = getClawBackendConfigForTesting();
  assert.equal(config.enabled, false);
  assert.equal(isClawBackendConfiguredForTesting(config), false);

  process.env = previous;
});

test("Claw backend 在配置完整时返回 command、args 和 timeout", () => {
  const previous = { ...process.env };
  process.env.BOSS_CLAW_ENABLED = "true";
  process.env.BOSS_CLAW_COMMAND = "claw";
  process.env.BOSS_CLAW_ARGS = "run --json";
  process.env.BOSS_CLAW_WORKDIR = "/tmp/claw";
  process.env.BOSS_CLAW_TIMEOUT_MS = "45000";

  const config = getClawBackendConfigForTesting();
  assert.equal(config.enabled, true);
  assert.equal(config.command, "claw");
  assert.deepEqual(config.args, ["run", "--json"]);
  assert.equal(config.cwd, "/tmp/claw");
  assert.equal(config.timeoutMs, 45000);
  assert.equal(isClawBackendConfiguredForTesting(config), true);

  process.env = previous;
});
  • Step 2: 运行测试确认当前失败

Run:

cd /Users/kris/code/boss
npx --yes tsx --test tests/claw-backend-config.test.ts

Expected:

ERR_MODULE_NOT_FOUND
  • Step 3: 写最小配置解析实现
// /Users/kris/code/boss/src/lib/execution/backends/claw-config.ts
export interface ClawBackendConfig {
  enabled: boolean;
  command?: string;
  args: string[];
  cwd?: string;
  timeoutMs: number;
  defaultModel?: string;
}

function parseBoolean(value: string | undefined) {
  return value?.trim().toLowerCase() === "true";
}

function parseArgs(value: string | undefined) {
  return String(value || "")
    .trim()
    .split(/\s+/)
    .filter(Boolean);
}

export function getClawBackendConfig(): ClawBackendConfig {
  return {
    enabled: parseBoolean(process.env.BOSS_CLAW_ENABLED),
    command: process.env.BOSS_CLAW_COMMAND?.trim() || undefined,
    args: parseArgs(process.env.BOSS_CLAW_ARGS),
    cwd: process.env.BOSS_CLAW_WORKDIR?.trim() || undefined,
    timeoutMs: Number.parseInt(process.env.BOSS_CLAW_TIMEOUT_MS || "45000", 10) || 45000,
    defaultModel: process.env.BOSS_CLAW_DEFAULT_MODEL?.trim() || undefined,
  };
}

export function isClawBackendConfigured(config: ClawBackendConfig) {
  return config.enabled && Boolean(config.command);
}

export const getClawBackendConfigForTesting = getClawBackendConfig;
export const isClawBackendConfiguredForTesting = isClawBackendConfigured;
  • Step 4: 再跑测试确认通过

Run:

cd /Users/kris/code/boss
npx --yes tsx --test tests/claw-backend-config.test.ts

Expected:

# pass 2
  • Step 5: 提交
cd /Users/kris/code/boss
git add tests/claw-backend-config.test.ts src/lib/execution/backends/claw-config.ts
git commit -m "feat: add claw backend config"

Task 2: 抽出 Claw runner统一外部命令协议

Files:

  • Create: /Users/kris/code/boss/src/lib/execution/backends/claw-runner.ts

  • Test: /Users/kris/code/boss/tests/claw-runner.test.ts

  • Step 1: 写失败测试,固定 JSON 协议映射

import test from "node:test";
import assert from "node:assert/strict";
import { createClawProcessResultForTesting } from "../src/lib/execution/backends/claw-runner.ts";

test("Claw runner 会把成功 JSON 映射成 completed", () => {
  const result = createClawProcessResultForTesting({
    exitCode: 0,
    stdout: JSON.stringify({ status: "completed", output: "链路正常" }),
    stderr: "",
  });

  assert.equal(result.status, "completed");
  assert.equal(result.output, "链路正常");
});

test("Claw runner 会把非法 JSON 映射成 failed", () => {
  const result = createClawProcessResultForTesting({
    exitCode: 0,
    stdout: "not-json",
    stderr: "",
  });

  assert.equal(result.status, "failed");
  assert.match(result.error, /INVALID_CLAW_RESPONSE/);
});

test("Claw runner 会把非零退出码映射成 failed", () => {
  const result = createClawProcessResultForTesting({
    exitCode: 2,
    stdout: "",
    stderr: "claw crashed",
  });

  assert.equal(result.status, "failed");
  assert.match(result.error, /claw crashed/);
});
  • Step 2: 运行测试确认当前失败

Run:

cd /Users/kris/code/boss
npx --yes tsx --test tests/claw-runner.test.ts

Expected:

ERR_MODULE_NOT_FOUND
  • Step 3: 写最小 runner 实现
// /Users/kris/code/boss/src/lib/execution/backends/claw-runner.ts
import { spawn } from "node:child_process";
import type { ExecutionImmediateResult } from "@/lib/execution/types";
import type { ClawBackendConfig } from "@/lib/execution/backends/claw-config";

function normalizeClawProcessResult(input: {
  exitCode: number;
  stdout: string;
  stderr: string;
}): ExecutionImmediateResult {
  if (input.exitCode !== 0) {
    return {
      status: "failed",
      backendId: "claw-runtime",
      error: input.stderr.trim() || `CLAW_EXIT_${input.exitCode}`,
    };
  }

  try {
    const parsed = JSON.parse(input.stdout);
    if (parsed?.status === "completed" && typeof parsed.output === "string") {
      return {
        status: "completed",
        backendId: "claw-runtime",
        output: parsed.output,
      };
    }
    if (parsed?.status === "failed" && typeof parsed.error === "string") {
      return {
        status: "failed",
        backendId: "claw-runtime",
        error: parsed.error,
      };
    }
    return {
      status: "failed",
      backendId: "claw-runtime",
      error: "INVALID_CLAW_RESPONSE",
    };
  } catch {
    return {
      status: "failed",
      backendId: "claw-runtime",
      error: "INVALID_CLAW_RESPONSE",
    };
  }
}

export async function runClawCommand(input: {
  config: ClawBackendConfig;
  payload: unknown;
}): Promise<ExecutionImmediateResult> {
  if (!input.config.command) {
    return {
      status: "failed",
      backendId: "claw-runtime",
      error: "CLAW_COMMAND_NOT_CONFIGURED",
    };
  }

  return new Promise((resolve) => {
    const child = spawn(input.config.command, input.config.args, {
      cwd: input.config.cwd,
      env: process.env,
      stdio: ["pipe", "pipe", "pipe"],
    });

    let stdout = "";
    let stderr = "";
    const timer = setTimeout(() => {
      child.kill("SIGKILL");
      resolve({
        status: "failed",
        backendId: "claw-runtime",
        error: "CLAW_TIMEOUT",
      });
    }, input.config.timeoutMs);

    child.stdout.on("data", (chunk) => {
      stdout += String(chunk);
    });
    child.stderr.on("data", (chunk) => {
      stderr += String(chunk);
    });
    child.on("error", (error) => {
      clearTimeout(timer);
      resolve({
        status: "failed",
        backendId: "claw-runtime",
        error: error.message,
      });
    });
    child.on("close", (code) => {
      clearTimeout(timer);
      resolve(normalizeClawProcessResult({ exitCode: code ?? 1, stdout, stderr }));
    });

    child.stdin.end(JSON.stringify(input.payload));
  });
}

export const createClawProcessResultForTesting = normalizeClawProcessResult;
  • Step 4: 再跑测试确认通过

Run:

cd /Users/kris/code/boss
npx --yes tsx --test tests/claw-runner.test.ts

Expected:

# pass 3
  • Step 5: 提交
cd /Users/kris/code/boss
git add tests/claw-runner.test.ts src/lib/execution/backends/claw-runner.ts
git commit -m "feat: add claw runtime runner"

Task 3: 新增 ClawBackendAdapter,只承接单次执行

Files:

  • Create: /Users/kris/code/boss/src/lib/execution/backends/claw-backend.ts

  • Test: /Users/kris/code/boss/tests/claw-backend.test.ts

  • Step 1: 写失败测试,固定可处理范围和描述信息

import test from "node:test";
import assert from "node:assert/strict";
import { createExecutionRequest } from "../src/lib/execution/types.ts";
import { createClawBackendForTesting } from "../src/lib/execution/backends/claw-backend.ts";

test("ClawBackendAdapter 未启用时不可处理任何请求", async () => {
  const backend = createClawBackendForTesting({ enabled: false, args: [], timeoutMs: 1000 });
  const canHandle = await backend.canHandle(
    createExecutionRequest({
      kind: "master_agent_reply",
      projectId: "master-agent",
      requestMessageId: "msg-1",
      body: "继续",
    }),
  );

  assert.equal(canHandle, false);
});

test("ClawBackendAdapter 仅接 master_agent_reply 和 thread_reply", async () => {
  const backend = createClawBackendForTesting({ enabled: true, command: "claw", args: [], timeoutMs: 1000 });
  assert.equal(
    await backend.canHandle(createExecutionRequest({ kind: "master_agent_reply", projectId: "master-agent", requestMessageId: "1", body: "a" })),
    true,
  );
  assert.equal(
    await backend.canHandle(createExecutionRequest({ kind: "thread_reply", projectId: "p1", requestMessageId: "2", body: "b" })),
    true,
  );
  assert.equal(
    await backend.canHandle(createExecutionRequest({ kind: "dispatch_execution", projectId: "g1", requestMessageId: "3", body: "c" })),
    false,
  );
});

test("ClawBackendAdapter describe 返回 claw backend 元信息", async () => {
  const backend = createClawBackendForTesting({ enabled: true, command: "claw", args: [], timeoutMs: 1000 });
  const description = await backend.describe(
    createExecutionRequest({ kind: "master_agent_reply", projectId: "master-agent", requestMessageId: "1", body: "a" }),
  );

  assert.equal(description.backendId, "claw-runtime");
  assert.equal(description.mode, "remote");
});
  • Step 2: 运行测试确认当前失败

Run:

cd /Users/kris/code/boss
npx --yes tsx --test tests/claw-backend.test.ts

Expected:

ERR_MODULE_NOT_FOUND
  • Step 3: 写最小 backend 实现
// /Users/kris/code/boss/src/lib/execution/backends/claw-backend.ts
import type { ExecutionBackend } from "@/lib/execution/execution-backend";
import type { ExecutionRequest } from "@/lib/execution/types";
import { getClawBackendConfig, isClawBackendConfigured } from "@/lib/execution/backends/claw-config";
import { runClawCommand } from "@/lib/execution/backends/claw-runner";

function isSupportedKind(kind: ExecutionRequest["kind"]) {
  return kind === "master_agent_reply" || kind === "thread_reply";
}

export function createClawBackend(config = getClawBackendConfig()): ExecutionBackend {
  return {
    backendId: "claw-runtime",
    async canHandle(input) {
      return isClawBackendConfigured(config) && isSupportedKind(input.kind);
    },
    async execute(input) {
      return runClawCommand({
        config,
        payload: {
          kind: input.kind,
          projectId: input.projectId,
          requestMessageId: input.requestMessageId,
          body: input.body,
          targetProjectId: input.targetProjectId,
          targetThreadId: input.targetThreadId,
          modelOverride: input.modelOverride ?? config.defaultModel,
          reasoningEffortOverride: input.reasoningEffortOverride,
        },
      });
    },
    async describe() {
      return {
        backendId: "claw-runtime",
        label: "Claw Runtime",
        mode: "remote",
      };
    },
  };
}

export const CLAW_BACKEND = createClawBackend();
export const createClawBackendForTesting = createClawBackend;
  • Step 4: 再跑测试确认通过

Run:

cd /Users/kris/code/boss
npx --yes tsx --test tests/claw-backend.test.ts

Expected:

# pass 3
  • Step 5: 提交
cd /Users/kris/code/boss
git add tests/claw-backend.test.ts src/lib/execution/backends/claw-backend.ts
git commit -m "feat: add claw backend adapter"

Task 4: 扩展 selectorclaw 仅在显式启用时参与候选

Files:

  • Modify: /Users/kris/code/boss/src/lib/execution/backend-selector.ts

  • Modify: /Users/kris/code/boss/src/lib/execution/backends/openai-backend.ts

  • Modify: /Users/kris/code/boss/src/lib/execution/backends/master-codex-node-backend.ts

  • Modify: /Users/kris/code/boss/src/lib/execution/backends/aliyun-qwen-backend.ts

  • Create: /Users/kris/code/boss/tests/claw-backend-selector.test.ts

  • Step 1: 写失败测试,固定 selector 行为不被 claw 默认影响

import test from "node:test";
import assert from "node:assert/strict";
import { listExecutionBackendChoices } from "../src/lib/execution/backend-selector.ts";

test("claw 未启用时 selector 行为保持不变", () => {
  delete process.env.BOSS_CLAW_ENABLED;
  delete process.env.BOSS_CLAW_COMMAND;

  const choices = listExecutionBackendChoices({
    primary: { provider: "master_codex_node", status: "ready" },
    backups: [{ provider: "openai_api", status: "ready" }],
  });

  assert.equal(choices[0]?.backendId, "master-codex-node");
  assert.equal(choices.some((item) => item.backendId === "claw-runtime"), false);
});

test("claw 启用且显式选中时会进入候选列表顶部", () => {
  process.env.BOSS_CLAW_ENABLED = "true";
  process.env.BOSS_CLAW_COMMAND = "claw";

  const choices = listExecutionBackendChoices({
    primary: { provider: "master_codex_node", status: "ready" },
    backups: [],
    requestedBackendId: "claw-runtime",
    requestKind: "master_agent_reply",
  });

  assert.equal(choices[0]?.backendId, "claw-runtime");
});
  • Step 2: 运行测试确认当前失败

Run:

cd /Users/kris/code/boss
npx --yes tsx --test tests/claw-backend-selector.test.ts

Expected:

FAIL
  • Step 3: 扩展 selector 输入与 claw 候选插入规则
// /Users/kris/code/boss/src/lib/execution/backend-selector.ts
// 在现有 ExecutionBackendSelectionInput 上补:
requestedBackendId?: string;
requestKind?: "master_agent_reply" | "thread_reply" | "dispatch_execution" | "attachment_analysis";

// 新增 Claw backend descriptor并在 listExecutionBackendChoices 中:
// 1. 默认不插入 claw
// 2. 只有当 requestedBackendId === "claw-runtime" 且 requestKind 可支持 且 claw 配置完整时,才 push 到最前面

要求:

  • 不改变当前不带 requestedBackendId 的所有既有行为

  • 不让 dispatch_execution 进入 claw 候选

  • 不让未启用的 claw 影响任何现有选择顺序

  • Step 4: 再跑 selector 测试

Run:

cd /Users/kris/code/boss
npx --yes tsx --test tests/execution-backend-selector.test.ts tests/claw-backend-selector.test.ts

Expected:

# pass
  • Step 5: 提交
cd /Users/kris/code/boss
git add tests/claw-backend-selector.test.ts tests/execution-backend-selector.test.ts src/lib/execution/backend-selector.ts src/lib/execution/backends/openai-backend.ts src/lib/execution/backends/master-codex-node-backend.ts src/lib/execution/backends/aliyun-qwen-backend.ts
 git commit -m "refactor: support optional claw backend selection"

Task 5: 把 boss-master-agent 接到可选 claw backend但默认保持原主链

Files:

  • Modify: /Users/kris/code/boss/src/lib/boss-master-agent.ts

  • Test: /Users/kris/code/boss/tests/master-agent-claw-backend.test.ts

  • Step 1: 写失败测试,固定“默认不走 claw显式选中才走”

import test from "node:test";
import assert from "node:assert/strict";
import { readState, saveAiAccount } from "../src/lib/boss-data.ts";
import { replyToMasterAgentUserMessage } from "../src/lib/boss-master-agent.ts";

test("master-agent 默认不因为 claw 启用而改变现有后端选择", async () => {
  process.env.BOSS_CLAW_ENABLED = "true";
  process.env.BOSS_CLAW_COMMAND = "claw";

  const result = await replyToMasterAgentUserMessage({
    requestMessageId: "msg-1",
    requestText: "继续",
    requestedBy: "Boss 超级管理员",
    requestedByAccount: "17600003315",
  });

  assert.notEqual(result.accountId, "claw-runtime");
});

再补一条可测试 helper 场景:

// 新增一个 resolveMasterAgentBackendForTesting(...) helper
// 显式传入 requestedBackendId="claw-runtime" 时,返回 claw backend
  • Step 2: 运行测试确认失败

Run:

cd /Users/kris/code/boss
npx --yes tsx --test tests/master-agent-claw-backend.test.ts

Expected:

FAIL
  • Step 3: 在 boss-master-agent 中引入 requestedBackendId 但不改变默认行为

实现要求:

  • 当前默认调用路径不传 requestedBackendId

  • 只在后续显式会话配置存在时,才把 requestedBackendId="claw-runtime" 传给 selector

  • 本轮不新增前台 UI只把内部能力接好

  • 如果 selector 选中了 claw-runtime,则调用 CLAW_BACKEND.execute(...)

  • 如果 claw 返回 failed,继续沿用现有后端回退逻辑

  • Step 4: 再跑 master-agent 相关测试

Run:

cd /Users/kris/code/boss
npx --yes tsx --test tests/master-agent-claw-backend.test.ts tests/master-agent-openai-fallback.test.ts tests/master-agent-message-queue.test.ts tests/master-agent-config-resolution.test.ts

Expected:

# pass
  • Step 5: 提交
cd /Users/kris/code/boss
git add tests/master-agent-claw-backend.test.ts src/lib/boss-master-agent.ts
git commit -m "feat: wire optional claw backend into master agent"

Task 6: 跑完整验证并补文档

Files:

  • Modify: /Users/kris/code/boss/README.md

  • Modify: /Users/kris/code/boss/docs/architecture/current_runtime_and_deploy_status_cn.md

  • Modify: /Users/kris/code/boss/docs/architecture/api_and_service_inventory_cn.md

  • Step 1: 跑本轮完整验证矩阵

Run:

cd /Users/kris/code/boss
npx --yes tsx --test \
  tests/claw-backend-config.test.ts \
  tests/claw-runner.test.ts \
  tests/claw-backend.test.ts \
  tests/claw-backend-selector.test.ts \
  tests/master-agent-claw-backend.test.ts \
  tests/execution-backend-selector.test.ts \
  tests/master-agent-openai-fallback.test.ts \
  tests/master-agent-message-queue.test.ts \
  tests/master-agent-config-resolution.test.ts \
  tests/remote-runtime-adapter.test.ts \
  tests/dispatch-execution-result.test.ts

node --test tests/local-agent-codex-task-runner.test.mjs
npm run lint
npm run build

Expected:

All tests pass
Next.js build succeeds
  • Step 2: 更新文档,明确 claw 已接入为可选 backend但默认不启用

至少补充:

- 当前 Boss 已新增 `ClawBackendAdapter`
- 当前 `claw-code` 仅作为可选单次执行 backend
- 当前默认主链仍不是 claw
- 当前 claw 只覆盖 `master_agent_reply / thread_reply`
- 当前未接群聊审批、设备导入、多租户账本
  • Step 3: 再跑一次 lint/build 确认最终状态一致

Run:

cd /Users/kris/code/boss
npm run lint
npm run build

Expected:

No new errors
  • Step 4: 提交
cd /Users/kris/code/boss
git add README.md docs/architecture/current_runtime_and_deploy_status_cn.md docs/architecture/api_and_service_inventory_cn.md
git commit -m "docs: record optional claw backend support"

Self-Review

Spec coverage

  • 已覆盖 ClawBackendAdapter 默认关闭
  • 已覆盖最小接入范围只限单次执行
  • 已覆盖 CLI/JSON 协议
  • 已覆盖 selector 显式选择与失败回退
  • 已覆盖文档同步

Placeholder scan

  • 无 TBD / TODO / implement later
  • 每个任务都有明确文件、测试、命令和提交边界

Type consistency

  • ClawBackendConfig / ExecutionRequest / ExecutionImmediateResult / requestedBackendId 命名保持一致
  • claw-runtime backendId 在任务间保持一致

Execution Handoff

Plan complete and saved to /Users/kris/code/boss/docs/superpowers/plans/2026-04-03-claw-backend-adapter.md. Two execution options:

1. Subagent-Driven (recommended) - I dispatch a fresh subagent per task, review between tasks, fast iteration

2. Inline Execution - Execute tasks in this session using executing-plans, batch execution with checkpoints

Which approach?