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 =&gt; (<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 &amp;&amp; tool.name &amp;&amp; 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/agents/_registry.json</code> 注册时声明<b>能调哪些工具</b>。Claude CLI 启动时 <code>--allowedTools</code> 参数据此生成。</p>

<pre class="code shape"><span class="c">// system/agents/_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/agents/devloop-pm/prompt.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"}\' &gt; /tmp/x.json &amp;&amp; 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) =&gt; ...,       <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 =&gt; <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>