From 8daaea01fd3369745b1b4aec1bd23d1ade5f80a2 Mon Sep 17 00:00:00 2001 From: kris Date: Fri, 3 Apr 2026 00:48:51 +0800 Subject: [PATCH] docs: add claw backend adapter plan --- .../plans/2026-04-03-claw-backend-adapter.md | 760 ++++++++++++++++++ 1 file changed, 760 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-03-claw-backend-adapter.md diff --git a/docs/superpowers/plans/2026-04-03-claw-backend-adapter.md b/docs/superpowers/plans/2026-04-03-claw-backend-adapter.md new file mode 100644 index 0000000..24999f7 --- /dev/null +++ b/docs/superpowers/plans/2026-04-03-claw-backend-adapter.md @@ -0,0 +1,760 @@ +# 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: 写失败测试,固定默认关闭与配置解析行为** + +```ts +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: + +```bash +cd /Users/kris/code/boss +npx --yes tsx --test tests/claw-backend-config.test.ts +``` + +Expected: + +```text +ERR_MODULE_NOT_FOUND +``` + +- [ ] **Step 3: 写最小配置解析实现** + +```ts +// /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: + +```bash +cd /Users/kris/code/boss +npx --yes tsx --test tests/claw-backend-config.test.ts +``` + +Expected: + +```text +# pass 2 +``` + +- [ ] **Step 5: 提交** + +```bash +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 协议映射** + +```ts +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: + +```bash +cd /Users/kris/code/boss +npx --yes tsx --test tests/claw-runner.test.ts +``` + +Expected: + +```text +ERR_MODULE_NOT_FOUND +``` + +- [ ] **Step 3: 写最小 runner 实现** + +```ts +// /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 { + 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: + +```bash +cd /Users/kris/code/boss +npx --yes tsx --test tests/claw-runner.test.ts +``` + +Expected: + +```text +# pass 3 +``` + +- [ ] **Step 5: 提交** + +```bash +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: 写失败测试,固定可处理范围和描述信息** + +```ts +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: + +```bash +cd /Users/kris/code/boss +npx --yes tsx --test tests/claw-backend.test.ts +``` + +Expected: + +```text +ERR_MODULE_NOT_FOUND +``` + +- [ ] **Step 3: 写最小 backend 实现** + +```ts +// /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: + +```bash +cd /Users/kris/code/boss +npx --yes tsx --test tests/claw-backend.test.ts +``` + +Expected: + +```text +# pass 3 +``` + +- [ ] **Step 5: 提交** + +```bash +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: 扩展 selector,让 `claw` 仅在显式启用时参与候选 + +**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 默认影响** + +```ts +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: + +```bash +cd /Users/kris/code/boss +npx --yes tsx --test tests/claw-backend-selector.test.ts +``` + +Expected: + +```text +FAIL +``` + +- [ ] **Step 3: 扩展 selector 输入与 claw 候选插入规则** + +```ts +// /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: + +```bash +cd /Users/kris/code/boss +npx --yes tsx --test tests/execution-backend-selector.test.ts tests/claw-backend-selector.test.ts +``` + +Expected: + +```text +# pass +``` + +- [ ] **Step 5: 提交** + +```bash +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,显式选中才走”** + +```ts +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 场景: + +```ts +// 新增一个 resolveMasterAgentBackendForTesting(...) helper +// 显式传入 requestedBackendId="claw-runtime" 时,返回 claw backend +``` + +- [ ] **Step 2: 运行测试确认失败** + +Run: + +```bash +cd /Users/kris/code/boss +npx --yes tsx --test tests/master-agent-claw-backend.test.ts +``` + +Expected: + +```text +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: + +```bash +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: + +```text +# pass +``` + +- [ ] **Step 5: 提交** + +```bash +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: + +```bash +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: + +```text +All tests pass +Next.js build succeeds +``` + +- [ ] **Step 2: 更新文档,明确 claw 已接入为可选 backend,但默认不启用** + +至少补充: + +```md +- 当前 Boss 已新增 `ClawBackendAdapter` +- 当前 `claw-code` 仅作为可选单次执行 backend +- 当前默认主链仍不是 claw +- 当前 claw 只覆盖 `master_agent_reply / thread_reply` +- 当前未接群聊审批、设备导入、多租户账本 +``` + +- [ ] **Step 3: 再跑一次 lint/build 确认最终状态一致** + +Run: + +```bash +cd /Users/kris/code/boss +npm run lint +npm run build +``` + +Expected: + +```text +No new errors +``` + +- [ ] **Step 4: 提交** + +```bash +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?**