# Tool Registry · 79 个工具如何变成可调用的 JS > 6 个 Agent skill 描述了 79 个工具签名(write_todolist · hash_contract · bug_open ...)但这些不是 Claude CLI 自带。本页讲清楚**它们怎么从签名变成实际可调用的 JS**,桥接到 LemonBoss 既有的 `tool-registry.js`,让 Claude CLI 通过 tool_use API 真的能调到。 ## ① 现状 · 三层工具栈 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`: ```ts // 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 都要遵守: 1. `inputSchema` 是**JSON Schema**(Claude CLI native tool_use 格式),不是 TypeBox(虽然 TypeBox 可以序列化到 JSON Schema,方便统一来源) 2. `execute` 接收**第二个参数 ctx**,含 pid · agent · call_id · sessionId 等运行时上下文(由 LoopController 注入) 3. 返回值必须是**纯 JSON 可序列化**对象,给 Claude 看(不能含 Buffer / Stream / Function) ## ④ tool-registry.js 改造 LemonBoss 既有 `server/src/tools/tool-registry.js`,**只加载新模块即可**: ```ts // 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/templates/_registry.json` 注册时声明**能调哪些工具**。Claude CLI 启动时 `--allowedTools` 参数据此生成。 ```ts // system/templates/_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/templates/devloop-pm.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 自觉。 ```ts // 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 时设置: ```ts 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 | ```ts // 标准化错误(所有 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 参数: ```ts // 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 完成的判据是**一个最小调用回环**跑通: ```js # 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](arch-acp-bridge.html) · LoopController 怎么调到这层 - [最小改造路径](plan-migration.html) · Phase 0 在那里被引用 - [PM](agents-pm.html) / [Architect](agents-architect.html) · 看每个 Agent 用了哪些工具