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><aside data-conflict></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/templates/devloop-pm.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 页 <!doctype html>...必须含:</span>
<span class="v"> <link rel="stylesheet" href="../assets/shell.css"> (相对路径)</span>
<span class="v"> <body data-page="{相对路径}">...<script src="../assets/shell.js"></span>
<span class="v">[C] 每个章节页根容器:</span>
<span class="v"> <article data-doc="prd" data-rev="N"></span>
<span class="v"> <section data-rev="M" data-origin="{user@iso}"></span>
<span class="v"> <h3>...</h3></span>
<span class="v"> <p>...</p></span>
<span class="v"> </section></span>
<span class="v"> <footer data-changelog>...</footer></span>
<span class="v"> </article></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. 矛盾必须显式:发现冲突 → <aside data-conflict>...</aside> + 不继续合并。</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" > "几个 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"><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">></span>
<span class="k"><h3></span><span class="v">扩展:OAuth 登录</span><span class="k"></h3></span>
<span class="k"><p></span><span class="v">支持 GitHub OAuth。Token 自动刷新 ≤ 5s。</span><span class="k"></p></span>
<span class="k"></section></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 的 <aside data-conflict>'</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"><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">></span>
<span class="k"><h3></span><span class="v">⚠ 待裁决 · 与历史 PRD 矛盾</span><span class="k"></h3></span>
<span class="k"><p></span><span class="v">新反馈:"砍掉登录,做成完全匿名"</span><span class="k"></p></span>
<span class="k"><p></span><span class="v">与历史冲突:</span>
<span class="k"><ul></span>
<span class="k"><li></span><span class="v">rev 1 · scope.html · "邮箱密码登录"</span><span class="k"></li></span>
<span class="k"><li></span><span class="v">rev 2 · scope.html · "扩展:OAuth 登录"</span><span class="k"></li></span>
<span class="k"></ul></span>
<span class="k"></p></span>
<span class="k"><p></span><span class="v">三种解读,请选:</span>
<span class="k"><ol></span>
<span class="k"><li></span><span class="v">完全移除登录(rev 1/2 标 removed,新 PRD 无 auth)</span><span class="k"></li></span>
<span class="k"><li></span><span class="v">登录变可选(默认匿名,登录加增值功能)</span><span class="k"></li></span>
<span class="k"><li></span><span class="v">仅砍 OAuth,保留邮箱密码登录</span><span class="k"></li></span>
<span class="k"></ol></span>
<span class="k"></p></span>
<span class="k"></aside></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 <article data-doc="prd"> 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) => <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 > 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">'<div>'</span> + sectionHtml + <span class="s">'</div>'</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 <section>'</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 <h3>'</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 <p>'</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">`<aside data-conflict data-origin="</span><span class="v">${origin}</span><span class="s">">...</aside>`</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: $ => $(<span class="s">'table'</span>).length > <span class="v">0</span>,
Code: $ => $(<span class="s">'pre.code, pre.tree'</span>).length > <span class="v">0</span>,
Illustrations: $ => $(<span class="s">'svg'</span>).length > <span class="v">0</span>,
Interaction: $ => $(<span class="s">'.tabs, details.acc, nav.toc'</span>).length > <span class="v">0</span>,
Spatial: $ => $(<span class="s">'pre.tree, .grid-2, .grid-3, .grid-4'</span>).length > <span class="v">0</span>,
Design: $ => $(<span class="s">'.card, .agent-hero, .mock'</span>).length > <span class="v">0</span>,
Images: $ => $(<span class="s">'svg, img, .mock'</span>).length > <span class="v">0</span>,
Workflows: $ => $(<span class="s">'svg'</span>).<span class="n">filter</span>((_,el) =>
$(el).<span class="n">html</span>().<span class="n">includes</span>(<span class="s">'marker-end'</span>)).length > <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 <article data-doc="prd">`</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) => <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]) => fn($));
<span class="k">if</span> (covered.length < <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) =>
<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) => i > <span class="v">0</span> && v < 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>需求过模糊(<20 字、无动词)</td><td>token 数 + 词性快速判断</td><td>不写 PRD,直接 ask_clarification → SSE</td></tr>
<tr><td>新需求与历史矛盾</td><td>读历史 + 语义比对</td><td>插 <aside data-conflict>,不合并,触发 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/templates/devloop-pm.md</span>
<span class="c"># 并在 system/templates/_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> > /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>