21 KiB
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: 扩展 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 默认影响
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-runtimebackendId 在任务间保持一致
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?