agents-pm.html 59.3 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 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" />
<title>PM Agent · 完整 Skill 规范 · 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="agents-pm.html">
<div class="shell">
  <aside id="sidebar"></aside>
  <main class="page">
    <header class="page-head">
      <div class="crumb"><span class="pill">02 · DESIGN / Agents</span><span class="sep">/</span><span>PM Agent</span></div>
      <h1>PM Agent · 完整 Skill 规范</h1>
      <p class="deck">这是「<b>喂进去就能跑</b>」版本。不需要 Innovator 训练。给 Claude Code 一个人类需求 + 本文档定义的 system prompt + 工具集 → 直接产出多页 HTML PRD 站点(3 级菜单 + 搜索 + 8 类信息)。</p>
    </header>
    <article class="body">

      <section>
        <div class="agent-hero">
          <div class="avatar" style="background:var(--L3-tint);color:var(--L3)">P</div>
          <div>
            <h2 style="margin:0">PM Agent</h2>
            <p style="margin:6px 0">产品经理 · 多页 PRD 站点维护人 · 持久化运行</p>
            <div class="meta">
              <div><b>Loop</b> <span class="tag L3">L3</span></div>
              <div><b>Model</b> Sonnet 4.6</div>
              <div><b>Modes</b> init · incremental · resume</div>
              <div><b>沙箱</b> projects/{pid}/prd/** + memory/devloop/pm/</div>
              <div><b>会话复用</b> 是(SessionPool key = pm.cid.pid)</div>
            </div>
          </div>
        </div>
      </section>

      <section>
        <h2>① 功能需求</h2>
        <ol>
          <li><b>三种调用模式:</b>
            <ul style="padding-left:20px">
              <li><code>init</code> · 新建项目 → 生成完整 PRD 站点(11 章节 + 3 个 asset)</li>
              <li><code>incremental</code> · 追加 / 修改章节,<b>不</b>重写已有内容</li>
              <li><code>resume</code> · 从 <code>resume_token</code> 恢复(崩溃后由 P-8 触发)</li>
            </ul>
          </li>
          <li><b>输出形态:</b>多页 HTML 站点(不是单 prd.html),符合 docs/prd/newfunction/ 的 3 级菜单 + 全局搜索结构。</li>
          <li><b>需求结构化:</b>把人类自然语言拆为 functional / non-functional / constraint / out-of-scope 四类,按章节归位。</li>
          <li><b>冲突检测:</b>新需求与历史 PRD 矛盾时<b>不静默合并</b>,在 PRD 顶部插 <code>&lt;aside data-conflict&gt;</code>,发 SSE 给人裁决。</li>
          <li><b>修订追踪:</b>每个 section 必带 <code>data-rev</code>;index 顶部维护 changelog footer。</li>
          <li><b>8 类信息覆盖:</b>每个章节页至少 4 类(Tables / Design / Illustrations / Code / Interaction / Workflows / Spatial / Images)。</li>
          <li><b>跨项目记忆:</b>启动时读 <code>memory/devloop/pm/</code>,结束前根据本次反馈写入新 memory(如果有提炼价值)。</li>
          <li><b>自带 todolist:</b>开工先写完整 todolist,每完成一项立即 update。</li>
        </ol>
      </section>

      <section>
        <h2>② Skill 设计</h2>

        <h3>2.1 输出形态 · 多页 PRD 站点目录结构</h3>
        <p>每次 init 调用必须铺出下面这棵树。<b>incremental 不重铺,仅追加新章节。</b></p>
<pre class="tree">
data/companies/{cid}/projects/{pid}/prd/
├── index.html                          (Hero + reading 入口)
├── reading-guide.html
├── assets/
│   ├── shell.css                       (从 docs/prd/newfunction/assets/shell.css 拷贝)
│   ├── shell.js                        (项目特化:SITE 注入 + 搜索逻辑)
│   └── search-index.json               (由 build-search-index 工具生成,每次写入后重建)
├── product/
│   ├── background.html
│   ├── personas.html
│   ├── original-req.html
│   ├── scope.html
│   ├── success-metrics.html
│   └── glossary.html
├── feature/
│   └── feature-tree.html
├── flow/
│   └── user-flows.html
└── prototype/
    └── proto-{viewName}.html           (UI 页一个一个 wireframe SVG)
</pre>
        <p>章节命名严格按上面,因为 Architect Agent 会按这个目录结构反推 SDD。</p>

        <h3>2.2 完整 System Prompt(可拷贝直用)</h3>
        <p>下面整段(约 4KB)直接写在 <code>system/agents/devloop-pm/prompt.md</code>。注入时 LoopController 在头部补 <code>## Memory</code> 段(来自 M-11),尾部补 <code>## Current Task Context</code> 段(运行时上下文)。</p>

<pre class="code"><span class="c"># ROLE</span>
<span class="v">你是 LemonBoss DevLoop 的 PM Agent。你的产出是给 Architect Agent 读、给</span>
<span class="v">Dashboard 渲染、给未来的 Claude Code session 复现项目用的多页 HTML PRD 站点。</span>

<span class="c"># MODES</span>
<span class="v">输入参数 mode 决定行为:</span>
<span class="v">- "init"        新建项目 PRD 站点(完整 11 章节)</span>
<span class="v">- "incremental" 追加/修改 PRD 章节(基于人类反馈)</span>
<span class="v">- "resume"     从 resume_token 恢复未完的 todolist</span>

<span class="c"># OUTPUT FORMAT (强约束 - 不满足 DocStore 拒收)</span>

<span class="v">[A] 文件布局:projects/{pid}/prd/ 下铺设(见上表)</span>

<span class="v">[B] 每个 HTML 页 &lt;!doctype html&gt;...必须含:</span>
<span class="v">    &lt;link rel="stylesheet" href="../assets/shell.css"&gt;       (相对路径)</span>
<span class="v">    &lt;body data-page="{相对路径}"&gt;...&lt;script src="../assets/shell.js"&gt;</span>

<span class="v">[C] 每个章节页根容器:</span>
<span class="v">    &lt;article data-doc="prd" data-rev="N"&gt;</span>
<span class="v">      &lt;section data-rev="M" data-origin="{user@iso}"&gt;</span>
<span class="v">        &lt;h3&gt;...&lt;/h3&gt;</span>
<span class="v">        &lt;p&gt;...&lt;/p&gt;</span>
<span class="v">      &lt;/section&gt;</span>
<span class="v">      &lt;footer data-changelog&gt;...&lt;/footer&gt;</span>
<span class="v">    &lt;/article&gt;</span>

<span class="v">[D] 每页至少 4 类(共 8 类)信息:</span>
<span class="v">    Tables · Design · Illustrations · Code · Interaction · Workflows · Spatial · Images</span>
<span class="v">    其中 Interaction 必含(tabs / accordion / 至少 1 个)。</span>

<span class="v">[E] 索引锚点(被 search-index-builder 抽取):</span>
<span class="v">    data-doc · data-rev · data-origin · data-conflict (冲突时)</span>

<span class="c"># MANDATORY WORKFLOW</span>

<span class="v">step 0 · 读取上下文</span>
<span class="v">  ├─ 读 projects/{pid}/prd/ 已存在的所有 .html (若 incremental)</span>
<span class="v">  ├─ 读 projects/{pid}/changelog.ndjson 最后 5 条(知道之前发生了什么)</span>
<span class="v">  └─ 注意: memory section 已在 prompt 顶部注入,不要重复读</span>

<span class="v">step 1 · 写 todolist (调 write_todolist 工具)</span>
<span class="v">  init 模式典型 todolist:</span>
<span class="v">    step-001 解析人类需求,拆 functional/NFR/constraint/out-of-scope</span>
<span class="v">    step-002 检查与历史 memory 是否冲突</span>
<span class="v">    step-003 写 index.html 含 Hero + 8-class ribbon</span>
<span class="v">    step-004 写 product/ 6 章节</span>
<span class="v">    step-005 写 feature/feature-tree.html</span>
<span class="v">    step-006 写 flow/user-flows.html</span>
<span class="v">    step-007 写 prototype/ wireframe</span>
<span class="v">    step-008 调 update_search_index</span>
<span class="v">    step-009 自检 (调 validate_prd_site 工具)</span>
<span class="v">    step-010 写 timeline + 调 record_memory(可选)</span>

<span class="v">step 2..N · 按 todolist 逐项执行</span>
<span class="v">  每项开始: update_todo_item(id, status='doing')</span>
<span class="v">  每项结束: update_todo_item(id, status='done', output_ref='...')</span>
<span class="v">  每项产物落盘后: timeline_append('prd_section_written', ref, summary)</span>

<span class="v">step N+1 · 收尾</span>
<span class="v">  ├─ update_search_index(pid)</span>
<span class="v">  ├─ validate_prd_site(pid) 必须返回 ok=true</span>
<span class="v">  ├─ record_memory 如有非平凡发现(用户偏好/约束)</span>
<span class="v">  └─ echo_diff 输出人话变更摘要</span>

<span class="c"># RULES (硬约束)</span>

<span class="v">1. 永远 append section,不重写历史 section 的 data-rev。</span>
<span class="v">2. 同一 section 多次修订,创建新 section data-rev=N+1,保留旧 section。</span>
<span class="v">3. 矛盾必须显式:发现冲突 → &lt;aside data-conflict&gt;...&lt;/aside&gt; + 不继续合并。</span>
<span class="v">4. 任何带 JSON body 的 Bash 命令 → 先写 /tmp/*.json 再 -d @file</span>
<span class="v">   (参见 docs/CLAUDE.md L91-99)。禁止 inline JSON。</span>
<span class="v">5. 不写 SDD/units.json/code/tests/contract。这些属于 Architect/Coder。</span>
<span class="v">6. 不调 call_sub_agent。你不分派任务。</span>
<span class="v">7. 沙箱: 只能写 prd/** + memory/devloop/pm/**。越权写会被拦截。</span>
<span class="v">8. 8 类信息覆盖不是建议是要求。少于 4 类 → validate_prd_site 报错 → 自己 patch。</span>

<span class="c"># OUTPUT VOICE</span>
<span class="v">- 中文写正文,英文写 token/code 标识符。</span>
<span class="v">- 用 Fraunces 风格的克制语气,不要营销腔。</span>
<span class="v">- 不写"完美""强大""革命性"这种字眼。</span>
<span class="v">- 数字胜过形容词:"5 个 Unit" &gt; "几个 Unit"。</span>

<span class="c"># SELF-CHECK (完工前必跑)</span>
<span class="v">1. validate_prd_site(pid) 返回 ok=true</span>
<span class="v">2. search-index.json 包含本次新写的所有页</span>
<span class="v">3. timeline.ndjson 末尾含 'prd_drafted' 事件</span>
<span class="v">4. agent-state/pm.json status=done · last_output_ref 非空</span>
<span class="v">5. 所有 todolist items status=done</span>
<span class="v">不过任一项 → 自己 patch 后再交付。不允许交残品。</span></pre>

        <h3>2.3 工具集(含签名)</h3>
        <table>
          <thead><tr><th>工具</th><th>签名</th><th>用途</th><th>访问边界</th></tr></thead>
          <tbody>
            <tr><td><code>Read</code></td><td>(path: string)</td><td>读现有 PRD / changelog / memory</td><td>项目内只读</td></tr>
            <tr><td><code>Write</code></td><td>(path: string, content: string)</td><td>底层落盘</td><td>沙箱白名单</td></tr>
            <tr><td><code>bootstrap_prd_site</code> <span class="tag NEW">NEW</span></td><td>(pid)</td><td>init 模式专用:建目录、复制 shell.css/js</td><td>项目内</td></tr>
            <tr><td><code>write_chapter</code> <span class="tag NEW">NEW</span></td><td>(pid, section, page, html)</td><td>包装 Write,自动校验 HTML、append 到 SITE 配置</td><td>prd/** 写</td></tr>
            <tr><td><code>archive_doc</code> <span class="tag NEW">NEW</span></td><td>(pid, kind, oldHtml)</td><td>归档旧版到 history/</td><td>—</td></tr>
            <tr><td><code>diff_html</code> <span class="tag NEW">NEW</span></td><td>(pid, oldFile, newFile)</td><td>cheerio section 级 diff</td><td>项目只读</td></tr>
            <tr><td><code>update_search_index</code> <span class="tag NEW">NEW</span></td><td>(pid)</td><td>跑 build-search-index 重建索引</td><td>—</td></tr>
            <tr><td><code>validate_prd_site</code> <span class="tag NEW">NEW</span></td><td>(pid) → {ok, errors[]}</td><td>cheerio + 规则校验整站</td><td>—</td></tr>
            <tr><td><code>write_todolist</code> <span class="tag NEW">NEW</span></td><td>(items: TodoItem[])</td><td>初始化 agent-state/pm-todo.json + SSE</td><td>agent-state/</td></tr>
            <tr><td><code>update_todo_item</code> <span class="tag NEW">NEW</span></td><td>(id, status, output_ref?)</td><td>更新单条 item + 同步 progress_pct</td><td>agent-state/</td></tr>
            <tr><td><code>update_runtime_state</code> <span class="tag NEW">NEW</span></td><td>(current_step, total_steps)</td><td>更新 M-10 AgentRuntimeState</td><td>agent-state/</td></tr>
            <tr><td><code>timeline_append</code> <span class="tag NEW">NEW</span></td><td>(kind, ref, summary, payload?)</td><td>append M-13 timeline.ndjson</td><td>—</td></tr>
            <tr><td><code>record_memory</code> <span class="tag NEW">NEW</span></td><td>(topic, content, relevance)</td><td>写 M-11 memory/devloop/pm/</td><td>memory/devloop/pm/</td></tr>
            <tr><td><code>echo_diff</code> <span class="tag NEW">NEW</span></td><td>(summary, sections_diff)</td><td>SSE 友好的人类变更摘要</td><td>—</td></tr>
          </tbody>
        </table>

        <h3>2.4 禁用的工具 / 行为</h3>
        <div class="callout warn">
          <ul style="margin:0">
            <li><b>不允许 Bash</b> — PM Agent 不需要跑代码。任何 shell 操作走专用工具。</li>
            <li><b>不允许 Edit</b> — PM 写的是新章节,不是细粒度 patch。要修就写新 section。</li>
            <li><b>不允许 call_sub_agent</b> — PM 不分派。</li>
            <li><b>不允许写 sdd/ · units.json · units/* · integration/*</b> — 沙箱拦截。</li>
          </ul>
        </div>
      </section>

      <section>
        <h2>③ 持久化集成(M-10/11/12/13 接入)</h2>
        <p>PM Agent 是第一个全面用持久化层的 Agent。下面是它在 ACP session 内的真实操作序列:</p>

        <svg viewBox="0 0 1100 360" width="100%" role="img" aria-label="PM 持久化操作时序">
          <defs><marker id="arrPMp" 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="10" fill="#0E0E10">
            <g font-weight="700">
              <text x="40"  y="30">LoopController</text>
              <text x="240" y="30">M-11 memory</text>
              <text x="420" y="30">PM Agent</text>
              <text x="600" y="30">M-12 todolist</text>
              <text x="780" y="30">M-13 timeline</text>
              <text x="940" y="30">M-10 runtime</text>
            </g>
            <g stroke="#dcd5c4" stroke-dasharray="2 4">
              <line x1="120" y1="40" x2="120" y2="350"/>
              <line x1="320" y1="40" x2="320" y2="350"/>
              <line x1="500" y1="40" x2="500" y2="350"/>
              <line x1="680" y1="40" x2="680" y2="350"/>
              <line x1="860" y1="40" x2="860" y2="350"/>
              <line x1="1020" y1="40" x2="1020" y2="350"/>
            </g>

            <line x1="120" y1="56" x2="315" y2="56" stroke="#0E0E10" marker-end="url(#arrPMp)"/>
            <text x="130" y="50">① 读 memory/devloop/pm/*.md (top 5 by relevance)</text>

            <line x1="320" y1="80" x2="495" y2="80" stroke="#0E0E10" marker-end="url(#arrPMp)"/>
            <text x="330" y="74">② memory 注入 system prompt</text>

            <line x1="120" y1="104" x2="1015" y2="104" stroke="#0E0E10" marker-end="url(#arrPMp)"/>
            <text x="130" y="98">③ 写 runtime status=running, current_step=0</text>

            <line x1="500" y1="128" x2="675" y2="128" stroke="#0E0E10" marker-end="url(#arrPMp)"/>
            <text x="510" y="122">④ write_todolist([step-001..step-010])</text>

            <line x1="500" y1="152" x2="855" y2="152" stroke="#0E0E10" marker-end="url(#arrPMp)"/>
            <text x="510" y="146">⑤ timeline_append('agent_started')</text>

            <line x1="500" y1="180" x2="675" y2="180" stroke="#C77C2C" marker-end="url(#arrPMp)"/>
            <text x="510" y="174">⑥ update_todo_item(step-001, doing)</text>

            <text x="510" y="206" fill="#3B3B40">⑦ ... 写章节 HTML / bootstrap / 调工具 ...</text>

            <line x1="500" y1="232" x2="675" y2="232" stroke="#0E8C5A" marker-end="url(#arrPMp)"/>
            <text x="510" y="226">⑧ update_todo_item(step-001, done, output_ref)</text>

            <line x1="500" y1="252" x2="855" y2="252" stroke="#0E8C5A" marker-end="url(#arrPMp)"/>
            <text x="510" y="246">⑨ timeline_append('prd_section_written')</text>

            <line x1="500" y1="280" x2="1015" y2="280" stroke="#0E0E10" marker-end="url(#arrPMp)"/>
            <text x="510" y="274">⑩ update_runtime_state(current_step=1, total_steps=10)</text>

            <text x="510" y="306" fill="#3B3B40">⑪ ... 重复 ⑥-⑩ 直到所有 step done ...</text>

            <line x1="500" y1="334" x2="315" y2="334" stroke="#6E48C3" marker-end="url(#arrPMp)"/>
            <text x="320" y="328">⑫ record_memory(topic='user-prefs', content) (可选)</text>
          </g>
        </svg>

        <h3>3.1 Memory 注入约定</h3>
        <p>LoopController 在调 PM 前,从 <code>memory/devloop/pm/*.md</code> 取 top 5(按 relevance × recency),拼到 system prompt 顶部 <code>## Memory</code> section。PM Agent 自己<b>不</b>主动读 memory dir。</p>

        <h3>3.2 写新 memory 的判断</h3>
        <p>结束前调 <code>record_memory(topic, content, relevance)</code> 仅在<b>非平凡</b>发现时:</p>
        <ul>
          <li>用户表达过明确偏好(如"我喜欢 RESTful 不要 GraphQL")</li>
          <li>项目背景里有可复用约束(如"我们公司统一用 better-sqlite3")</li>
          <li><b>不</b>记录单项目的具体功能。那是 PRD 内容,不是 memory。</li>
        </ul>
      </section>

      <section>
        <h2>④ I/O 契约</h2>

        <h3>4.1 输入 schema</h3>
<pre class="code"><span class="k">const</span> PMInput = Type.<span class="n">Object</span>(<span class="p">{</span>
  pid:        Type.<span class="n">String</span>(),
  mode:       Type.<span class="n">Union</span>(<span class="p">[</span>Type.<span class="n">Literal</span>(<span class="s">'init'</span>), Type.<span class="n">Literal</span>(<span class="s">'incremental'</span>), Type.<span class="n">Literal</span>(<span class="s">'resume'</span>)<span class="p">]</span>),

  <span class="c">// init / incremental 时填</span>
  feedback:   Type.<span class="n">Optional</span>(Type.<span class="n">Object</span>(<span class="p">{</span>
    text:   Type.<span class="n">String</span>(<span class="p">{</span> minLength: <span class="v">1</span>, maxLength: <span class="v">8000</span> <span class="p">}</span>),
    origin: Type.<span class="n">String</span>(),               <span class="c">// user id / 'system'</span>
    iso_ts: Type.<span class="n">String</span>(),
  <span class="p">}</span>)),

  <span class="c">// resume 时填</span>
  resume_token: Type.<span class="n">Optional</span>(Type.<span class="n">String</span>()),

  <span class="c">// 自动注入</span>
  call_id:     Type.<span class="n">String</span>(),
  company_id:  Type.<span class="n">String</span>(),
<span class="p">}</span>);</pre>

        <h3>4.2 输出 schema</h3>
<pre class="code"><span class="k">const</span> PMOutput = Type.<span class="n">Object</span>(<span class="p">{</span>
  ok:          Type.<span class="n">Boolean</span>(),
  mode:        Type.<span class="n">String</span>(),
  prd_rev:     Type.<span class="n">Number</span>(),

  files_written: Type.<span class="n">Array</span>(Type.<span class="n">String</span>()),    <span class="c">// 相对 prd/ 的路径</span>
  files_archived: Type.<span class="n">Array</span>(Type.<span class="n">String</span>()),

  change_summary: Type.<span class="n">String</span>(<span class="p">{</span> maxLength: <span class="v">500</span> <span class="p">}</span>),  <span class="c">// 给人看</span>
  diff: Type.<span class="n">Object</span>(<span class="p">{</span>
    sections_added:   Type.<span class="n">Array</span>(Type.<span class="n">String</span>()),
    sections_changed: Type.<span class="n">Array</span>(Type.<span class="n">String</span>()),
    sections_removed: Type.<span class="n">Array</span>(Type.<span class="n">String</span>()),
  <span class="p">}</span>),

  has_conflict: Type.<span class="n">Boolean</span>(),
  conflict_detail: Type.<span class="n">Optional</span>(Type.<span class="n">String</span>()),

  memory_recorded: Type.<span class="n">Array</span>(Type.<span class="n">Object</span>(<span class="p">{</span>
    topic: Type.<span class="n">String</span>(), relevance: Type.<span class="n">Number</span>()
  <span class="p">}</span>)),

  validation: Type.<span class="n">Object</span>(<span class="p">{</span>
    ok: Type.<span class="n">Boolean</span>(),
    issues: Type.<span class="n">Array</span>(Type.<span class="n">String</span>()),
  <span class="p">}</span>),

  duration_ms: Type.<span class="n">Number</span>(),
  tokens_used: Type.<span class="n">Number</span>(),
<span class="p">}</span>);</pre>
      </section>

      <section>
        <h2>⑤ 上下游传递</h2>
        <div class="io-grid">
          <div class="io-box">
            <h4>⬆ 上游</h4>
            <ul>
              <li><b>LoopController</b> · onHumanFeedback → callAgent(pm, input)</li>
              <li><b>L1/L2 自动信号</b>(blocked / l2_exhausted)→ 触发 incremental mode 让人补</li>
              <li><b>M-11 Memory</b> · 启动时由 LoopController 注入 system prompt</li>
              <li><b>历史 PRD</b>(incremental mode)· 读 projects/{pid}/prd/**</li>
              <li><b>changelog.ndjson</b> · 读最后 5 条了解前因</li>
            </ul>
          </div>
          <div class="io-box">
            <h4>⬇ 下游</h4>
            <ul>
              <li><b>Architect Agent</b> · 通过新 prd/ 站点(mode=generate 或 patch)</li>
              <li><b>DocStore</b> · 自动归档 + changelog.ndjson append</li>
              <li><b>SSE</b> · prd.updated / agent.todolist / agent.step / agent.done</li>
              <li><b>Human</b>(冲突时)· SSE human_intervention_needed</li>
              <li><b>memory/devloop/pm/</b> · 新 memory 文件</li>
              <li><b>timeline.ndjson</b> · prd_drafted / prd_conflict / agent_done 事件</li>
            </ul>
          </div>
        </div>
      </section>

      <section>
        <h2>⑥ 完整执行样例</h2>

        <div class="tabs" data-tabs>
          <div class="tabbar" role="tablist">
            <button role="tab" aria-selected="true"  aria-controls="sa">Sample A · init (TODO API)</button>
            <button role="tab" aria-selected="false" aria-controls="sb">Sample B · incremental (+ OAuth)</button>
            <button role="tab" aria-selected="false" aria-controls="sc">Sample C · conflict 检测</button>
          </div>

          <div class="tabpanel" id="sa">
<pre class="code"><span class="c"># ─────────── INPUT ───────────</span>
<span class="p">{</span>
  <span class="n">"pid"</span>: <span class="s">"p-731042"</span>,
  <span class="n">"mode"</span>: <span class="s">"init"</span>,
  <span class="n">"feedback"</span>: <span class="p">{</span>
    <span class="n">"text"</span>: <span class="s">"做一个 TODO 列表 API + 简单 Vue 前端,能新增/列出。"</span>,
    <span class="n">"origin"</span>: <span class="s">"yibo"</span>,
    <span class="n">"iso_ts"</span>: <span class="s">"2026-05-13T10:30:00Z"</span>
  <span class="p">}</span>,
  <span class="n">"call_id"</span>: <span class="s">"call-20260513-1030-a1b2"</span>,
  <span class="n">"company_id"</span>: <span class="s">"c-lemonv-001"</span>
<span class="p">}</span>

<span class="c"># ─────────── INJECTED MEMORY (top 5) ───────────</span>
## Memory (from prior projects)

### project-conventions  (relevance: 0.94)
- 用户偏好 RESTful API + 完整 OpenAPI 注解
- 数据库统一用 better-sqlite3
- 认证: JWT 放 Authorization header

### code-style  (relevance: 0.82)
- 后端用 Express + service 双层
- 前端 Vue 3 Composition API

<span class="c"># ─────────── PM AGENT 内部执行 ───────────</span>

<span class="c"># step 0 - 读上下文 (incremental 才需要;init 跳)</span>

<span class="c"># step 1 - write_todolist</span>
write_todolist([
  <span class="p">{</span> id:<span class="s">'step-001'</span>, description:<span class="s">'解析需求拆四类'</span> <span class="p">}</span>,
  <span class="p">{</span> id:<span class="s">'step-002'</span>, description:<span class="s">'检查 memory 冲突'</span> <span class="p">}</span>,
  <span class="p">{</span> id:<span class="s">'step-003'</span>, description:<span class="s">'bootstrap_prd_site(p-731042)'</span> <span class="p">}</span>,
  <span class="p">{</span> id:<span class="s">'step-004'</span>, description:<span class="s">'写 index.html + reading-guide.html'</span> <span class="p">}</span>,
  <span class="p">{</span> id:<span class="s">'step-005'</span>, description:<span class="s">'写 product/ 6 章节'</span> <span class="p">}</span>,
  <span class="p">{</span> id:<span class="s">'step-006'</span>, description:<span class="s">'写 feature/feature-tree.html (2 个特性: TODO API + UI)'</span> <span class="p">}</span>,
  <span class="p">{</span> id:<span class="s">'step-007'</span>, description:<span class="s">'写 flow/user-flows.html'</span> <span class="p">}</span>,
  <span class="p">{</span> id:<span class="s">'step-008'</span>, description:<span class="s">'写 prototype/ 1 个 (TODO 列表 UI)'</span> <span class="p">}</span>,
  <span class="p">{</span> id:<span class="s">'step-009'</span>, description:<span class="s">'update_search_index + validate_prd_site'</span> <span class="p">}</span>,
  <span class="p">{</span> id:<span class="s">'step-010'</span>, description:<span class="s">'record_memory (本次无新 memory) + echo_diff'</span> <span class="p">}</span>,
])

<span class="c"># step 2-9 - 顺次执行 (略)</span>

<span class="c"># step 10 收尾 - validate_prd_site</span>
validate_prd_site(<span class="s">"p-731042"</span>) <span class="c">// → { ok: true, issues: [] }</span>
update_search_index(<span class="s">"p-731042"</span>)
timeline_append(<span class="s">"prd_drafted"</span>, <span class="s">"prd@rev1"</span>, <span class="s">"PRD rev 1 完成 · 2 个特性 · 8 章节"</span>)
echo_diff(<span class="s">"创建 TODO API + Vue UI 项目"</span>, <span class="p">{</span>sections_added:<span class="p">[</span>...11 个<span class="p">]</span><span class="p">}</span>)

<span class="c"># ─────────── OUTPUT ───────────</span>
<span class="p">{</span>
  <span class="n">"ok"</span>: <span class="v">true</span>,
  <span class="n">"mode"</span>: <span class="s">"init"</span>,
  <span class="n">"prd_rev"</span>: <span class="v">1</span>,
  <span class="n">"files_written"</span>: <span class="p">[</span>
    <span class="s">"prd/index.html"</span>,
    <span class="s">"prd/reading-guide.html"</span>,
    <span class="s">"prd/assets/shell.css"</span>,
    <span class="s">"prd/assets/shell.js"</span>,
    <span class="s">"prd/assets/search-index.json"</span>,
    <span class="s">"prd/product/background.html"</span>,
    <span class="s">"prd/product/personas.html"</span>,
    <span class="s">"prd/product/original-req.html"</span>,
    <span class="s">"prd/product/scope.html"</span>,
    <span class="s">"prd/product/success-metrics.html"</span>,
    <span class="s">"prd/product/glossary.html"</span>,
    <span class="s">"prd/feature/feature-tree.html"</span>,
    <span class="s">"prd/flow/user-flows.html"</span>,
    <span class="s">"prd/prototype/proto-todo-list.html"</span>
  <span class="p">]</span>,
  <span class="n">"files_archived"</span>: <span class="p">[]</span>,
  <span class="n">"change_summary"</span>: <span class="s">"创建 TODO API + Vue UI 项目 PRD · 2 个特性 · 11 章节"</span>,
  <span class="n">"diff"</span>: <span class="p">{</span> <span class="n">"sections_added"</span>: <span class="p">[</span><span class="s">"#todo-create"</span>, <span class="s">"#todo-list"</span>, <span class="s">"#ui-vue"</span>, ...<span class="p">]</span> <span class="p">}</span>,
  <span class="n">"has_conflict"</span>: <span class="v">false</span>,
  <span class="n">"memory_recorded"</span>: <span class="p">[]</span>,
  <span class="n">"validation"</span>: <span class="p">{</span> <span class="n">"ok"</span>: <span class="v">true</span>, <span class="n">"issues"</span>: <span class="p">[]</span> <span class="p">}</span>,
  <span class="n">"duration_ms"</span>: <span class="v">42_318</span>,
  <span class="n">"tokens_used"</span>: <span class="v">8_421</span>
<span class="p">}</span>

<span class="c"># ─────────── SSE 事件序列 ───────────</span>
agent.started     · pm · call-20260513-1030-a1b2
agent.todolist    · 10 items
agent.step        · step-001 → doing → done (进度 10%)
agent.step        · step-002 → done (进度 20%)
...
prd.updated       · rev 1 · 11 章节
agent.done        · duration 42s · tokens 8.4k</pre>
          </div>

          <div class="tabpanel" id="sb" hidden>
<pre class="code"><span class="c"># ─────────── INPUT (incremental) ───────────</span>
<span class="p">{</span>
  <span class="n">"pid"</span>: <span class="s">"p-731042"</span>,
  <span class="n">"mode"</span>: <span class="s">"incremental"</span>,
  <span class="n">"feedback"</span>: <span class="p">{</span>
    <span class="n">"text"</span>: <span class="s">"再加 GitHub OAuth 登录,token 自动刷新 ≤ 5s"</span>,
    <span class="n">"origin"</span>: <span class="s">"yibo"</span>,
    <span class="n">"iso_ts"</span>: <span class="s">"2026-05-13T10:42:00Z"</span>
  <span class="p">}</span>,
  ...
<span class="p">}</span>

<span class="c"># PM 关键决策 (注释化):</span>

<span class="c"># step 0 读现有 PRD (prd/product/scope.html 已声明"无认证")</span>
<span class="c">#   → 这是一个新功能,与 scope 矛盾吗? 不,只是<b>扩展</b> scope。</span>

<span class="c"># step 1 todolist 7 项:</span>
<span class="p">[</span>
  <span class="p">{</span> id:<span class="s">'step-001'</span>, description:<span class="s">'读现有 PRD + changelog last 5'</span> <span class="p">}</span>,
  <span class="p">{</span> id:<span class="s">'step-002'</span>, description:<span class="s">'判断:与 scope 矛盾还是扩展(本次=扩展)'</span> <span class="p">}</span>,
  <span class="p">{</span> id:<span class="s">'step-003'</span>, description:<span class="s">'追加新 section: prd/product/scope.html 加 NFR-OAuth'</span> <span class="p">}</span>,
  <span class="p">{</span> id:<span class="s">'step-004'</span>, description:<span class="s">'追加新章节: prd/feature/feature-tree.html 加 OAuth 子树'</span> <span class="p">}</span>,
  <span class="p">{</span> id:<span class="s">'step-005'</span>, description:<span class="s">'追加 user-flows.html 加 OAuth flow'</span> <span class="p">}</span>,
  <span class="p">{</span> id:<span class="s">'step-006'</span>, description:<span class="s">'update_search_index + validate'</span> <span class="p">}</span>,
  <span class="p">{</span> id:<span class="s">'step-007'</span>, description:<span class="s">'record_memory (token 续期偏好) + echo_diff'</span> <span class="p">}</span>,
<span class="p">]</span>

<span class="c"># 关键: scope.html 内部追加</span>
<span class="c">#   不重写已有 section data-rev=1,而是 append:</span>
<span class="k">&lt;section</span> <span class="n">data-rev</span>=<span class="s">"2"</span> <span class="n">data-origin</span>=<span class="s">"yibo@2026-05-13T10:42"</span><span class="k">&gt;</span>
  <span class="k">&lt;h3&gt;</span><span class="v">扩展:OAuth 登录</span><span class="k">&lt;/h3&gt;</span>
  <span class="k">&lt;p&gt;</span><span class="v">支持 GitHub OAuth。Token 自动刷新 ≤ 5s。</span><span class="k">&lt;/p&gt;</span>
<span class="k">&lt;/section&gt;</span>

<span class="c"># 收尾</span>
record_memory(<span class="s">"oauth-conventions"</span>, <span class="s">"用户偏好 GitHub OAuth + 5s 续期"</span>, <span class="v">0.78</span>)
timeline_append(<span class="s">"prd_drafted"</span>, <span class="s">"prd@rev2"</span>, <span class="s">"+ OAuth section"</span>)

<span class="c"># OUTPUT</span>
<span class="p">{</span>
  <span class="n">"ok"</span>: <span class="v">true</span>, <span class="n">"mode"</span>: <span class="s">"incremental"</span>, <span class="n">"prd_rev"</span>: <span class="v">2</span>,
  <span class="n">"files_written"</span>: <span class="p">[</span>
    <span class="s">"prd/product/scope.html"</span>,
    <span class="s">"prd/feature/feature-tree.html"</span>,
    <span class="s">"prd/flow/user-flows.html"</span>,
    <span class="s">"prd/assets/search-index.json"</span>
  <span class="p">]</span>,
  <span class="n">"files_archived"</span>: <span class="p">[</span><span class="s">"history/prd/scope-rev-1.html"</span>, ...<span class="p">]</span>,
  <span class="n">"change_summary"</span>: <span class="s">"+ OAuth section · 3 章节追加"</span>,
  <span class="n">"diff"</span>: <span class="p">{</span> <span class="n">"sections_added"</span>: <span class="p">[</span><span class="s">"#oauth-scope"</span>, <span class="s">"#oauth-feature"</span>, <span class="s">"#oauth-flow"</span><span class="p">]</span>,
            <span class="n">"sections_changed"</span>: <span class="p">[]</span>, <span class="n">"sections_removed"</span>: <span class="p">[]</span> <span class="p">}</span>,
  <span class="n">"has_conflict"</span>: <span class="v">false</span>,
  <span class="n">"memory_recorded"</span>: <span class="p">[{</span> <span class="n">"topic"</span>: <span class="s">"oauth-conventions"</span>, <span class="n">"relevance"</span>: <span class="v">0.78</span> <span class="p">}]</span>,
  <span class="n">"validation"</span>: <span class="p">{</span> <span class="n">"ok"</span>: <span class="v">true</span> <span class="p">}</span>,
  ...
<span class="p">}</span></pre>
          </div>

          <div class="tabpanel" id="sc" hidden>
<pre class="code"><span class="c"># ─────────── INPUT (conflict detection) ───────────</span>
<span class="p">{</span>
  <span class="n">"pid"</span>: <span class="s">"p-731042"</span>,
  <span class="n">"mode"</span>: <span class="s">"incremental"</span>,
  <span class="n">"feedback"</span>: <span class="p">{</span>
    <span class="n">"text"</span>: <span class="s">"砍掉登录,做成完全匿名"</span>,
    <span class="n">"origin"</span>: <span class="s">"yibo"</span>
  <span class="p">}</span>,
  ...
<span class="p">}</span>

<span class="c"># PM 检测到:rev 2 刚刚加了 OAuth,现在却要砍登录。</span>
<span class="c"># 这是<b>显式矛盾</b>,不能静默合并。</span>

<span class="c"># step 1 todolist 缩短到 3 项:</span>
<span class="p">[</span>
  <span class="p">{</span> id:<span class="s">'step-001'</span>, description:<span class="s">'读现有 PRD 检测矛盾'</span> <span class="p">}</span>,
  <span class="p">{</span> id:<span class="s">'step-002'</span>, description:<span class="s">'写 prd/index.html 的 &lt;aside data-conflict&gt;'</span> <span class="p">}</span>,
  <span class="p">{</span> id:<span class="s">'step-003'</span>, description:<span class="s">'触发 human_intervention_needed SSE,停在这里'</span> <span class="p">}</span>,
<span class="p">]</span>

<span class="c"># 写入 index.html 顶部:</span>
<span class="k">&lt;aside</span> <span class="n">data-conflict</span> <span class="n">data-origin</span>=<span class="s">"yibo@2026-05-13T11:01"</span><span class="k">&gt;</span>
  <span class="k">&lt;h3&gt;</span><span class="v">⚠ 待裁决 · 与历史 PRD 矛盾</span><span class="k">&lt;/h3&gt;</span>
  <span class="k">&lt;p&gt;</span><span class="v">新反馈:"砍掉登录,做成完全匿名"</span><span class="k">&lt;/p&gt;</span>
  <span class="k">&lt;p&gt;</span><span class="v">与历史冲突:</span>
    <span class="k">&lt;ul&gt;</span>
      <span class="k">&lt;li&gt;</span><span class="v">rev 1 · scope.html · "邮箱密码登录"</span><span class="k">&lt;/li&gt;</span>
      <span class="k">&lt;li&gt;</span><span class="v">rev 2 · scope.html · "扩展:OAuth 登录"</span><span class="k">&lt;/li&gt;</span>
    <span class="k">&lt;/ul&gt;</span>
  <span class="k">&lt;/p&gt;</span>
  <span class="k">&lt;p&gt;</span><span class="v">三种解读,请选:</span>
    <span class="k">&lt;ol&gt;</span>
      <span class="k">&lt;li&gt;</span><span class="v">完全移除登录(rev 1/2 标 removed,新 PRD 无 auth)</span><span class="k">&lt;/li&gt;</span>
      <span class="k">&lt;li&gt;</span><span class="v">登录变可选(默认匿名,登录加增值功能)</span><span class="k">&lt;/li&gt;</span>
      <span class="k">&lt;li&gt;</span><span class="v">仅砍 OAuth,保留邮箱密码登录</span><span class="k">&lt;/li&gt;</span>
    <span class="k">&lt;/ol&gt;</span>
  <span class="k">&lt;/p&gt;</span>
<span class="k">&lt;/aside&gt;</span>

<span class="c"># 触发 SSE 后 PM 立即返回(不写其他章节,等人决策)</span>
timeline_append(<span class="s">"prd_conflict"</span>, <span class="s">"prd@conflict-pending"</span>, <span class="s">"3 选 1 等人"</span>)
sseBroker.emit(pid, <span class="s">"human_intervention_needed"</span>, <span class="p">{</span>
  reason: <span class="s">"prd_conflict"</span>,
  options: <span class="p">[</span><span class="s">"remove_login"</span>, <span class="s">"login_optional"</span>, <span class="s">"remove_oauth_only"</span><span class="p">]</span>
<span class="p">}</span>)

<span class="c"># OUTPUT</span>
<span class="p">{</span>
  <span class="n">"ok"</span>: <span class="v">true</span>,           <span class="c">// PM 没失败,只是停下等人</span>
  <span class="n">"mode"</span>: <span class="s">"incremental"</span>,
  <span class="n">"prd_rev"</span>: <span class="v">2</span>,          <span class="c">// 没升 rev,只挂 conflict</span>
  <span class="n">"files_written"</span>: <span class="p">[</span><span class="s">"prd/index.html"</span><span class="p">]</span>,  <span class="c">// 只在 index 加 aside</span>
  <span class="n">"has_conflict"</span>: <span class="v">true</span>,
  <span class="n">"conflict_detail"</span>: <span class="s">"与 rev 1+2 的 scope/login 矛盾"</span>,
  ...
<span class="p">}</span></pre>
          </div>
        </div>
      </section>

      <section>
        <h2>⑦ 助手工具源码</h2>

        <h3>7.1 <code>html-section-patcher.js</code> · section 级精准 patch</h3>
<pre class="code"><span class="c">// server/services/agent-tools/html-section-patcher.js · ~80 行</span>
<span class="k">const</span> cheerio = <span class="n">require</span>(<span class="s">'cheerio'</span>);
<span class="k">const</span> fs      = <span class="n">require</span>(<span class="s">'fs'</span>);

<span class="c">/**
 * Append a new section to a PRD chapter file.
 * 不重写已有 section,只增 data-rev=N+1 的新 section。
 */</span>
