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:
// 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/templates/_registry.json 注册时声明能调哪些工具。Claude CLI 启动时 --allowedTools 参数据此生成。
// 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 自觉。
// 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 用了哪些工具