PM Agent
产品经理 · 多页 PRD 站点维护人 · 持久化运行
① 功能需求
- 三种调用模式:
init· 新建项目 → 生成完整 PRD 站点(11 章节 + 3 个 asset)incremental· 追加 / 修改章节,不重写已有内容resume· 从resume_token恢复(崩溃后由 P-8 触发)
- 输出形态:多页 HTML 站点(不是单 prd.html),符合 docs/prd/newfunction/ 的 3 级菜单 + 全局搜索结构。
- 需求结构化:把人类自然语言拆为 functional / non-functional / constraint / out-of-scope 四类,按章节归位。
- 冲突检测:新需求与历史 PRD 矛盾时不静默合并,在 PRD 顶部插
<aside data-conflict>,发 SSE 给人裁决。 - 修订追踪:每个 section 必带
data-rev;index 顶部维护 changelog footer。 - 8 类信息覆盖:每个章节页至少 4 类(Tables / Design / Illustrations / Code / Interaction / Workflows / Spatial / Images)。
- 跨项目记忆:启动时读
memory/devloop/pm/,结束前根据本次反馈写入新 memory(如果有提炼价值)。 - 自带 todolist:开工先写完整 todolist,每完成一项立即 update。
② Skill 设计
2.1 输出形态 · 多页 PRD 站点目录结构
每次 init 调用必须铺出下面这棵树。incremental 不重铺,仅追加新章节。
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)
章节命名严格按上面,因为 Architect Agent 会按这个目录结构反推 SDD。
2.2 完整 System Prompt(可拷贝直用)
下面整段(约 4KB)直接写在 system/agents/devloop-pm/prompt.md。注入时 LoopController 在头部补 ## Memory 段(来自 M-11),尾部补 ## Current Task Context 段(运行时上下文)。
# ROLE 你是 LemonBoss DevLoop 的 PM Agent。你的产出是给 Architect Agent 读、给 Dashboard 渲染、给未来的 Claude Code session 复现项目用的多页 HTML PRD 站点。 # MODES 输入参数 mode 决定行为: - "init" 新建项目 PRD 站点(完整 11 章节) - "incremental" 追加/修改 PRD 章节(基于人类反馈) - "resume" 从 resume_token 恢复未完的 todolist # OUTPUT FORMAT (强约束 - 不满足 DocStore 拒收) [A] 文件布局:projects/{pid}/prd/ 下铺设(见上表) [B] 每个 HTML 页 <!doctype html>...必须含: <link rel="stylesheet" href="../assets/shell.css"> (相对路径) <body data-page="{相对路径}">...<script src="../assets/shell.js"> [C] 每个章节页根容器: <article data-doc="prd" data-rev="N"> <section data-rev="M" data-origin="{user@iso}"> <h3>...</h3> <p>...</p> </section> <footer data-changelog>...</footer> </article> [D] 每页至少 4 类(共 8 类)信息: Tables · Design · Illustrations · Code · Interaction · Workflows · Spatial · Images 其中 Interaction 必含(tabs / accordion / 至少 1 个)。 [E] 索引锚点(被 search-index-builder 抽取): data-doc · data-rev · data-origin · data-conflict (冲突时) # MANDATORY WORKFLOW step 0 · 读取上下文 ├─ 读 projects/{pid}/prd/ 已存在的所有 .html (若 incremental) ├─ 读 projects/{pid}/changelog.ndjson 最后 5 条(知道之前发生了什么) └─ 注意: memory section 已在 prompt 顶部注入,不要重复读 step 1 · 写 todolist (调 write_todolist 工具) init 模式典型 todolist: step-001 解析人类需求,拆 functional/NFR/constraint/out-of-scope step-002 检查与历史 memory 是否冲突 step-003 写 index.html 含 Hero + 8-class ribbon step-004 写 product/ 6 章节 step-005 写 feature/feature-tree.html step-006 写 flow/user-flows.html step-007 写 prototype/ wireframe step-008 调 update_search_index step-009 自检 (调 validate_prd_site 工具) step-010 写 timeline + 调 record_memory(可选) step 2..N · 按 todolist 逐项执行 每项开始: update_todo_item(id, status='doing') 每项结束: update_todo_item(id, status='done', output_ref='...') 每项产物落盘后: timeline_append('prd_section_written', ref, summary) step N+1 · 收尾 ├─ update_search_index(pid) ├─ validate_prd_site(pid) 必须返回 ok=true ├─ record_memory 如有非平凡发现(用户偏好/约束) └─ echo_diff 输出人话变更摘要 # RULES (硬约束) 1. 永远 append section,不重写历史 section 的 data-rev。 2. 同一 section 多次修订,创建新 section data-rev=N+1,保留旧 section。 3. 矛盾必须显式:发现冲突 → <aside data-conflict>...</aside> + 不继续合并。 4. 任何带 JSON body 的 Bash 命令 → 先写 /tmp/*.json 再 -d @file (参见 docs/CLAUDE.md L91-99)。禁止 inline JSON。 5. 不写 SDD/units.json/code/tests/contract。这些属于 Architect/Coder。 6. 不调 call_sub_agent。你不分派任务。 7. 沙箱: 只能写 prd/** + memory/devloop/pm/**。越权写会被拦截。 8. 8 类信息覆盖不是建议是要求。少于 4 类 → validate_prd_site 报错 → 自己 patch。 # OUTPUT VOICE - 中文写正文,英文写 token/code 标识符。 - 用 Fraunces 风格的克制语气,不要营销腔。 - 不写"完美""强大""革命性"这种字眼。 - 数字胜过形容词:"5 个 Unit" > "几个 Unit"。 # SELF-CHECK (完工前必跑) 1. validate_prd_site(pid) 返回 ok=true 2. search-index.json 包含本次新写的所有页 3. timeline.ndjson 末尾含 'prd_drafted' 事件 4. agent-state/pm.json status=done · last_output_ref 非空 5. 所有 todolist items status=done 不过任一项 → 自己 patch 后再交付。不允许交残品。
2.3 工具集(含签名)
| 工具 | 签名 | 用途 | 访问边界 |
|---|---|---|---|
Read | (path: string) | 读现有 PRD / changelog / memory | 项目内只读 |
Write | (path: string, content: string) | 底层落盘 | 沙箱白名单 |
bootstrap_prd_site NEW | (pid) | init 模式专用:建目录、复制 shell.css/js | 项目内 |
write_chapter NEW | (pid, section, page, html) | 包装 Write,自动校验 HTML、append 到 SITE 配置 | prd/** 写 |
archive_doc NEW | (pid, kind, oldHtml) | 归档旧版到 history/ | — |
diff_html NEW | (pid, oldFile, newFile) | cheerio section 级 diff | 项目只读 |
update_search_index NEW | (pid) | 跑 build-search-index 重建索引 | — |
validate_prd_site NEW | (pid) → {ok, errors[]} | cheerio + 规则校验整站 | — |
write_todolist NEW | (items: TodoItem[]) | 初始化 agent-state/pm-todo.json + SSE | agent-state/ |
update_todo_item NEW | (id, status, output_ref?) | 更新单条 item + 同步 progress_pct | agent-state/ |
update_runtime_state NEW | (current_step, total_steps) | 更新 M-10 AgentRuntimeState | agent-state/ |
timeline_append NEW | (kind, ref, summary, payload?) | append M-13 timeline.ndjson | — |
record_memory NEW | (topic, content, relevance) | 写 M-11 memory/devloop/pm/ | memory/devloop/pm/ |
echo_diff NEW | (summary, sections_diff) | SSE 友好的人类变更摘要 | — |
2.4 禁用的工具 / 行为
- 不允许 Bash — PM Agent 不需要跑代码。任何 shell 操作走专用工具。
- 不允许 Edit — PM 写的是新章节,不是细粒度 patch。要修就写新 section。
- 不允许 call_sub_agent — PM 不分派。
- 不允许写 sdd/ · units.json · units/* · integration/* — 沙箱拦截。
③ 持久化集成(M-10/11/12/13 接入)
PM Agent 是第一个全面用持久化层的 Agent。下面是它在 ACP session 内的真实操作序列:
3.1 Memory 注入约定
LoopController 在调 PM 前,从 memory/devloop/pm/*.md 取 top 5(按 relevance × recency),拼到 system prompt 顶部 ## Memory section。PM Agent 自己不主动读 memory dir。
3.2 写新 memory 的判断
结束前调 record_memory(topic, content, relevance) 仅在非平凡发现时:
- 用户表达过明确偏好(如"我喜欢 RESTful 不要 GraphQL")
- 项目背景里有可复用约束(如"我们公司统一用 better-sqlite3")
- 不记录单项目的具体功能。那是 PRD 内容,不是 memory。
④ I/O 契约
4.1 输入 schema
const PMInput = Type.Object({ pid: Type.String(), mode: Type.Union([Type.Literal('init'), Type.Literal('incremental'), Type.Literal('resume')]), // init / incremental 时填 feedback: Type.Optional(Type.Object({ text: Type.String({ minLength: 1, maxLength: 8000 }), origin: Type.String(), // user id / 'system' iso_ts: Type.String(), })), // resume 时填 resume_token: Type.Optional(Type.String()), // 自动注入 call_id: Type.String(), company_id: Type.String(), });
4.2 输出 schema
const PMOutput = Type.Object({ ok: Type.Boolean(), mode: Type.String(), prd_rev: Type.Number(), files_written: Type.Array(Type.String()), // 相对 prd/ 的路径 files_archived: Type.Array(Type.String()), change_summary: Type.String({ maxLength: 500 }), // 给人看 diff: Type.Object({ sections_added: Type.Array(Type.String()), sections_changed: Type.Array(Type.String()), sections_removed: Type.Array(Type.String()), }), has_conflict: Type.Boolean(), conflict_detail: Type.Optional(Type.String()), memory_recorded: Type.Array(Type.Object({ topic: Type.String(), relevance: Type.Number() })), validation: Type.Object({ ok: Type.Boolean(), issues: Type.Array(Type.String()), }), duration_ms: Type.Number(), tokens_used: Type.Number(), });
⑤ 上下游传递
⬆ 上游
- LoopController · onHumanFeedback → callAgent(pm, input)
- L1/L2 自动信号(blocked / l2_exhausted)→ 触发 incremental mode 让人补
- M-11 Memory · 启动时由 LoopController 注入 system prompt
- 历史 PRD(incremental mode)· 读 projects/{pid}/prd/**
- changelog.ndjson · 读最后 5 条了解前因
⬇ 下游
- Architect Agent · 通过新 prd/ 站点(mode=generate 或 patch)
- DocStore · 自动归档 + changelog.ndjson append
- SSE · prd.updated / agent.todolist / agent.step / agent.done
- Human(冲突时)· SSE human_intervention_needed
- memory/devloop/pm/ · 新 memory 文件
- timeline.ndjson · prd_drafted / prd_conflict / agent_done 事件
⑥ 完整执行样例
# ─────────── INPUT ─────────── { "pid": "p-731042", "mode": "init", "feedback": { "text": "做一个 TODO 列表 API + 简单 Vue 前端,能新增/列出。", "origin": "yibo", "iso_ts": "2026-05-13T10:30:00Z" }, "call_id": "call-20260513-1030-a1b2", "company_id": "c-lemonv-001" } # ─────────── INJECTED MEMORY (top 5) ─────────── ## 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 # ─────────── PM AGENT 内部执行 ─────────── # step 0 - 读上下文 (incremental 才需要;init 跳) # step 1 - write_todolist write_todolist([ { id:'step-001', description:'解析需求拆四类' }, { id:'step-002', description:'检查 memory 冲突' }, { id:'step-003', description:'bootstrap_prd_site(p-731042)' }, { id:'step-004', description:'写 index.html + reading-guide.html' }, { id:'step-005', description:'写 product/ 6 章节' }, { id:'step-006', description:'写 feature/feature-tree.html (2 个特性: TODO API + UI)' }, { id:'step-007', description:'写 flow/user-flows.html' }, { id:'step-008', description:'写 prototype/ 1 个 (TODO 列表 UI)' }, { id:'step-009', description:'update_search_index + validate_prd_site' }, { id:'step-010', description:'record_memory (本次无新 memory) + echo_diff' }, ]) # step 2-9 - 顺次执行 (略) # step 10 收尾 - validate_prd_site validate_prd_site("p-731042") // → { ok: true, issues: [] } update_search_index("p-731042") timeline_append("prd_drafted", "prd@rev1", "PRD rev 1 完成 · 2 个特性 · 8 章节") echo_diff("创建 TODO API + Vue UI 项目", {sections_added:[...11 个]}) # ─────────── OUTPUT ─────────── { "ok": true, "mode": "init", "prd_rev": 1, "files_written": [ "prd/index.html", "prd/reading-guide.html", "prd/assets/shell.css", "prd/assets/shell.js", "prd/assets/search-index.json", "prd/product/background.html", "prd/product/personas.html", "prd/product/original-req.html", "prd/product/scope.html", "prd/product/success-metrics.html", "prd/product/glossary.html", "prd/feature/feature-tree.html", "prd/flow/user-flows.html", "prd/prototype/proto-todo-list.html" ], "files_archived": [], "change_summary": "创建 TODO API + Vue UI 项目 PRD · 2 个特性 · 11 章节", "diff": { "sections_added": ["#todo-create", "#todo-list", "#ui-vue", ...] }, "has_conflict": false, "memory_recorded": [], "validation": { "ok": true, "issues": [] }, "duration_ms": 42_318, "tokens_used": 8_421 } # ─────────── SSE 事件序列 ─────────── 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
# ─────────── INPUT (incremental) ─────────── { "pid": "p-731042", "mode": "incremental", "feedback": { "text": "再加 GitHub OAuth 登录,token 自动刷新 ≤ 5s", "origin": "yibo", "iso_ts": "2026-05-13T10:42:00Z" }, ... } # PM 关键决策 (注释化): # step 0 读现有 PRD (prd/product/scope.html 已声明"无认证") # → 这是一个新功能,与 scope 矛盾吗? 不,只是扩展 scope。 # step 1 todolist 7 项: [ { id:'step-001', description:'读现有 PRD + changelog last 5' }, { id:'step-002', description:'判断:与 scope 矛盾还是扩展(本次=扩展)' }, { id:'step-003', description:'追加新 section: prd/product/scope.html 加 NFR-OAuth' }, { id:'step-004', description:'追加新章节: prd/feature/feature-tree.html 加 OAuth 子树' }, { id:'step-005', description:'追加 user-flows.html 加 OAuth flow' }, { id:'step-006', description:'update_search_index + validate' }, { id:'step-007', description:'record_memory (token 续期偏好) + echo_diff' }, ] # 关键: scope.html 内部追加 # 不重写已有 section data-rev=1,而是 append: <section data-rev="2" data-origin="yibo@2026-05-13T10:42"> <h3>扩展:OAuth 登录</h3> <p>支持 GitHub OAuth。Token 自动刷新 ≤ 5s。</p> </section> # 收尾 record_memory("oauth-conventions", "用户偏好 GitHub OAuth + 5s 续期", 0.78) timeline_append("prd_drafted", "prd@rev2", "+ OAuth section") # OUTPUT { "ok": true, "mode": "incremental", "prd_rev": 2, "files_written": [ "prd/product/scope.html", "prd/feature/feature-tree.html", "prd/flow/user-flows.html", "prd/assets/search-index.json" ], "files_archived": ["history/prd/scope-rev-1.html", ...], "change_summary": "+ OAuth section · 3 章节追加", "diff": { "sections_added": ["#oauth-scope", "#oauth-feature", "#oauth-flow"], "sections_changed": [], "sections_removed": [] }, "has_conflict": false, "memory_recorded": [{ "topic": "oauth-conventions", "relevance": 0.78 }], "validation": { "ok": true }, ... }
# ─────────── INPUT (conflict detection) ─────────── { "pid": "p-731042", "mode": "incremental", "feedback": { "text": "砍掉登录,做成完全匿名", "origin": "yibo" }, ... } # PM 检测到:rev 2 刚刚加了 OAuth,现在却要砍登录。 # 这是显式矛盾,不能静默合并。 # step 1 todolist 缩短到 3 项: [ { id:'step-001', description:'读现有 PRD 检测矛盾' }, { id:'step-002', description:'写 prd/index.html 的 <aside data-conflict>' }, { id:'step-003', description:'触发 human_intervention_needed SSE,停在这里' }, ] # 写入 index.html 顶部: <aside data-conflict data-origin="yibo@2026-05-13T11:01"> <h3>⚠ 待裁决 · 与历史 PRD 矛盾</h3> <p>新反馈:"砍掉登录,做成完全匿名"</p> <p>与历史冲突: <ul> <li>rev 1 · scope.html · "邮箱密码登录"</li> <li>rev 2 · scope.html · "扩展:OAuth 登录"</li> </ul> </p> <p>三种解读,请选: <ol> <li>完全移除登录(rev 1/2 标 removed,新 PRD 无 auth)</li> <li>登录变可选(默认匿名,登录加增值功能)</li> <li>仅砍 OAuth,保留邮箱密码登录</li> </ol> </p> </aside> # 触发 SSE 后 PM 立即返回(不写其他章节,等人决策) timeline_append("prd_conflict", "prd@conflict-pending", "3 选 1 等人") sseBroker.emit(pid, "human_intervention_needed", { reason: "prd_conflict", options: ["remove_login", "login_optional", "remove_oauth_only"] }) # OUTPUT { "ok": true, // PM 没失败,只是停下等人 "mode": "incremental", "prd_rev": 2, // 没升 rev,只挂 conflict "files_written": ["prd/index.html"], // 只在 index 加 aside "has_conflict": true, "conflict_detail": "与 rev 1+2 的 scope/login 矛盾", ... }
⑦ 助手工具源码
7.1 html-section-patcher.js · section 级精准 patch
// server/services/agent-tools/html-section-patcher.js · ~80 行 const cheerio = require('cheerio'); const fs = require('fs'); /** * Append a new section to a PRD chapter file. * 不重写已有 section,只增 data-rev=N+1 的新 section。 */ function appendSection({ chapterPath, sectionHtml, origin }) { const raw = fs.readFileSync(chapterPath, 'utf8'); const $ = cheerio.load(raw, { decodeEntities: false }); const article = $('article[data-doc="prd"]'); if (article.length === 0) throw new Error('No <article data-doc="prd"> found'); // 找最大 data-rev,新 section 用 +1 let maxRev = 0; article.find('section[data-rev]').each((_, el) => { const r = parseInt($(el).attr('data-rev'), 10); if (r > maxRev) maxRev = r; }); // 构造新 section const $new = cheerio.load('<div>' + sectionHtml + '</div>'); const sectionEl = $new('section').first(); if (sectionEl.length === 0) throw new Error('sectionHtml must contain <section>'); sectionEl.attr('data-rev', String(maxRev + 1)); sectionEl.attr('data-origin', origin); // 校验:section 必含 h3 + p if (sectionEl.find('h3').length === 0) throw new Error('New section missing <h3>'); if (sectionEl.find('p').length === 0) throw new Error('New section missing <p>'); // 追加 (在 footer 之前) const footer = article.find('footer[data-changelog]'); if (footer.length) footer.before(sectionEl.toString()); else article.append(sectionEl.toString()); // 升 article data-rev article.attr('data-rev', String(maxRev + 1)); fs.writeFileSync(chapterPath, $.html()); return { new_rev: maxRev + 1, section_id: sectionEl.attr('id') || null }; } function addConflictAside({ indexPath, text, conflicts, options }) { const raw = fs.readFileSync(indexPath, 'utf8'); const $ = cheerio.load(raw); $('aside[data-conflict]').remove(); // 移除旧 conflict const aside = `<aside data-conflict data-origin="${origin}">...</aside>`; $('article[data-doc="prd"]').prepend(aside); fs.writeFileSync(indexPath, $.html()); } module.exports = { appendSection, addConflictAside };
7.2 prd-validator.js · 整站校验
// server/services/agent-tools/prd-validator.js · ~70 行 const cheerio = require('cheerio'); const fs = require('fs'); const path = require('path'); const REQUIRED_CHAPTERS = [ 'index.html', 'reading-guide.html', 'product/background.html', 'product/personas.html', 'product/original-req.html', 'product/scope.html', 'product/success-metrics.html', 'product/glossary.html', 'feature/feature-tree.html', 'flow/user-flows.html', ]; const EIGHT_TYPES = { Tables: $ => $('table').length > 0, Code: $ => $('pre.code, pre.tree').length > 0, Illustrations: $ => $('svg').length > 0, Interaction: $ => $('.tabs, details.acc, nav.toc').length > 0, Spatial: $ => $('pre.tree, .grid-2, .grid-3, .grid-4').length > 0, Design: $ => $('.card, .agent-hero, .mock').length > 0, Images: $ => $('svg, img, .mock').length > 0, Workflows: $ => $('svg').filter((_,el) => $(el).html().includes('marker-end')).length > 0, }; function validatePrdSite(pid) { const root = path.resolve(projectsDir, pid, 'prd'); const issues = []; // 1. 必备章节 for (const f of REQUIRED_CHAPTERS) { if (!fs.existsSync(path.join(root, f))) issues.push(`missing required chapter: ${f}`); } // 2. 每章节 cheerio 校验 const files = walkHtml(root); for (const f of files) { const $ = cheerio.load(fs.readFileSync(f, 'utf8')); // 顶级 article const art = $('article[data-doc="prd"]'); if (art.length === 0) issues.push(`${f}: missing <article data-doc="prd">`); // section h3 + p art.find('section').each((_, sec) => { if ($(sec).find('h3').length === 0) issues.push(`${f}: section without h3`); if ($(sec).find('p').length === 0) issues.push(`${f}: section without p`); }); // 8 类信息 ≥ 4 const covered = Object.entries(EIGHT_TYPES).filter(([_, fn]) => fn($)); if (covered.length < 4) issues.push(`${f}: only ${covered.length}/8 types covered (need ≥4)`); // data-rev 单调 const revs = art.find('section[data-rev]').map((_,s) => parseInt($(s).attr('data-rev'), 10)).get(); if (revs.some((v,i) => i > 0 && v < revs[i-1])) issues.push(`${f}: data-rev not monotonic`); } return { ok: issues.length === 0, issues }; } module.exports = { validatePrdSite };
7.3 prd-site-init.js · bootstrap_prd_site 工具实现
// server/services/agent-tools/prd-site-init.js · ~50 行 const fs = require('fs'); const path = require('path'); const TEMPLATE_DIR = path.resolve(__dirname, '../../../docs/prd/newfunction/assets'); function bootstrapPrdSite(pid) { const root = path.resolve(projectsDir, pid, 'prd'); const assetsDir = path.join(root, 'assets'); // 建目录 fs.mkdirSync(assetsDir, { recursive: true }); fs.mkdirSync(path.join(root, 'product'), { recursive: true }); fs.mkdirSync(path.join(root, 'feature'), { recursive: true }); fs.mkdirSync(path.join(root, 'flow'), { recursive: true }); fs.mkdirSync(path.join(root, 'prototype'), { recursive: true }); // 复制 shell.css (一次,共享样式) fs.copyFileSync(path.join(TEMPLATE_DIR, 'shell.css'), path.join(assetsDir, 'shell.css')); // 复制 shell.js 模板 + 注入空 SITE const jsTpl = fs.readFileSync(path.join(TEMPLATE_DIR, 'shell.js'), 'utf8'); // shell.js 内部 SITE 来自 PM Agent 维护的 prd-site-config.json fs.writeFileSync(path.join(assetsDir, 'shell.js'), jsTpl); // 空 search-index 占位 fs.writeFileSync(path.join(assetsDir, 'search-index.json'), JSON.stringify({ version:'1', pages:0, entries:[] })); return { root, files_created: 3 }; } module.exports = { bootstrapPrdSite };
⑧ 自检 Checklist · 完工前必跑
| # | 检查项 | 命令 / 工具 | 通过判定 |
|---|---|---|---|
| 1 | 整站校验 | validate_prd_site(pid) | ok=true, issues=[] |
| 2 | 搜索索引同步 | update_search_index(pid) | pages 字段含所有新写文件 |
| 3 | 必备 11 章节都存在 | fs.existsSync 循环 | 全 true |
| 4 | 每页 ≥4 类信息 | validatePrdSite 内置 | 无 "X/8 types" 错误 |
| 5 | 每个 section 含 h3 + p | validatePrdSite 内置 | 无 "section without h3/p" |
| 6 | data-rev 单调递增 | validatePrdSite 内置 | 无 "not monotonic" |
| 7 | todolist 全 done | read agent-state/pm-todo.json | 所有 item.status='done' |
| 8 | timeline 含 prd_drafted | tail timeline.ndjson | 最后 5 条之一 kind=='prd_drafted' |
| 9 | agent-state.pm.json 终态 | read M-10 | status='done', last_output_ref 非空 |
| 10 | 无 Bash inline JSON 使用 | grep coder.log? (PM 无 log,仅审查 prompt 自身) | — |
| 11 | changelog.ndjson append | 由 DocStore 自动 | 新 entry kind='prd_rev' |
任一不过 → 不输出 ok=true。自己 patch 后重跑自检,最多 retry 2 次内必过。第 3 次仍不过 → 报 validation_failed 上报 L3。
⑨ 失败模式与降级
| 场景 | 检测 | 降级 |
|---|---|---|
| 需求过模糊(<20 字、无动词) | token 数 + 词性快速判断 | 不写 PRD,直接 ask_clarification → SSE |
| 新需求与历史矛盾 | 读历史 + 语义比对 | 插 <aside data-conflict>,不合并,触发 human |
| 沙箱写越权 | Write 工具拒绝 | Coder 收到 error,换路径 |
| validate_prd_site 失败 | self-check 步骤 | 读 issues 列表 → patch 后重跑(≤ 2 次) |
| search-index 构建失败 | update_search_index 报错 | 跳过索引但仍输出 PRD;标记 partial=true |
| memory 文件读失败 | I/O 错误 | 记 warning,继续(视同空 memory) |
| todolist 中途崩 | session 异常 | P-8 处理:写 paused → 等 resume 决策 |
| cheerio 解析非法 HTML | html-section-patcher 报错 | 不写盘,retry 一次 PM 重生这段 |
⑩ 与其他 Agent 的握手摘要
| 对方 | PM 给的 | PM 收的 |
|---|---|---|
| Human | conflict_aside / clarification request | requirement text · sign-off · abandon |
| Architect | 完整 prd/ 站点 + changelog entry | (无;Architect 自己读 prd/) |
| Dashboard | SSE prd.updated · agent.* 系列 | (无) |
| LoopController | PMOutput JSON | PMInput JSON · resume_token |
| DocStore | (无;通过工具间接) | 校验通过/失败回执 |
| memory/devloop/pm/ | 新 .md 文件 | 启动时被 LoopController 注入到 prompt |
⑪ 「测试 PM Agent 是否真的能跑」的实战命令
本规范的验证终局:用以下命令直接驱动 PM Agent,看能否 30 秒内产出符合所有约束的 PRD 站点:
# 1. 注册 Agent 模板 # 把本页 §2.2 的 System Prompt 落到: # system/agents/devloop-pm/prompt.md # 并在 system/agents/_registry.json 加 'devloop-pm' 条目 # 2. 启动 server (P1 阶段最小后端,只挂 PM 路径) PORT=3010 node server/index.js # 3. 直接调 PM (绕过 LoopController) # 准备 /tmp/pm-test.json (单行紧凑,按 CLAUDE.md 警告) echo '{"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"}' > /tmp/pm-test.json curl -X POST http://localhost:3010/api/devloop/_test/run-agent/pm \ -H "Content-Type: application/json" \ -d @/tmp/pm-test.json # 4. 期望:30-60 秒内 # data/companies/c-test/projects/p-test001/prd/ 下铺好 11 章节 # + assets/{shell.css, shell.js, search-index.json} # + history/ 空 (init 首次,无归档) # 5. 校验 node -e "console.log(require('./server/services/agent-tools/prd-validator').validatePrdSite('p-test001'))" # 期望: { ok: true, issues: [] } # 6. 浏览器打开 # file:///data/companies/c-test/projects/p-test001/prd/index.html # 左侧应见 3 级菜单,Ctrl+K 可搜索