<span class="k">function</span> <span class="n">appendSection</span>(<span class="p">{</span> chapterPath, sectionHtml, origin <span class="p">}</span>) <span class="p">{</span>
  <span class="k">const</span> raw = fs.<span class="n">readFileSync</span>(chapterPath, <span class="s">'utf8'</span>);
  <span class="k">const</span> $   = cheerio.<span class="n">load</span>(raw, <span class="p">{</span> decodeEntities: <span class="v">false</span> <span class="p">}</span>);

  <span class="k">const</span> article = $(<span class="s">'article[data-doc="prd"]'</span>);
  <span class="k">if</span> (article.<span class="n">length</span> === <span class="v">0</span>) <span class="k">throw new</span> <span class="n">Error</span>(<span class="s">'No &lt;article data-doc="prd"&gt; found'</span>);

  <span class="c">// 找最大 data-rev,新 section 用 +1</span>
  <span class="k">let</span> maxRev = <span class="v">0</span>;
  article.<span class="n">find</span>(<span class="s">'section[data-rev]'</span>).<span class="n">each</span>((_, el) =&gt; <span class="p">{</span>
    <span class="k">const</span> r = <span class="n">parseInt</span>($(el).<span class="n">attr</span>(<span class="s">'data-rev'</span>), <span class="v">10</span>);
    <span class="k">if</span> (r &gt; maxRev) maxRev = r;
  <span class="p">}</span>);

  <span class="c">// 构造新 section</span>
  <span class="k">const</span> $new = cheerio.<span class="n">load</span>(<span class="s">'&lt;div&gt;'</span> + sectionHtml + <span class="s">'&lt;/div&gt;'</span>);
  <span class="k">const</span> sectionEl = $new(<span class="s">'section'</span>).<span class="n">first</span>();
  <span class="k">if</span> (sectionEl.<span class="n">length</span> === <span class="v">0</span>) <span class="k">throw new</span> <span class="n">Error</span>(<span class="s">'sectionHtml must contain &lt;section&gt;'</span>);

  sectionEl.<span class="n">attr</span>(<span class="s">'data-rev'</span>, <span class="n">String</span>(maxRev + <span class="v">1</span>));
  sectionEl.<span class="n">attr</span>(<span class="s">'data-origin'</span>, origin);

  <span class="c">// 校验:section 必含 h3 + p</span>
  <span class="k">if</span> (sectionEl.<span class="n">find</span>(<span class="s">'h3'</span>).<span class="n">length</span> === <span class="v">0</span>)
    <span class="k">throw new</span> <span class="n">Error</span>(<span class="s">'New section missing &lt;h3&gt;'</span>);
  <span class="k">if</span> (sectionEl.<span class="n">find</span>(<span class="s">'p'</span>).<span class="n">length</span> === <span class="v">0</span>)
    <span class="k">throw new</span> <span class="n">Error</span>(<span class="s">'New section missing &lt;p&gt;'</span>);

  <span class="c">// 追加 (在 footer 之前)</span>
  <span class="k">const</span> footer = article.<span class="n">find</span>(<span class="s">'footer[data-changelog]'</span>);
  <span class="k">if</span> (footer.<span class="n">length</span>) footer.<span class="n">before</span>(sectionEl.<span class="n">toString</span>());
  <span class="k">else</span> article.<span class="n">append</span>(sectionEl.<span class="n">toString</span>());

  <span class="c">// 升 article data-rev</span>
  article.<span class="n">attr</span>(<span class="s">'data-rev'</span>, <span class="n">String</span>(maxRev + <span class="v">1</span>));

  fs.<span class="n">writeFileSync</span>(chapterPath, $.<span class="n">html</span>());
  <span class="k">return</span> <span class="p">{</span> new_rev: maxRev + <span class="v">1</span>, section_id: sectionEl.<span class="n">attr</span>(<span class="s">'id'</span>) || <span class="v">null</span> <span class="p">}</span>;
<span class="p">}</span>

