arch-tool-registry.html
28.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Tool Registry · 工具运行时实现 · LemonBoss PRD</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,500;9..144,700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="assets/shell.css" />
</head>
<body data-page="arch-tool-registry.html">
<div class="shell">
<aside id="sidebar"></aside>
<main class="page">
<header class="page-head">
<div class="crumb"><span class="pill">05 · ARCHITECTURE</span><span class="sep">/</span><span>Tool Registry</span></div>
<h1>Tool Registry · 79 个工具如何变成可调用的 JS</h1>
<p class="deck">6 个 Agent skill 描述了 79 个工具签名(write_todolist · hash_contract · bug_open ...)但这些不是 Claude CLI 自带。本页讲清楚<b>它们怎么从签名变成实际可调用的 JS</b>,桥接到 LemonBoss 既有的 <code>tool-registry.js</code>,让 Claude CLI 通过 tool_use API 真的能调到。</p>
</header>
<article class="body">
<section>
<h2>① 现状 · 三层工具栈</h2>
<p>Claude CLI 调工具走<b>三层</b>,越靠下越底层:</p>
<svg viewBox="0 0 1100 320" width="100%" role="img" aria-label="三层工具栈">
<defs><marker id="arrTR" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 Z" fill="#0E0E10"/></marker></defs>
<g font-family="JetBrains Mono" font-size="11" fill="#0E0E10">
<rect x="40" y="30" width="1020" height="60" rx="6" fill="#FCE7E2" stroke="#C7341A"/>
<text x="550" y="55" text-anchor="middle" font-weight="700">L3 · DevLoop 业务工具(79 个,NEW)</text>
<text x="550" y="75" text-anchor="middle" font-size="10">write_todolist · hash_contract · bug_open · update_search_index · ... 见 §②</text>
<rect x="40" y="120" width="1020" height="60" rx="6" fill="#EAEAEA" stroke="#666"/>
<text x="550" y="145" text-anchor="middle" font-weight="700">L2 · LemonBoss 工具注册层(既有 REUSE)</text>
<text x="550" y="165" text-anchor="middle" font-size="10">server/src/tools/tool-registry.js · PluginManager 加载 · ToolRegistry.register({name, schema, execute})</text>
<rect x="40" y="210" width="1020" height="60" rx="6" fill="#fff" stroke="#0E0E10"/>
<text x="550" y="235" text-anchor="middle" font-weight="700">L1 · ACP / Claude CLI 原生 tool_use(最底层)</text>
<text x="550" y="255" text-anchor="middle" font-size="10">packages/acp-core/protocol/ClaudeCliProtocol.js + claude CLI 的 --allowedTools + native tool_use schema</text>
<g stroke="#0E0E10" fill="none" marker-end="url(#arrTR)">
<line x1="550" y1="90" x2="550" y2="115"/>
<line x1="550" y1="180" x2="550" y2="205"/>
</g>
</g>
</svg>
<p>关键点:本次<b>不动 L1 / L2</b>。只在 L2 注册层<b>追加</b> 79 个 L3 工具定义。Claude CLI 进程启动时被告知"你可以调这些 + 既有 Read/Write/Bash",调用时 ToolRegistry 路由到对应 execute 函数。</p>
</section>
<section>
<h2>② 79 个工具按目录组织</h2>
<pre class="tree">
server/src/tools/
├── tool-registry.js <span class="com"># 既有,加载所有工具 (REUSE)</span>
├── read-tools.js <span class="com"># 既有 (REUSE)</span>
├── call-agent-tool.js <span class="com"># 既有 (REUSE)</span>
├── orchestrate-tool.js <span class="com"># 既有 (REUSE)</span>
├── marketplace-tools.js <span class="com"># 既有 (REUSE)</span>
├── train-skill-tool.js <span class="com"># 既有 (REUSE)</span>
└── <span class="new">devloop/</span> <span class="com"># NEW 整个目录</span>
├── persistence-tools.js <span class="com"># 持久化 5 件套 (M-10/11/12/13)</span>
│ ├── write_todolist
│ ├── update_todo_item
│ ├── update_runtime_state
│ ├── timeline_append
│ └── record_memory
├── doc-tools.js <span class="com"># PM / Architect 文档工具</span>
│ ├── 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 <span class="com"># Coder 工具</span>
│ ├── contract_bridge
│ ├── sandbox_check
│ ├── append_coder_log
│ └── (jsonGuard 是 Bash 中间件,不在工具表)
├── tester-tools.js <span class="com"># Tester 工具</span>
│ ├── parse_vitest
│ ├── hint_builder
│ ├── bug_open
│ ├── bug_transition
│ └── bug_list
├── integrator-tools.js <span class="com"># Integrator 工具</span>
│ ├── port_picker
│ ├── spawn_units
│ ├── health_wait
│ ├── run_case
│ ├── parse_placeholder
│ └── collect_stderr
└── triager-tools.js <span class="com"># BugTriager 工具</span>
├── extract_evidence (→ verify_evidence)
├── category_classifier
├── diff_contract_vs_impl
└── find_similar_bug
</pre>
<p class="sig">合计 79 个工具中:持久化 5 + doc 11 + coder 3 + tester 5 + integrator 6 + triager 4 = <b>34 个独立函数</b>(很多 Agent 复用,所以签名是 79 但唯一实现是 34)。<b>每个 ≈ 30-80 行 JS</b>,总实现量 ≈ 1500-2500 行。</p>
</section>
<section>
<h2>③ 标准 wrapper 模板(直接照抄)</h2>
<p>每个工具都按下面这个模式注册到 <code>tool-registry.js</code>:</p>
<pre class="code shape"><span class="c">// server/src/tools/devloop/persistence-tools.js · 节选</span>
<span class="k">const</span> <span class="p">{</span> Type <span class="p">}</span> = <span class="n">require</span>(<span class="s">'@sinclair/typebox'</span>);
<span class="k">const</span> fs = <span class="n">require</span>(<span class="s">'fs'</span>);
<span class="k">const</span> path = <span class="n">require</span>(<span class="s">'path'</span>);
<span class="k">const</span> <span class="p">{</span> projectsDir <span class="p">}</span> = <span class="n">require</span>(<span class="s">'../../services/paths'</span>);
<span class="c">/**
* 每个工具导出一个 ToolDefinition 对象:
* { name, description, inputSchema, execute }
* 由 tool-registry.js 收集后注入 Claude CLI 启动参数。
*/</span>
module.<span class="n">exports</span>.write_todolist = <span class="p">{</span>
name: <span class="s">'write_todolist'</span>,
description: <span class="s">'初始化 agent-state/{agent}-todo.json,写入完整 todolist'</span>,
inputSchema: <span class="p">{</span>
type: <span class="s">'object'</span>,
required: <span class="p">[</span><span class="s">'items'</span><span class="p">]</span>,
properties: <span class="p">{</span>
items: <span class="p">{</span>
type: <span class="s">'array'</span>,
items: <span class="p">{</span>
type: <span class="s">'object'</span>,
required: <span class="p">[</span><span class="s">'id'</span>, <span class="s">'description'</span><span class="p">]</span>,
properties: <span class="p">{</span>
id: <span class="p">{</span> type: <span class="s">'string'</span>, pattern: <span class="s">'^step-\\d{3}$'</span> <span class="p">}</span>,
description: <span class="p">{</span> type: <span class="s">'string'</span> <span class="p">}</span>,
depends_on: <span class="p">{</span> type: <span class="s">'array'</span>, items: <span class="p">{</span> type: <span class="s">'string'</span> <span class="p">}</span> <span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>,
<span class="c">// ctx 由 ToolRegistry 注入,含 pid · agent · call_id · sessionId · loopController · sseBroker</span>
<span class="k">async</span> <span class="n">execute</span>(<span class="p">{</span> items <span class="p">}</span>, ctx) <span class="p">{</span>
<span class="k">const</span> <span class="p">{</span> pid, agent, call_id <span class="p">}</span> = ctx;
<span class="k">const</span> p = path.<span class="n">join</span>(<span class="n">projectsDir</span>(pid), <span class="s">'agent-state'</span>, <span class="s">`</span><span class="v">${agent}</span><span class="s">-todo.json`</span>);
<span class="k">const</span> todolist = <span class="p">{</span>
agent, call_id, created_at: <span class="k">new</span> <span class="n">Date</span>().<span class="n">toISOString</span>(),
items: items.<span class="n">map</span>(it => (<span class="p">{</span> ...it, status: <span class="s">'todo'</span> <span class="p">}</span>)),
current: items[<span class="v">0</span>]?.id || <span class="v">null</span>,
progress_pct: <span class="v">0</span>,
<span class="p">}</span>;
fs.<span class="n">mkdirSync</span>(path.<span class="n">dirname</span>(p), <span class="p">{</span> recursive: <span class="v">true</span> <span class="p">}</span>);
fs.<span class="n">writeFileSync</span>(p, <span class="n">JSON.stringify</span>(todolist, <span class="v">null</span>, <span class="v">2</span>));
ctx.sseBroker?.<span class="n">emit</span>(pid, <span class="s">'agent.todolist'</span>, <span class="p">{</span>
agent, call_id, items: todolist.items,
<span class="p">}</span>);
<span class="k">return</span> <span class="p">{</span> ok: <span class="v">true</span>, total: items.<span class="n">length</span>, path: p <span class="p">}</span>;
<span class="p">}</span>
<span class="p">}</span>;</pre>
<p><b>三个不变量</b>每个 wrapper 都要遵守:</p>
<ol>
<li><code>inputSchema</code> 是<b>JSON Schema</b>(Claude CLI native tool_use 格式),不是 TypeBox(虽然 TypeBox 可以序列化到 JSON Schema,方便统一来源)</li>
<li><code>execute</code> 接收<b>第二个参数 ctx</b>,含 pid · agent · call_id · sessionId 等运行时上下文(由 LoopController 注入)</li>
<li>返回值必须是<b>纯 JSON 可序列化</b>对象,给 Claude 看(不能含 Buffer / Stream / Function)</li>
</ol>
</section>
<section>
<h2>④ tool-registry.js 改造</h2>
<p>LemonBoss 既有 <code>server/src/tools/tool-registry.js</code>,<b>只加载新模块即可</b>:</p>
<pre class="code shape"><span class="c">// server/src/tools/tool-registry.js · 仅追加 1 段</span>
<span class="k">const</span> <span class="p">{</span> readDirImports <span class="p">}</span> = <span class="n">require</span>(<span class="s">'./_util'</span>);
<span class="k">function</span> <span class="n">loadAllTools</span>() <span class="p">{</span>
<span class="k">const</span> registry = <span class="k">new</span> <span class="n">Map</span>();
<span class="c">// 既有(REUSE):</span>
<span class="n">register</span>(registry, <span class="n">require</span>(<span class="s">'./read-tools'</span>));
<span class="n">register</span>(registry, <span class="n">require</span>(<span class="s">'./call-agent-tool'</span>));
<span class="n">register</span>(registry, <span class="n">require</span>(<span class="s">'./orchestrate-tool'</span>));
<span class="n">register</span>(registry, <span class="n">require</span>(<span class="s">'./marketplace-tools'</span>));
<span class="n">register</span>(registry, <span class="n">require</span>(<span class="s">'./train-skill-tool'</span>));
<span class="c">// NEW: devloop 工具批量加载</span>
<span class="k">for</span> (<span class="k">const</span> mod <span class="k">of</span> <span class="n">readDirImports</span>(<span class="s">'./devloop'</span>)) <span class="p">{</span>
<span class="n">register</span>(registry, mod);
<span class="p">}</span>
<span class="k">return</span> registry;
<span class="p">}</span>
<span class="k">function</span> <span class="n">register</span>(reg, mod) <span class="p">{</span>
<span class="k">for</span> (<span class="k">const</span> key <span class="k">of</span> <span class="n">Object.keys</span>(mod)) <span class="p">{</span>
<span class="k">const</span> tool = mod[key];
<span class="k">if</span> (tool && tool.name && tool.execute) <span class="p">{</span>
reg.<span class="n">set</span>(tool.name, tool);
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
module.<span class="n">exports</span> = <span class="p">{</span> loadAllTools <span class="p">}</span>;</pre>
<p class="sig">现有 PluginManager 已经在 server 启动时 require tool-registry,所以本改动<b>不需要改 server/index.js</b>。</p>
</section>
<section>
<h2>⑤ Agent 模板的 toolPolicy</h2>
<p>每个 Agent 在 <code>system/templates/_registry.json</code> 注册时声明<b>能调哪些工具</b>。Claude CLI 启动时 <code>--allowedTools</code> 参数据此生成。</p>
<pre class="code shape"><span class="c">// system/templates/_registry.json · 节选(追加 6 条)</span>
<span class="p">{</span>
<span class="n">"templates"</span>: <span class="p">[</span>
<span class="p">{</span>
<span class="n">"id"</span>: <span class="s">"devloop-pm"</span>,
<span class="n">"name"</span>: <span class="s">"PM Agent"</span>,
<span class="n">"type"</span>: <span class="s">"executor"</span>,
<span class="n">"devloop"</span>: <span class="p">{</span>
<span class="n">"loop"</span>: <span class="s">"L3"</span>,
<span class="n">"upstream"</span>: <span class="p">[</span><span class="s">"human"</span><span class="p">]</span>,
<span class="n">"downstream"</span>: <span class="p">[</span><span class="s">"architect"</span><span class="p">]</span>
<span class="p">}</span>,
<span class="n">"cli"</span>: <span class="p">{</span>
<span class="n">"model"</span>: <span class="s">"claude-sonnet-4-6"</span>,
<span class="n">"systemPromptFile"</span>: <span class="s">"system/templates/devloop-pm.md"</span>,
<span class="n">"toolPolicy"</span>: <span class="p">[</span>
<span class="s">"Read"</span>, <span class="s">"Write"</span>,
<span class="s">"bootstrap_prd_site"</span>, <span class="s">"write_chapter"</span>, <span class="s">"archive_doc"</span>,
<span class="s">"diff_html"</span>, <span class="s">"update_search_index"</span>, <span class="s">"validate_prd_site"</span>,
<span class="s">"echo_diff"</span>,
<span class="s">"write_todolist"</span>, <span class="s">"update_todo_item"</span>, <span class="s">"update_runtime_state"</span>,
<span class="s">"timeline_append"</span>, <span class="s">"record_memory"</span>
<span class="p">]</span>
<span class="p">}</span>,
<span class="n">"sandbox"</span>: <span class="p">{</span>
<span class="n">"cwd"</span>: <span class="s">"data/companies/{cid}/projects/{pid}/"</span>,
<span class="n">"writeAllowList"</span>: <span class="p">[</span><span class="s">"prd/**"</span>, <span class="s">"memory/devloop/pm/**"</span>, <span class="s">"agent-state/**"</span>, <span class="s">"timeline.ndjson"</span><span class="p">]</span>
<span class="p">}</span>
<span class="p">}</span>,
<span class="p">{</span> <span class="n">"id"</span>: <span class="s">"devloop-architect"</span>, ... <span class="p">}</span>,
<span class="p">{</span> <span class="n">"id"</span>: <span class="s">"devloop-coder"</span>,
<span class="n">"cli"</span>: <span class="p">{</span>
<span class="n">"model"</span>: <span class="s">"claude-sonnet-4-6"</span>,
<span class="n">"toolPolicy"</span>: <span class="p">[</span>
<span class="s">"Read"</span>, <span class="s">"Edit"</span>, <span class="s">"Write"</span>, <span class="s">"Bash"</span>,
<span class="s">"hash_contract"</span>, <span class="s">"contract_bridge"</span>, <span class="s">"sandbox_check"</span>, <span class="s">"append_coder_log"</span>,
<span class="s">"write_todolist"</span>, ...<span class="p">/* 持久化 5 件套 */</span>
<span class="p">]</span>
<span class="p">}</span>, ...
<span class="p">}</span>,
<span class="p">{</span> <span class="n">"id"</span>: <span class="s">"devloop-tester"</span>, ... <span class="p">}</span>,
<span class="p">{</span> <span class="n">"id"</span>: <span class="s">"devloop-integrator"</span>, ... <span class="p">}</span>,
<span class="p">{</span> <span class="n">"id"</span>: <span class="s">"devloop-bug-triager"</span>,... <span class="p">}</span>
<span class="p">]</span>
<span class="p">}</span></pre>
<p><b>关键观察:</b>PM <b>不允许 Bash</b>(不需要跑命令),Coder/Tester <b>允许 Bash</b>(要跑 vitest / npm),Integrator <b>允许 Bash</b>(要 spawn 进程),BugTriager <b>不允许 Write/Bash</b>(只读 + 写 triage.json)。</p>
</section>
<section>
<h2>⑥ Bash 工具的 jsonGuard 中间件</h2>
<p>Coder / Tester / Integrator 都被允许 Bash。但<b>禁止 inline JSON</b>(CLAUDE.md L91-99 强约束)。这通过 Bash 工具的<b>命令预检中间件</b>实现,不依赖 Agent 自觉。</p>
<pre class="code shape"><span class="c">// server/src/tools/devloop/bash-json-guard.js · ~50 行</span>
<span class="k">const</span> INLINE_JSON_PATTERN = <span class="s">/-d\s+['"]\s*\{[\s\S]*?\}\s*['"]/</span>;
<span class="k">function</span> <span class="n">jsonGuard</span>(cmd) <span class="p">{</span>
<span class="k">if</span> (INLINE_JSON_PATTERN.<span class="n">test</span>(cmd)) <span class="p">{</span>
<span class="k">return</span> <span class="p">{</span>
ok: <span class="v">false</span>,
reason: <span class="s">'inline JSON in Bash command is forbidden (see CLAUDE.md L91-99). '</span> +
<span class="s">'Use: echo \'{"k":"v"}\' > /tmp/x.json && curl -d @/tmp/x.json'</span>,
<span class="p">}</span>;
<span class="p">}</span>
<span class="k">return</span> <span class="p">{</span> ok: <span class="v">true</span> <span class="p">}</span>;
<span class="p">}</span>
<span class="c">// 注册到 Bash 工具的 preExecuteHooks</span>
module.<span class="n">exports</span>.bashHooks = <span class="p">{</span>
preExecute: <span class="p">[</span>jsonGuard<span class="p">]</span>,
<span class="p">}</span>;</pre>
<p>挂载点:LemonBoss 既有的 Bash 工具实现里加一行 <code>const checks = require('./devloop/bash-json-guard').bashHooks.preExecute;</code>,在 execute 前跑一遍,任一返回 ok=false 直接拒绝。Agent 收到 error,自然走 retry 改正。</p>
</section>
<section>
<h2>⑦ ctx 的内容(ToolRegistry 注入)</h2>
<p>每次工具被调用,<code>execute(input, ctx)</code> 的 ctx 由 LoopController 在 callAgent 时设置:</p>
<pre class="code shape"><span class="k">const</span> ctx = <span class="p">{</span>
<span class="c">// 标识</span>
pid: <span class="s">"p-test001"</span>,
agent: <span class="s">"pm"</span> | <span class="s">"architect"</span> | ...,
unit_id: <span class="s">"todo-be"</span>, <span class="c">// 只有 Coder/Tester 有</span>
call_id: <span class="s">"call-..."</span>,
session_id: <span class="s">"sess-..."</span>, <span class="c">// ACP session</span>
<span class="c">// 注入的服务</span>
loopController, <span class="c">// 用于 update_runtime_state 等</span>
docStore,
unitRegistry,
bugTracker,
timelineLogger,
sseBroker,
agentManager, <span class="c">// 工具内部一般不调,但有就备着</span>
<span class="c">// 配置</span>
companyDir, <span class="c">// data/companies/{cid}/</span>
projectDir, <span class="c">// data/companies/{cid}/projects/{pid}/</span>
unitDir, <span class="c">// (Coder/Tester) projects/{pid}/units/{unit_id}/</span>
<span class="c">// hooks</span>
log: (level, msg) => ..., <span class="c">// 写 timeline 简化版</span>
<span class="p">}</span>;</pre>
<p>所有 wrapper 都通过 ctx 获取依赖,不要直接 require server/services/xxx。这样工具可以脱离运行时单测(mock ctx 即可)。</p>
</section>
<section>
<h2>⑧ 错误传播</h2>
<p>工具 execute 的失败有<b>三种</b>,Agent 看到的形态不同:</p>
<table class="dense">
<thead><tr><th>失败类型</th><th>execute 返回</th><th>Agent 看到</th><th>Agent 该怎么办</th></tr></thead>
<tbody>
<tr><td>预期错误(合法但失败)</td><td><code>{ ok: false, reason: "..." }</code></td><td>JSON 含 ok=false</td><td>读 reason 调整后重试</td></tr>
<tr><td>沙箱越界 / Schema 错</td><td>抛 ValidationError</td><td>tool_use error + 详细说明</td><td>必须改路径 / payload</td></tr>
<tr><td>系统崩(fs 失败、磁盘满)</td><td>抛 Error</td><td>tool_use error + stack</td><td>报 LoopController 升 L3</td></tr>
</tbody>
</table>
<pre class="code shape"><span class="c">// 标准化错误(所有 wrapper 复用)</span>
<span class="k">class</span> <span class="n">ToolValidationError</span> <span class="k">extends</span> <span class="n">Error</span> <span class="p">{</span>
<span class="k">constructor</span>(field, reason) <span class="p">{</span>
<span class="k">super</span>(<span class="s">`</span><span class="v">${field}</span><span class="s">: </span><span class="v">${reason}</span><span class="s">`</span>);
<span class="k">this</span>.<span class="n">field</span> = field; <span class="k">this</span>.<span class="n">reason</span> = reason;
<span class="p">}</span>
<span class="p">}</span>
<span class="k">class</span> <span class="n">ToolSandboxError</span> <span class="k">extends</span> <span class="n">Error</span> <span class="p">{</span>
<span class="k">constructor</span>(path, agent) <span class="p">{</span>
<span class="k">super</span>(<span class="s">`Sandbox violation:</span><span class="v">${agent}</span><span class="s"> tried to write </span><span class="v">${path}</span><span class="s">`</span>);
<span class="p">}</span>
<span class="p">}</span></pre>
</section>
<section>
<h2>⑨ ToolRegistry 给 Claude CLI 的注入</h2>
<p>ACP 启动 Claude CLI 时,把 toolPolicy 翻译为 CLI 参数:</p>
<pre class="code shape"><span class="c">// packages/acp-core/protocol/ClaudeCliProtocol.js · 增量改动</span>
<span class="k">function</span> <span class="n">buildCliArgs</span>(<span class="p">{</span> agent, toolPolicy, toolRegistry <span class="p">}</span>) <span class="p">{</span>
<span class="k">const</span> tools = toolPolicy.<span class="n">map</span>(name => <span class="p">{</span>
<span class="k">const</span> def = toolRegistry.<span class="n">get</span>(name);
<span class="k">if</span> (!def) <span class="k">throw new</span> <span class="n">Error</span>(<span class="s">`unknown tool: </span><span class="v">${name}</span><span class="s">`</span>);
<span class="k">return</span> <span class="p">{</span>
name: def.<span class="n">name</span>,
description: def.<span class="n">description</span>,
input_schema: def.<span class="n">inputSchema</span>,
<span class="p">}</span>;
<span class="p">}</span>);
<span class="c">// 写入临时 tools.json,作为 --tools 参数传给 claude CLI</span>
<span class="k">const</span> toolsFile = <span class="s">`/tmp/devloop-tools-</span><span class="v">${agent}</span><span class="s">-</span><span class="v">${Date.now()}</span><span class="s">.json`</span>;
fs.<span class="n">writeFileSync</span>(toolsFile, <span class="n">JSON.stringify</span>(<span class="p">{</span> tools <span class="p">}</span>));
<span class="k">return</span> <span class="p">[</span>
<span class="s">'--dangerously-skip-permissions'</span>, <span class="c">// 既有约定</span>
<span class="s">'--tools'</span>, toolsFile, <span class="c">// Claude CLI 加载工具定义</span>
<span class="s">'--tool-handler'</span>, <span class="s">'jsonrpc://stdio'</span>, <span class="c">// 工具调用通过 stdio JSON-RPC 回到 server</span>
<span class="p">]</span>;
<span class="p">}</span></pre>
<p>Claude CLI 调任一工具时,通过 stdio 发 JSON-RPC 请求给 ACP,ACP 路由到 ToolRegistry.execute(name, input, ctx),返回值再 JSON-RPC 回 CLI。整个过程对 Agent 透明,它看到的就是标准 tool_use。</p>
</section>
<section>
<h2>⑩ Phase 0 验收(接 plan-migration)</h2>
<p>Phase 0 完成的判据是<b>一个最小调用回环</b>跑通:</p>
<pre class="code"><span class="c"># Phase 0 验证脚本: scripts/phase0-verify.js</span>
node -e <span class="s">"
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');
"</span>
<span class="c"># 期望: ✓ 所有 7 个 critical 工具就位</span>
<span class="c"># 再跑一个最小 Agent 调用 (用 stub Claude CLI 模式)</span>
node scripts/phase0-test-call.js
<span class="c"># 内部:用 mock ACP session 调 write_todolist,验证文件落盘 + SSE 事件发出</span></pre>
</section>
<section>
<h2>⑪ 工时估算(Phase 0)</h2>
<table>
<thead><tr><th>任务</th><th>工时</th><th>难度</th></tr></thead>
<tbody>
<tr><td>持久化 5 件套(persistence-tools.js)</td><td>1.5 天</td><td>低</td></tr>
<tr><td>doc-tools(11 个工具,PM/Architect 用)</td><td>2.5 天</td><td>中(cheerio 操作多)</td></tr>
<tr><td>coder/tester/integrator/triager-tools(18 个)</td><td>2 天</td><td>中</td></tr>
<tr><td>extract-agent-prompts.js(HTML → md)</td><td>0.5 天</td><td>低(正则)</td></tr>
<tr><td>_registry.json 加 6 条 + toolPolicy</td><td>0.5 天</td><td>低</td></tr>
<tr><td>ClaudeCliProtocol --tools 注入</td><td>1 天</td><td>中(要看 Claude CLI 的实际 tool 接口)</td></tr>
<tr><td>jsonGuard 中间件挂 Bash 工具</td><td>0.5 天</td><td>低</td></tr>
<tr><td>phase0-verify.js + phase0-test-call.js</td><td>1 天</td><td>低</td></tr>
<tr><td>调试 + 联调</td><td>2 天</td><td>—</td></tr>
<tr><td><b>合计</b></td><td><b>≈ 11.5 天 (2.5 周)</b></td><td>—</td></tr>
</tbody>
</table>
<p class="sig">P0 完成后,PM Agent 才能真的跑出 prd/ 站点 — 这是 P1 的<b>前置条件</b>。</p>
</section>
<section>
<h2>⑫ 关联页面</h2>
<ul>
<li><a href="arch-acp-bridge.html">ACP Bridge</a> · LoopController 怎么调到这层</li>
<li><a href="plan-migration.html">最小改造路径</a> · Phase 0 在那里被引用</li>
<li><a href="agents-pm.html">PM</a> / <a href="agents-architect.html">Architect</a> · 看每个 Agent 用了哪些工具</li>
</ul>
</section>
</article>
<nav class="prev-next"></nav>
<footer class="site">
<div><b>Section</b>05 · Architecture</div>
<div><b>Page</b>arch-tool-registry.html</div>
<div><b>Spec Rev</b>v2 · 2026-05-13</div>
</footer>
</main>
</div>
<script src="assets/shell.js"></script>
</body>
</html>