① 现状 · 三层工具栈
Claude CLI 调工具走三层,越靠下越底层:
关键点:本次不动 L1 / L2。只在 L2 注册层追加 79 个 L3 工具定义。Claude CLI 进程启动时被告知"你可以调这些 + 既有 Read/Write/Bash",调用时 ToolRegistry 路由到对应 execute 函数。
② 79 个工具按目录组织
server/src/tools/ ├── tool-registry.js # 既有,加载所有工具 (REUSE) ├── read-tools.js # 既有 (REUSE) ├── call-agent-tool.js # 既有 (REUSE) ├── orchestrate-tool.js # 既有 (REUSE) ├── marketplace-tools.js # 既有 (REUSE) ├── train-skill-tool.js # 既有 (REUSE) └── devloop/ # NEW 整个目录 ├── persistence-tools.js # 持久化 5 件套 (M-10/11/12/13) │ ├── write_todolist │ ├── update_todo_item │ ├── update_runtime_state │ ├── timeline_append │ └── record_memory ├── doc-tools.js # PM / Architect 文档工具 │ ├── bootstrap_prd_site / bootstrap_sdd_site │ ├── write_chapter / write_mvcp_page │ ├── archive_doc │ ├── diff_html / diff_units │ ├── echo_diff │ ├── update_search_index │ ├── validate_prd_site / validate_sdd_consistency │ └── hash_contract ├── coder-tools.js # Coder 工具 │ ├── contract_bridge │ ├── sandbox_check │ ├── append_coder_log │ └── (jsonGuard 是 Bash 中间件,不在工具表) ├── tester-tools.js # Tester 工具 │ ├── parse_vitest │ ├── hint_builder │ ├── bug_open │ ├── bug_transition │ └── bug_list ├── integrator-tools.js # Integrator 工具 │ ├── port_picker │ ├── spawn_units │ ├── health_wait │ ├── run_case │ ├── parse_placeholder │ └── collect_stderr └── triager-tools.js # BugTriager 工具 ├── extract_evidence (→ verify_evidence) ├── category_classifier ├── diff_contract_vs_impl └── find_similar_bug
合计 79 个工具中:持久化 5 + doc 11 + coder 3 + tester 5 + integrator 6 + triager 4 = 34 个独立函数(很多 Agent 复用,所以签名是 79 但唯一实现是 34)。每个 ≈ 30-80 行 JS,总实现量 ≈ 1500-2500 行。
③ 标准 wrapper 模板(直接照抄)
每个工具都按下面这个模式注册到 tool-registry.js:
// server/src/tools/devloop/persistence-tools.js · 节选 const { Type } = require('@sinclair/typebox'); const fs = require('fs'); const path = require('path'); const { projectsDir } = require('../../services/paths'); /** * 每个工具导出一个 ToolDefinition 对象: * { name, description, inputSchema, execute } * 由 tool-registry.js 收集后注入 Claude CLI 启动参数。 */ module.exports.write_todolist = { name: 'write_todolist', description: '初始化 agent-state/{agent}-todo.json,写入完整 todolist', inputSchema: { type: 'object', required: ['items'], properties: { items: { type: 'array', items: { type: 'object', required: ['id', 'description'], properties: { id: { type: 'string', pattern: '^step-\\d{3}$' }, description: { type: 'string' }, depends_on: { type: 'array', items: { type: 'string' } } } } } } }, // ctx 由 ToolRegistry 注入,含 pid · agent · call_id · sessionId · loopController · sseBroker async execute({ items }, ctx) { const { pid, agent, call_id } = ctx; const p = path.join(projectsDir(pid), 'agent-state', `${agent}-todo.json`); const todolist = { agent, call_id, created_at: new Date().toISOString(), items: items.map(it => ({ ...it, status: 'todo' })), current: items[0]?.id || null, progress_pct: 0, }; fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, JSON.stringify(todolist, null, 2)); ctx.sseBroker?.emit(pid, 'agent.todolist', { agent, call_id, items: todolist.items, }); return { ok: true, total: items.length, path: p }; } };
三个不变量每个 wrapper 都要遵守:
inputSchema是JSON Schema(Claude CLI native tool_use 格式),不是 TypeBox(虽然 TypeBox 可以序列化到 JSON Schema,方便统一来源)execute接收第二个参数 ctx,含 pid · agent · call_id · sessionId 等运行时上下文(由 LoopController 注入)- 返回值必须是纯 JSON 可序列化对象,给 Claude 看(不能含 Buffer / Stream / Function)
④ tool-registry.js 改造
LemonBoss 既有 server/src/tools/tool-registry.js,只加载新模块即可:
// server/src/tools/tool-registry.js · 仅追加 1 段 const { readDirImports } = require('./_util'); function loadAllTools() { const registry = new Map(); // 既有(REUSE): register(registry, require('./read-tools')); register(registry, require('./call-agent-tool')); register(registry, require('./orchestrate-tool')); register(registry, require('./marketplace-tools')); register(registry, require('./train-skill-tool')); // NEW: devloop 工具批量加载 for (const mod of readDirImports('./devloop')) { register(registry, mod); } return registry; } function register(reg, mod) { for (const key of Object.keys(mod)) { const tool = mod[key]; if (tool && tool.name && tool.execute) { reg.set(tool.name, tool); } } } module.exports = { loadAllTools };
现有 PluginManager 已经在 server 启动时 require tool-registry,所以本改动不需要改 server/index.js。
⑤ Agent 模板的 toolPolicy
每个 Agent 在 system/agents/_registry.json 注册时声明能调哪些工具。Claude CLI 启动时 --allowedTools 参数据此生成。
// system/agents/_registry.json · 节选(追加 6 条) { "templates": [ { "id": "devloop-pm", "name": "PM Agent", "type": "executor", "devloop": { "loop": "L3", "upstream": ["human"], "downstream": ["architect"] }, "cli": { "model": "claude-sonnet-4-6", "systemPromptFile": "system/agents/devloop-pm/prompt.md", "toolPolicy": [ "Read", "Write", "bootstrap_prd_site", "write_chapter", "archive_doc", "diff_html", "update_search_index", "validate_prd_site", "echo_diff", "write_todolist", "update_todo_item", "update_runtime_state", "timeline_append", "record_memory" ] }, "sandbox": { "cwd": "data/companies/{cid}/projects/{pid}/", "writeAllowList": ["prd/**", "memory/devloop/pm/**", "agent-state/**", "timeline.ndjson"] } }, { "id": "devloop-architect", ... }, { "id": "devloop-coder", "cli": { "model": "claude-sonnet-4-6", "toolPolicy": [ "Read", "Edit", "Write", "Bash", "hash_contract", "contract_bridge", "sandbox_check", "append_coder_log", "write_todolist", .../* 持久化 5 件套 */ ] }, ... }, { "id": "devloop-tester", ... }, { "id": "devloop-integrator", ... }, { "id": "devloop-bug-triager",... } ] }
关键观察:PM 不允许 Bash(不需要跑命令),Coder/Tester 允许 Bash(要跑 vitest / npm),Integrator 允许 Bash(要 spawn 进程),BugTriager 不允许 Write/Bash(只读 + 写 triage.json)。
⑥ Bash 工具的 jsonGuard 中间件
Coder / Tester / Integrator 都被允许 Bash。但禁止 inline JSON(CLAUDE.md L91-99 强约束)。这通过 Bash 工具的命令预检中间件实现,不依赖 Agent 自觉。
// server/src/tools/devloop/bash-json-guard.js · ~50 行 const INLINE_JSON_PATTERN = /-d\s+['"]\s*\{[\s\S]*?\}\s*['"]/; function jsonGuard(cmd) { if (INLINE_JSON_PATTERN.test(cmd)) { return { ok: false, reason: 'inline JSON in Bash command is forbidden (see CLAUDE.md L91-99). ' + 'Use: echo \'{"k":"v"}\' > /tmp/x.json && curl -d @/tmp/x.json', }; } return { ok: true }; } // 注册到 Bash 工具的 preExecuteHooks module.exports.bashHooks = { preExecute: [jsonGuard], };
挂载点:LemonBoss 既有的 Bash 工具实现里加一行 const checks = require('./devloop/bash-json-guard').bashHooks.preExecute;,在 execute 前跑一遍,任一返回 ok=false 直接拒绝。Agent 收到 error,自然走 retry 改正。
⑦ ctx 的内容(ToolRegistry 注入)
每次工具被调用,execute(input, ctx) 的 ctx 由 LoopController 在 callAgent 时设置:
const ctx = { // 标识 pid: "p-test001", agent: "pm" | "architect" | ..., unit_id: "todo-be", // 只有 Coder/Tester 有 call_id: "call-...", session_id: "sess-...", // ACP session // 注入的服务 loopController, // 用于 update_runtime_state 等 docStore, unitRegistry, bugTracker, timelineLogger, sseBroker, agentManager, // 工具内部一般不调,但有就备着 // 配置 companyDir, // data/companies/{cid}/ projectDir, // data/companies/{cid}/projects/{pid}/ unitDir, // (Coder/Tester) projects/{pid}/units/{unit_id}/ // hooks log: (level, msg) => ..., // 写 timeline 简化版 };
所有 wrapper 都通过 ctx 获取依赖,不要直接 require server/services/xxx。这样工具可以脱离运行时单测(mock ctx 即可)。
⑧ 错误传播
工具 execute 的失败有三种,Agent 看到的形态不同:
| 失败类型 | execute 返回 | Agent 看到 | Agent 该怎么办 |
|---|---|---|---|
| 预期错误(合法但失败) | { ok: false, reason: "..." } | JSON 含 ok=false | 读 reason 调整后重试 |
| 沙箱越界 / Schema 错 | 抛 ValidationError | tool_use error + 详细说明 | 必须改路径 / payload |
| 系统崩(fs 失败、磁盘满) | 抛 Error | tool_use error + stack | 报 LoopController 升 L3 |
// 标准化错误(所有 wrapper 复用) class ToolValidationError extends Error { constructor(field, reason) { super(`${field}: ${reason}`); this.field = field; this.reason = reason; } } class ToolSandboxError extends Error { constructor(path, agent) { super(`Sandbox violation:${agent} tried to write ${path}`); } }
⑨ ToolRegistry 给 Claude CLI 的注入
ACP 启动 Claude CLI 时,把 toolPolicy 翻译为 CLI 参数:
// packages/acp-core/protocol/ClaudeCliProtocol.js · 增量改动 function buildCliArgs({ agent, toolPolicy, toolRegistry }) { const tools = toolPolicy.map(name => { const def = toolRegistry.get(name); if (!def) throw new Error(`unknown tool: ${name}`); return { name: def.name, description: def.description, input_schema: def.inputSchema, }; }); // 写入临时 tools.json,作为 --tools 参数传给 claude CLI const toolsFile = `/tmp/devloop-tools-${agent}-${Date.now()}.json`; fs.writeFileSync(toolsFile, JSON.stringify({ tools })); return [ '--dangerously-skip-permissions', // 既有约定 '--tools', toolsFile, // Claude CLI 加载工具定义 '--tool-handler', 'jsonrpc://stdio', // 工具调用通过 stdio JSON-RPC 回到 server ]; }
Claude CLI 调任一工具时,通过 stdio 发 JSON-RPC 请求给 ACP,ACP 路由到 ToolRegistry.execute(name, input, ctx),返回值再 JSON-RPC 回 CLI。整个过程对 Agent 透明,它看到的就是标准 tool_use。
⑩ Phase 0 验收(接 plan-migration)
Phase 0 完成的判据是一个最小调用回环跑通:
# Phase 0 验证脚本: scripts/phase0-verify.js node -e " const { loadAllTools } = require('./server/src/tools/tool-registry'); const reg = loadAllTools(); const want = ['write_todolist','update_todo_item','hash_contract','bug_open', 'archive_doc','update_search_index','validate_prd_site']; for (const w of want) { const t = reg.get(w); if (!t) { console.error('MISSING:', w); process.exit(1); } console.log('✓', w, '·', t.description.slice(0,40)); } console.log('All', want.length, 'critical tools registered'); " # 期望: ✓ 所有 7 个 critical 工具就位 # 再跑一个最小 Agent 调用 (用 stub Claude CLI 模式) node scripts/phase0-test-call.js # 内部:用 mock ACP session 调 write_todolist,验证文件落盘 + SSE 事件发出
⑪ 工时估算(Phase 0)
| 任务 | 工时 | 难度 |
|---|---|---|
| 持久化 5 件套(persistence-tools.js) | 1.5 天 | 低 |
| doc-tools(11 个工具,PM/Architect 用) | 2.5 天 | 中(cheerio 操作多) |
| coder/tester/integrator/triager-tools(18 个) | 2 天 | 中 |
| extract-agent-prompts.js(HTML → md) | 0.5 天 | 低(正则) |
| _registry.json 加 6 条 + toolPolicy | 0.5 天 | 低 |
| ClaudeCliProtocol --tools 注入 | 1 天 | 中(要看 Claude CLI 的实际 tool 接口) |
| jsonGuard 中间件挂 Bash 工具 | 0.5 天 | 低 |
| phase0-verify.js + phase0-test-call.js | 1 天 | 低 |
| 调试 + 联调 | 2 天 | — |
| 合计 | ≈ 11.5 天 (2.5 周) | — |
P0 完成后,PM Agent 才能真的跑出 prd/ 站点 — 这是 P1 的前置条件。
⑫ 关联页面
- ACP Bridge · LoopController 怎么调到这层
- 最小改造路径 · Phase 0 在那里被引用
- PM / Architect · 看每个 Agent 用了哪些工具