<span class="k">function</span> <span class="n">addConflictAside</span>(<span class="p">{</span> indexPath, text, conflicts, options <span class="p">}</span>) <span class="p">{</span>
  <span class="k">const</span> raw = fs.<span class="n">readFileSync</span>(indexPath, <span class="s">'utf8'</span>);
  <span class="k">const</span> $   = cheerio.<span class="n">load</span>(raw);
  $(<span class="s">'aside[data-conflict]'</span>).<span class="n">remove</span>();   <span class="c">// 移除旧 conflict</span>
  <span class="k">const</span> aside = <span class="s">`&lt;aside data-conflict data-origin="</span><span class="v">${origin}</span><span class="s">"&gt;...&lt;/aside&gt;`</span>;
  $(<span class="s">'article[data-doc="prd"]'</span>).<span class="n">prepend</span>(aside);
  fs.<span class="n">writeFileSync</span>(indexPath, $.<span class="n">html</span>());
<span class="p">}</span>

module.<span class="n">exports</span> = <span class="p">{</span> appendSection, addConflictAside <span class="p">}</span>;</pre>

        <h3>7.2 <code>prd-validator.js</code> · 整站校验</h3>
<pre class="code"><span class="c">// server/services/agent-tools/prd-validator.js · ~70 行</span>
<span class="k">const</span> cheerio = <span class="n">require</span>(<span class="s">'cheerio'</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> REQUIRED_CHAPTERS = <span class="p">[</span>
  <span class="s">'index.html'</span>, <span class="s">'reading-guide.html'</span>,
  <span class="s">'product/background.html'</span>, <span class="s">'product/personas.html'</span>,
  <span class="s">'product/original-req.html'</span>, <span class="s">'product/scope.html'</span>,
  <span class="s">'product/success-metrics.html'</span>, <span class="s">'product/glossary.html'</span>,
  <span class="s">'feature/feature-tree.html'</span>,
  <span class="s">'flow/user-flows.html'</span>,
<span class="p">]</span>;

<span class="k">const</span> EIGHT_TYPES = <span class="p">{</span>
  Tables:        $ =&gt; $(<span class="s">'table'</span>).length &gt; <span class="v">0</span>,
  Code:          $ =&gt; $(<span class="s">'pre.code, pre.tree'</span>).length &gt; <span class="v">0</span>,
  Illustrations: $ =&gt; $(<span class="s">'svg'</span>).length &gt; <span class="v">0</span>,
  Interaction:   $ =&gt; $(<span class="s">'.tabs, details.acc, nav.toc'</span>).length &gt; <span class="v">0</span>,
  Spatial:       $ =&gt; $(<span class="s">'pre.tree, .grid-2, .grid-3, .grid-4'</span>).length &gt; <span class="v">0</span>,
  Design:        $ =&gt; $(<span class="s">'.card, .agent-hero, .mock'</span>).length &gt; <span class="v">0</span>,
  Images:        $ =&gt; $(<span class="s">'svg, img, .mock'</span>).length &gt; <span class="v">0</span>,
  Workflows:     $ =&gt; $(<span class="s">'svg'</span>).<span class="n">filter</span>((_,el) =&gt;
                       $(el).<span class="n">html</span>().<span class="n">includes</span>(<span class="s">'marker-end'</span>)).length &gt; <span class="v">0</span>,
<span class="p">}</span>;

<span class="k">function</span> <span class="n">validatePrdSite</span>(pid) <span class="p">{</span>
  <span class="k">const</span> root = path.<span class="n">resolve</span>(projectsDir, pid, <span class="s">'prd'</span>);
  <span class="k">const</span> issues = <span class="p">[]</span>;

  <span class="c">// 1. 必备章节</span>
  <span class="k">for</span> (<span class="k">const</span> f <span class="k">of</span> REQUIRED_CHAPTERS) <span class="p">{</span>
    <span class="k">if</span> (!fs.<span class="n">existsSync</span>(path.<span class="n">join</span>(root, f)))
      issues.<span class="n">push</span>(<span class="s">`missing required chapter: </span><span class="v">${f}</span><span class="s">`</span>);
  <span class="p">}</span>

  <span class="c">// 2. 每章节 cheerio 校验</span>
  <span class="k">const</span> files = <span class="n">walkHtml</span>(root);
  <span class="k">for</span> (<span class="k">const</span> f <span class="k">of</span> files) <span class="p">{</span>
    <span class="k">const</span> $ = cheerio.<span class="n">load</span>(fs.<span class="n">readFileSync</span>(f, <span class="s">'utf8'</span>));

    <span class="c">// 顶级 article</span>
    <span class="k">const</span> art = $(<span class="s">'article[data-doc="prd"]'</span>);
    <span class="k">if</span> (art.length === <span class="v">0</span>)
      issues.<span class="n">push</span>(<span class="s">`</span><span class="v">${f}</span><span class="s">: missing &lt;article data-doc="prd"&gt;`</span>);

    <span class="c">// section h3 + p</span>
    art.<span class="n">find</span>(<span class="s">'section'</span>).<span class="n">each</span>((_, sec) =&gt; <span class="p">{</span>
      <span class="k">if</span> ($(sec).<span class="n">find</span>(<span class="s">'h3'</span>).length === <span class="v">0</span>) issues.<span class="n">push</span>(<span class="s">`</span><span class="v">${f}</span><span class="s">: section without h3`</span>);
      <span class="k">if</span> ($(sec).<span class="n">find</span>(<span class="s">'p'</span>).length === <span class="v">0</span>)  issues.<span class="n">push</span>(<span class="s">`</span><span class="v">${f}</span><span class="s">: section without p`</span>);
    <span class="p">}</span>);

    <span class="c">// 8 类信息 ≥ 4</span>
    <span class="k">const</span> covered = Object.<span class="n">entries</span>(EIGHT_TYPES).<span class="n">filter</span>(([_, fn]) =&gt; fn($));
    <span class="k">if</span> (covered.length &lt; <span class="v">4</span>)
      issues.<span class="n">push</span>(<span class="s">`</span><span class="v">${f}</span><span class="s">: only </span><span class="v">${covered.length}</span><span class="s">/8 types covered (need ≥4)`</span>);

    <span class="c">// data-rev 单调</span>
    <span class="k">const</span> revs = art.<span class="n">find</span>(<span class="s">'section[data-rev]'</span>).<span class="n">map</span>((_,s) =&gt;
                  <span class="n">parseInt</span>($(s).<span class="n">attr</span>(<span class="s">'data-rev'</span>), <span class="v">10</span>)).<span class="n">get</span>();
    <span class="k">if</span> (revs.<span class="n">some</span>((v,i) =&gt; i &gt; <span class="v">0</span> &amp;&amp; v &lt; revs<span class="p">[</span>i-<span class="v">1</span><span class="p">]</span>))
      issues.<span class="n">push</span>(<span class="s">`</span><span class="v">${f}</span><span class="s">: data-rev not monotonic`</span>);
  <span class="p">}</span>

  <span class="k">return</span> <span class="p">{</span> ok: issues.length === <span class="v">0</span>, issues <span class="p">}</span>;
<span class="p">}</span>

module.<span class="n">exports</span> = <span class="p">{</span> validatePrdSite <span class="p">}</span>;</pre>

        <h3>7.3 <code>prd-site-init.js</code> · bootstrap_prd_site 工具实现</h3>
<pre class="code"><span class="c">// server/services/agent-tools/prd-site-init.js · ~50 行</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> TEMPLATE_DIR = path.<span class="n">resolve</span>(__dirname, <span class="s">'../../../docs/prd/newfunction/assets'</span>);

<span class="k">function</span> <span class="n">bootstrapPrdSite</span>(pid) <span class="p">{</span>
  <span class="k">const</span> root = path.<span class="n">resolve</span>(projectsDir, pid, <span class="s">'prd'</span>);
  <span class="k">const</span> assetsDir = path.<span class="n">join</span>(root, <span class="s">'assets'</span>);

  <span class="c">// 建目录</span>
  fs.<span class="n">mkdirSync</span>(assetsDir,                  <span class="p">{</span> recursive: <span class="v">true</span> <span class="p">}</span>);
  fs.<span class="n">mkdirSync</span>(path.<span class="n">join</span>(root, <span class="s">'product'</span>),   <span class="p">{</span> recursive: <span class="v">true</span> <span class="p">}</span>);
  fs.<span class="n">mkdirSync</span>(path.<span class="n">join</span>(root, <span class="s">'feature'</span>),   <span class="p">{</span> recursive: <span class="v">true</span> <span class="p">}</span>);
  fs.<span class="n">mkdirSync</span>(path.<span class="n">join</span>(root, <span class="s">'flow'</span>),      <span class="p">{</span> recursive: <span class="v">true</span> <span class="p">}</span>);
  fs.<span class="n">mkdirSync</span>(path.<span class="n">join</span>(root, <span class="s">'prototype'</span>), <span class="p">{</span> recursive: <span class="v">true</span> <span class="p">}</span>);

  <span class="c">// 复制 shell.css (一次,共享样式)</span>
  fs.<span class="n">copyFileSync</span>(path.<span class="n">join</span>(TEMPLATE_DIR, <span class="s">'shell.css'</span>),
                   path.<span class="n">join</span>(assetsDir,    <span class="s">'shell.css'</span>));

  <span class="c">// 复制 shell.js 模板 + 注入空 SITE</span>
  <span class="k">const</span> jsTpl = fs.<span class="n">readFileSync</span>(path.<span class="n">join</span>(TEMPLATE_DIR, <span class="s">'shell.js'</span>), <span class="s">'utf8'</span>);
  <span class="c">// shell.js 内部 SITE 来自 PM Agent 维护的 prd-site-config.json</span>
  fs.<span class="n">writeFileSync</span>(path.<span class="n">join</span>(assetsDir, <span class="s">'shell.js'</span>), jsTpl);

  <span class="c">// 空 search-index 占位</span>
  fs.<span class="n">writeFileSync</span>(path.<span class="n">join</span>(assetsDir, <span class="s">'search-index.json'</span>),
                    <span class="n">JSON.stringify</span>(<span class="p">{</span> version:<span class="s">'1'</span>, pages:<span class="v">0</span>, entries:<span class="p">[]</span> <span class="p">}</span>));

  <span class="k">return</span> <span class="p">{</span> root, files_created: <span class="v">3</span> <span class="p">}</span>;
<span class="p">}</span>

module.<span class="n">exports</span> = <span class="p">{</span> bootstrapPrdSite <span class="p">}</span>;</pre>
      </section>

      <section>
        <h2>⑧ 自检 Checklist · 完工前必跑</h2>
        <table>
          <thead><tr><th>#</th><th>检查项</th><th>命令 / 工具</th><th>通过判定</th></tr></thead>
          <tbody>
            <tr><td>1</td><td>整站校验</td><td><code>validate_prd_site(pid)</code></td><td>ok=true, issues=[]</td></tr>
            <tr><td>2</td><td>搜索索引同步</td><td><code>update_search_index(pid)</code></td><td>pages 字段含所有新写文件</td></tr>
            <tr><td>3</td><td>必备 11 章节都存在</td><td>fs.existsSync 循环</td><td>全 true</td></tr>
            <tr><td>4</td><td>每页 ≥4 类信息</td><td>validatePrdSite 内置</td><td>无 "X/8 types" 错误</td></tr>
            <tr><td>5</td><td>每个 section 含 h3 + p</td><td>validatePrdSite 内置</td><td>无 "section without h3/p"</td></tr>
            <tr><td>6</td><td>data-rev 单调递增</td><td>validatePrdSite 内置</td><td>无 "not monotonic"</td></tr>
            <tr><td>7</td><td>todolist 全 done</td><td>read agent-state/pm-todo.json</td><td>所有 item.status='done'</td></tr>
            <tr><td>8</td><td>timeline 含 prd_drafted</td><td>tail timeline.ndjson</td><td>最后 5 条之一 kind=='prd_drafted'</td></tr>
            <tr><td>9</td><td>agent-state.pm.json 终态</td><td>read M-10</td><td>status='done', last_output_ref 非空</td></tr>
            <tr><td>10</td><td>无 Bash inline JSON 使用</td><td>grep coder.log? (PM 无 log,仅审查 prompt 自身)</td><td>—</td></tr>
            <tr><td>11</td><td>changelog.ndjson append</td><td>由 DocStore 自动</td><td>新 entry kind='prd_rev'</td></tr>
          </tbody>
        </table>
        <p class="sig">任一不过 → 不输出 ok=true。自己 patch 后重跑自检,最多 retry 2 次内必过。第 3 次仍不过 → 报 <code>validation_failed</code> 上报 L3。</p>
      </section>

      <section>
        <h2>⑨ 失败模式与降级</h2>
        <table class="dense">
          <thead><tr><th>场景</th><th>检测</th><th>降级</th></tr></thead>
          <tbody>
            <tr><td>需求过模糊(&lt;20 字、无动词)</td><td>token 数 + 词性快速判断</td><td>不写 PRD,直接 ask_clarification → SSE</td></tr>
            <tr><td>新需求与历史矛盾</td><td>读历史 + 语义比对</td><td>插 &lt;aside data-conflict&gt;,不合并,触发 human</td></tr>
            <tr><td>沙箱写越权</td><td>Write 工具拒绝</td><td>Coder 收到 error,换路径</td></tr>
            <tr><td>validate_prd_site 失败</td><td>self-check 步骤</td><td>读 issues 列表 → patch 后重跑(≤ 2 次)</td></tr>
            <tr><td>search-index 构建失败</td><td>update_search_index 报错</td><td>跳过索引但仍输出 PRD;标记 partial=true</td></tr>
            <tr><td>memory 文件读失败</td><td>I/O 错误</td><td>记 warning,继续(视同空 memory)</td></tr>
            <tr><td>todolist 中途崩</td><td>session 异常</td><td>P-8 处理:写 paused → 等 resume 决策</td></tr>
            <tr><td>cheerio 解析非法 HTML</td><td>html-section-patcher 报错</td><td>不写盘,retry 一次 PM 重生这段</td></tr>
          </tbody>
        </table>
      </section>

      <section>
        <h2>⑩ 与其他 Agent 的握手摘要</h2>
        <table class="dense">
          <thead><tr><th>对方</th><th>PM 给的</th><th>PM 收的</th></tr></thead>
          <tbody>
            <tr><td><b>Human</b></td><td>conflict_aside / clarification request</td><td>requirement text · sign-off · abandon</td></tr>
            <tr><td><b>Architect</b></td><td>完整 prd/ 站点 + changelog entry</td><td>(无;Architect 自己读 prd/)</td></tr>
            <tr><td><b>Dashboard</b></td><td>SSE prd.updated · agent.* 系列</td><td>(无)</td></tr>
            <tr><td><b>LoopController</b></td><td>PMOutput JSON</td><td>PMInput JSON · resume_token</td></tr>
            <tr><td><b>DocStore</b></td><td>(无;通过工具间接)</td><td>校验通过/失败回执</td></tr>
            <tr><td><b>memory/devloop/pm/</b></td><td>新 .md 文件</td><td>启动时被 LoopController 注入到 prompt</td></tr>
          </tbody>
        </table>
      </section>

      <section>
        <h2>⑪ 「测试 PM Agent 是否真的能跑」的实战命令</h2>
        <p>本规范的<b>验证终局</b>:用以下命令直接驱动 PM Agent,看能否 30 秒内产出符合所有约束的 PRD 站点:</p>
<pre class="code"><span class="c"># 1. 注册 Agent 模板</span>
<span class="c">#    把本页 §2.2 的 System Prompt 落到:</span>
<span class="c">#    system/agents/devloop-pm/prompt.md</span>
<span class="c">#    并在 system/agents/_registry.json 加 'devloop-pm' 条目</span>

<span class="c"># 2. 启动 server (P1 阶段最小后端,只挂 PM 路径)</span>
PORT=3010 node server/index.js

<span class="c"># 3. 直接调 PM (绕过 LoopController)</span>
<span class="c">#    准备 /tmp/pm-test.json (单行紧凑,按 CLAUDE.md 警告)</span>
echo <span class="s">'{"pid":"p-test001","mode":"init","feedback":{"text":"做一个 TODO API 加 Vue 前端","origin":"yibo","iso_ts":"2026-05-13T10:00:00Z"},"call_id":"call-test","company_id":"c-test"}'</span> &gt; /tmp/pm-test.json

curl -X POST http://localhost:3010/api/devloop/_test/run-agent/pm \
  -H <span class="s">"Content-Type: application/json"</span> \
  -d @/tmp/pm-test.json

<span class="c"># 4. 期望:30-60 秒内</span>
<span class="c">#    data/companies/c-test/projects/p-test001/prd/ 下铺好 11 章节</span>
<span class="c">#    + assets/{shell.css, shell.js, search-index.json}</span>
<span class="c">#    + history/ 空 (init 首次,无归档)</span>

<span class="c"># 5. 校验</span>
node -e <span class="s">"console.log(require('./server/services/agent-tools/prd-validator').validatePrdSite('p-test001'))"</span>
<span class="c"># 期望: { ok: true, issues: [] }</span>

<span class="c"># 6. 浏览器打开</span>
<span class="c">#    file:///data/companies/c-test/projects/p-test001/prd/index.html</span>
<span class="c">#    左侧应见 3 级菜单,Ctrl+K 可搜索</span></pre>
        <div class="callout ok">
          <b>这一组命令跑通 = PM Agent skill 验证通过。</b>之后 Architect Agent 就能从这个 prd/ 站点反向派生 SDD。
        </div>
      </section>

    </article>
    <nav class="prev-next"></nav>
    <footer class="site">
      <div><b>Section</b>02 · Design / Agents</div>
      <div><b>Page</b>agents-pm.html</div>
      <div><b>Spec Rev</b>v2 · 2026-05-13</div>
    </footer>
  </main>
</div>
<script src="assets/shell.js"></script>
</body>
</html>