Commit 105a2e5c69725734f3fad79546758dfafb8d5524

Authored by zichun
1 parent bc2388c0

coding.mjs: trim duplicated rationale comments and dedupe identical schemas/prompt blocks

Showing 1 changed file with 58 additions and 143 deletions
workflows/coding.mjs
1 1 // workflows/coding.mjs
2 2 //
3 3 // 整个 ERP Coding(B 阶段)= 一个静默、全自动的 Workflow 脚本。
4   -//
5   -// 设计原则(详见仓库根 README.md 「阶段 B」 节 与 「设计原则」 节):
6   -// - 所有 stage 都是 agent() 子代理,物理上无法 AskUserQuestion → 编码期结构性静默。
7   -// - 缺值不再问人:派生 stage 把具体阻塞点写进产物并 throw(fail-fast,合法 halt → 终止态,非对话框)。
8   -// - 后端 / 前端功能循环由同一份 featureLoop(items, phase) 驱动;phase 切换 reviewer checklist、
9   -// 测试命令、路径作用域(backend/ vs frontend/)、id 格式(REQ-XXX-NNN vs FE-NN)。
10   -// - **featureLoop 采用顺序 for-await**(不是 pipeline)。两条理由:
11   -// (1) tdd/fix stage 会在共享工作树 + 同一功能分支上 git commit / 编辑源码;并发会争 .git/index.lock
12   -// 并撞 migration 版本号;
13   -// (2) pipeline 的语义是"stage 抛异常 → 该 item 掉 null、pipeline 永不 reject",会把
14   -// reviewWithFixLoop / verify / tdd 的 HALT throw 静默吞掉,使 fail-fast 在功能链层级失效,
15   -// 残缺模块照样会被 testGate/report/milestone 推进。顺序 for-await 让 throw 自然冒泡到
16   -// 模块主循环的 try/catch,被捕获后整阶段 fail-fast break。
17   -// - 状态账本 = docs/08 §二/§三 + git tag;halt 后重跑 coding-start,router 从账本+tag 重算进度。
18   -// - reviewer 统一为 agents/code-reviewer.md,review stage 用 agentType:'code-reviewer'。
19   -//
20   -// 运行时约束:Workflow 运行时禁用非确定性内建(取当天日期 / 随机数的 API)。本脚本不调用它们;
21   -// 凡需要"当天日期"的产物路径(<YYYY-MM-DD>-<id>.md),一律由子代理在其自身上下文中解析并落盘,
22   -// 脚本只负责编排,不计算日期 / 随机数。
  4 +// 设计原则见仓库根 README.md「阶段 B」与「设计原则」节;featureLoop 顺序 for-await 的取舍
  5 +// 详见 featureLoop 函数处的注释。运行时禁用日期 / 随机数 builtin,所有"今天"由子代理解析。
23 6  
24 7 export const meta = {
25 8 name: 'erp-coding',
... ... @@ -38,12 +21,7 @@ const ROUTER_SCHEMA = { type:&#39;object&#39;, additionalProperties:false,
38 21 reqs:{type:'array',items:{type:'string'}},
39 22 feItems:{type:'array',items:{type:'string'}} } } } } }
40 23  
41   -// REVIEW_SCHEMA:reviewer 返回的裁决。issues 改为结构化对象,避免"模糊一行 must-fix"
42   -// 让 fix stage 无从下手就空转 5 轮(见 reviewWithFixLoop 的 must-fix 闸门)。
43   -// - summary:人类可读的问题摘要(一句)。
44   -// - locator:必须能让 fix stage 定位到文件(含 `<repo-relative-path>` 或 `<path>:<line>`),
45   -// 否则在 reviewWithFixLoop 里直接判违约 HALT。
46   -// - severity:blocker/high/medium/low,方便后续把 low/medium 降级为 suggestion 而不卡循环。
  24 +// REVIEW_SCHEMA:reviewer 裁决;issues 结构化对象(summary/locator/severity)驱动 fix。
47 25 const REVIEW_SCHEMA = { type:'object', additionalProperties:false,
48 26 required:['verdict','round','issues'], properties:{
49 27 verdict:{type:'string',enum:['approve','request-changes']},
... ... @@ -56,13 +34,7 @@ const REVIEW_SCHEMA = { type:&#39;object&#39;, additionalProperties:false,
56 34 locator:{type:'string'},
57 35 severity:{type:'string', enum:['blocker','high','medium','low']} } } } } }
58 36  
59   -// STAGE_RESULT_SCHEMA:派生 stage(spec/plan/tdd/verify/fix/report)的统一结构化返回。
60   -// - status=ok:本步骤产出可用,artifactPath 必填(spec/plan/verify/report 的落盘文件),
61   -// summary 可放给下游用作 prompt 上下文。tdd/fix 没有单一 artifact,artifactPath 可省。
62   -// - status=halt:sub-agent 已经决定无法继续(缺值 / 越界 / 重试到顶),把阻塞点写进 reason,
63   -// JS 端读到立即 throw `HALT …`,让 fail-fast 顺序 for-await 冒泡到模块主循环 try。
64   -// - 无 schema 时,sub-agent 可以"写一段散文说我跑不下去了,但仍然算成功返回"——这是真正的
65   -// fail-fast 漏洞;加 schema 后所有派生 stage 都有显式 halt 通道。
  37 +// STAGE_RESULT_SCHEMA:派生 stage 统一返回,status=halt 时 JS 立即 throw HALT。
66 38 const STAGE_RESULT_SCHEMA = { type:'object', additionalProperties:false,
67 39 required:['status'], properties:{
68 40 status:{type:'string', enum:['ok','halt']},
... ... @@ -75,11 +47,6 @@ const GATE_SCHEMA = { type:&#39;object&#39;, additionalProperties:false,
75 47 failures:{type:'array',items:{type:'string'}} } }
76 48  
77 49 // ── 微步骤 schemas(runBranchSetup / runMilestone / runCrossModule 用)─────────
78   -// 这三个阶段是纯机械的 git/文件操作 + 条件跳过;与其让子代理读"1. 2. 3. 若 X 则跳过"的散文
79   -// 流程,不如把"observe → JS branch → execute"切成多个 agent 微步骤,每步带强 schema 返回。
80   -// 这样:(a) 跳过/分支条件由 JS 判定(不再依赖 LLM 读散文条件),idempotency 一致;
81   -// (b) 每步语义单一、prompt 短,schema 校验阻断畸形返回;
82   -// (c) action 步统一返回 ACTION_RESULT_SCHEMA(success/error),失败可由 JS 抛错 halt。
83 50 const WT_SCHEMA = { type:'object', additionalProperties:false,
84 51 required:['clean'], properties:{
85 52 clean:{type:'boolean'},
... ... @@ -91,30 +58,20 @@ const DEFAULT_BRANCH_SCHEMA = { type:&#39;object&#39;, additionalProperties:false,
91 58 const EXISTS_SCHEMA = { type:'object', additionalProperties:false,
92 59 required:['exists'], properties:{ exists:{type:'boolean'} } }
93 60  
94   -const CURRENT_BRANCH_SCHEMA = { type:'object', additionalProperties:false,
95   - required:['branch'], properties:{ branch:{type:'string'} } }
96   -
97 61 const FIELD_VALUE_SCHEMA = { type:'object', additionalProperties:false,
98 62 required:['found','value'], properties:{
99 63 found:{type:'boolean'},
100 64 value:{type:'string'},
101 65 lineNumber:{type:'integer'} } }
102 66  
103   -// CHECKBOX_STATE_SCHEMA:docs/08 中 `- [ ] REQ-XXX-NNN ...` / `- [x] FE-NN ...` 这类功能行
104   -// 的勾选态。把"审阅 approve 后 flip checkbox"从 reviewPrompt 的隐式 side-effect 改为可观测
105   -// 的 read-then-write micro step(参见 reviewWithFixLoop 的 approve 分支)。
106   -// 注意:state 必填——schema 只 require found 时,sub-agent 返回 {found:true} 而漏掉 state 仍合法,
107   -// 上层 if (cb.state === 'unchecked') 会静默落入"已 checked"分支,docs/08 与 review 裁决悄悄背离。
  67 +// CHECKBOX_STATE_SCHEMA:docs/08 功能行勾选态;state 必填——只 require found 时 cb.state 缺失会静默走 checked 分支。
108 68 const CHECKBOX_STATE_SCHEMA = { type:'object', additionalProperties:false,
109 69 required:['found','state'], properties:{
110 70 found:{type:'boolean'},
111 71 state:{type:'string', enum:['checked','unchecked']},
112 72 lineNumber:{type:'integer'} } }
113 73  
114   -// TAG_REPORT_FRESHNESS_SCHEMA:当 milestone tag 已存在(resume 或前一轮残留)时,校验
115   -// tag 指向的 commit 是否包含已落地的 § ⑫ 值。旧版 bug:tag → § ⑫ commit 的错序会让 tag
116   -// 指向 \`{{milestone_tag}}\` 占位符 commit;新顺序(report → tag)下不会再产生,但保留
117   -// freshness 自检以发现历史残留。
  74 +// TAG_REPORT_FRESHNESS_SCHEMA:保留以发现旧版 tag → § ⑫ 错序 bug 残留(tag 指向占位符 commit)。
118 75 const TAG_REPORT_FRESHNESS_SCHEMA = { type:'object', additionalProperties:false,
119 76 required:['fresh'], properties:{
120 77 fresh:{type:'boolean'},
... ... @@ -150,20 +107,12 @@ const ACTION_RESULT_SCHEMA = { type:&#39;object&#39;, additionalProperties:false,
150 107 detail:{type:'string'} } }
151 108  
152 109 const ROOT = args?.projectRoot || '.'
153   -// 子代理在 ${ROOT} 路径上跑 git -C / Read / Edit。相对路径 '.' 会绑定到子代理隐式 cwd,无保证。
154   -// 必须由 coding-start 显式传绝对路径;否则 fail-fast 让人工修复入口而不是在错路径上静默打 tag。
  110 +// ROOT 必须是绝对路径——相对 '.' 会绑定到子代理隐式 cwd,无保证。
155 111 if (ROOT === '.' || !(/^(?:\/|[A-Za-z]:[\\/])/.test(ROOT))) {
156 112 throw new Error(`HALT invalid-projectRoot: must be absolute, got ${JSON.stringify(ROOT)}. coding-start 必须把绝对路径传入 args.projectRoot。`)
157 113 }
158 114  
159   -// ============================================================================
160   -// Stage prompt builders(纯字符串构造;只用 ROOT / id / phase / 入参,不触非确定性内建)
161   -//
162   -// 每个 prompt 的共同契约(见 featureStageContract):
163   -// - 子代理是非交互的,物理上无法弹窗;缺任何值都不要"问人"——把具体阻塞点写进产物并失败。
164   -// - phase=backend 与 phase=frontend 的差异(路径作用域 / id 形态 / 测试命令来源)逐条写明。
165   -// - 所有输出文档用中文。
166   -// ============================================================================
  115 +// ── Feature-loop stage prompts(共享非交互契约见 featureStageContract)──
167 116  
168 117 function isFrontend(phase) { return phase === 'frontend' }
169 118  
... ... @@ -190,7 +139,9 @@ function featureStageContract(phase) {
190 139 return [
191 140 '## 硬约束(非交互子代理)',
192 141 '- 你是 Workflow 派生的**非交互子代理**,物理上无法弹出 AskUserQuestion / 等待人类输入。**绝不要尝试问人**。',
193   - '- 需要具体值时,按此顺序自行获取:(1) 先查 `.env.local`、`docs/07-环境配置.md`、`docs/04-技术规范.md`、`docs/05-API接口契约.md`、`docs/06-UI交互规范.md`、`CLAUDE.md`、现有代码;(2) 仍查不到 → **不要编造、不要留 `【人工填写:】` / `TBD` / `TODO` 占位**,而是把**具体的阻塞点**(缺哪个值、应在哪个 Plan 期闸门锁定、为何无法继续)写进产物,然后让本步骤**失败**(以非零结果 / 显式 throw 结束),由上层 Workflow 转为带诊断的 halt。',
  142 + '- 缺值查找顺序:`.env.local` → `docs/07-环境配置.md` → `docs/04-技术规范.md` → `docs/05-API接口契约.md` → `docs/06-UI交互规范.md` → `CLAUDE.md` → 现有代码。',
  143 + '- 仍查不到 → **不要编造、不要留 `【人工填写:】` / `TBD` / `TODO` 占位**;把具体阻塞点(缺哪个值、应在哪个 Plan 闸门锁定、为何无法继续)写进产物。',
  144 + '- 然后让本步骤以非零结果 / 显式 throw 结束,由上层 Workflow 转为带诊断的 halt(fail-fast)。',
194 145 '- 全部输出文档**使用中文**。',
195 146 `- **阶段 = ${fe ? '前端(frontend)' : '后端(backend)'}**。路径作用域:${fe
196 147 ? '实现文件必须落在 `frontend/`(或 `docs/09-项目目录结构.md § 前端目录结构` 声明的前端根)下;命中 `backend/` / `sql/` / `scripts/` 即越界,硬停。'
... ... @@ -199,6 +150,17 @@ function featureStageContract(phase) {
199 150 ].join('\n')
200 151 }
201 152  
  153 +// commitBlock:spec/plan/verify/review 共用的"写完 → add → commit → 失败 halt"四行块。
  154 +function commitBlock(addPath, msg, tail = '- commit 失败 → halt,把 stderr 摘要写进 reason。') {
  155 + return [
  156 + '## commit',
  157 + `- 写完后必须 commit(milestone 的 worktree-clean 前置依赖此 commit):`,
  158 + ` 1. \`git -C ${ROOT} add ${addPath}\``,
  159 + ` 2. \`git -C ${ROOT} commit -m "${msg}"\``,
  160 + tail,
  161 + ].join('\n')
  162 +}
  163 +
202 164 // Router:读 docs/08 §二/§三 + git tag,重算进度,返回 ROUTER_SCHEMA。
203 165 function routerPrompt(root) {
204 166 return [
... ... @@ -264,11 +226,7 @@ function deriveSpecPrompt(id, phase) {
264 226 ? '- 规格至少含:关联 REQ + 关联原型;组件树(按页面 / 区域分块,推导自 prototype DOM);页面状态机(loading / empty / error / 正常 / 表单提交中 至少 5 态);消费的后端端点(对齐 docs/05);业务规则前端复刻清单(逐条:规则 / 触发时机 / 报错文案 / 来源 REQ);Design Tokens 引用清单(`var(--color-*)`)。'
265 227 : '- 规格覆盖:goal / 输入输出 / 业务规则 / 约束 / schema / API 引用 / acceptance criteria。',
266 228 '',
267   - '## commit',
268   - `- 写完 spec 后必须 commit(milestone 的 worktree-clean 前置依赖此 commit):`,
269   - ` 1. \`git -C ${ROOT} add <spec artifactPath>\``,
270   - ` 2. \`git -C ${ROOT} commit -m "docs(spec:${id}): 派生规格"\``,
271   - '- commit 失败 → halt,把 stderr 摘要写进 reason。',
  229 + commitBlock('<spec artifactPath>', `docs(spec:${id}): 派生规格`),
272 230 '',
273 231 '## 自审(inline 修,无须等待)',
274 232 `- 占位符扫描:\`TBD\` / \`TODO\` / \`【人工填写:】\`${fe ? ' / `controller` / `service` / `SQL` / `migration`(前端 spec 不应出现后端字样)' : ''} → 命中即修;修不掉的缺值按硬约束失败。`,
... ... @@ -315,11 +273,7 @@ function planPrompt(id, phase, specPath) {
315 273 `- 落盘路径:\`docs/superpowers/plans/<同 spec 的 YYYY-MM-DD>-${id}.md\`,文件头含 Goal / Architecture / Tech Stack + checkbox 任务。`,
316 274 '- 自审:占位符扫描(按硬约束清单);spec coverage(spec 每节至少指向一个 task,补 gap);类型一致性(签名 / 方法名 / 错误码 / props 一致)。',
317 275 '',
318   - '## commit',
319   - `- 写完 plan 后必须 commit(milestone 的 worktree-clean 前置依赖此 commit):`,
320   - ` 1. \`git -C ${ROOT} add <plan artifactPath>\``,
321   - ` 2. \`git -C ${ROOT} commit -m "docs(plan:${id}): 任务级 TDD 计划"\``,
322   - '- commit 失败 → halt,把 stderr 摘要写进 reason。',
  276 + commitBlock('<plan artifactPath>', `docs(plan:${id}): 任务级 TDD 计划`),
323 277 '',
324 278 '## 输出(必须符合下发的 STAGE_RESULT JSON schema)',
325 279 '- 成功:`{ "status": "ok", "artifactPath": "docs/superpowers/plans/YYYY-MM-DD-' + id + '.md", "summary": "<1-2 句中文摘要:任务数 / 涉及文件作用域>" }`',
... ... @@ -398,11 +352,8 @@ function verifyPrompt(id, phase, implSummary, specPath, round = 0) {
398 352 ].join('\n'),
399 353 `- 证据落盘路径固定为 \`docs/superpowers/reviews/<同 spec 的 YYYY-MM-DD>-${id}-${suffix}.md\`(与 review 报告同目录;round=0 → \`-verify.md\`;round>=1 → \`-verify-r<N>.md\`,**每轮独立文件不覆盖前轮**)。同时把核心结构化结果摘要打印到会话便于上层 review stage 引用,**不要**自行另起目录或自由命名文件。`,
400 354 '',
401   - '## commit',
402   - `- 写完证据后必须 commit(milestone 的 worktree-clean 前置依赖此 commit):`,
403   - ` 1. \`git -C ${ROOT} add <证据 artifactPath>\``,
404   - ` 2. \`git -C ${ROOT} commit -m "docs(verify:${id}${round > 0 ? `:r${round}` : ''}): 证据验证"\``,
405   - '- commit 失败 → halt,把 stderr 摘要写进 reason(仍要返回已写入的证据路径)。',
  355 + commitBlock('<证据 artifactPath>', `docs(verify:${id}${round > 0 ? `:r${round}` : ''}): 证据验证`,
  356 + '- commit 失败 → halt,把 stderr 摘要写进 reason(仍要返回已写入的证据路径)。'),
406 357 '',
407 358 '## 输出(必须符合下发的 STAGE_RESULT JSON schema)',
408 359 `- 全部通过:\`{ "status": "ok", "artifactPath": "docs/superpowers/reviews/YYYY-MM-DD-${id}-${suffix}.md", "summary": "<exit_code / passed / failed / failed_list 摘要 ≤ 200 字>" }\`。`,
... ... @@ -439,11 +390,9 @@ function reviewPrompt(id, phase, round, lastVerifySummary, specPath) {
439 390 `- **不要**在本步骤里编辑 docs/08 的 \`- [ ]\` checkbox——该 side effect 由上层 Workflow 的 micro step 在 approve 后另行落盘(你只负责裁决)。`,
440 391 '- 不要返回额外字段(schema 是 `additionalProperties:false`)。',
441 392 '',
442   - '## commit',
443   - `- 写完审阅报告后必须 commit(milestone 的 worktree-clean 前置依赖此 commit;该 commit 与 verdict 无关,approve 或 request-changes 都要 commit 报告本身):`,
444   - ` 1. \`git -C ${ROOT} add docs/superpowers/reviews/<同 spec 的 YYYY-MM-DD>-${id}.md\``,
445   - ` 2. \`git -C ${ROOT} commit -m "docs(review:${id}:r${round}): <verdict>"\``,
446   - '- commit 失败时仍按 schema 返回 verdict / issues;commit 错误信息打印到日志即可(不要在 schema 中夹带额外字段)。',
  393 + commitBlock(`docs/superpowers/reviews/<同 spec 的 YYYY-MM-DD>-${id}.md`,
  394 + `docs(review:${id}:r${round}): <verdict>`,
  395 + '- commit 失败时仍按 schema 返回 verdict / issues;commit 错误信息打印到日志即可(不要在 schema 中夹带额外字段)。'),
447 396 ].filter(Boolean).join('\n')
448 397 }
449 398  
... ... @@ -465,9 +414,6 @@ function fixPrompt(id, phase, issues) {
465 414 '## 流程',
466 415 '- 逐项编辑 locator 指向的代码文件(遵守阶段路径作用域护栏)。',
467 416 `- 编辑前必须先校验 locator 文件存在:跑 \`git -C ${ROOT} cat-file -e HEAD:<locator 的文件部分>\`(locator 形如 \`path:line\` 时取 \`path\`)。文件不存在 → halt,把 locator 写进 reason,不要"修一个不存在的文件"。`,
468   - fe
469   - ? '- **硬护栏(与 tdd 同款)**:命中 `backend/` / `sql/` / `scripts/` → halt 并把 file 路径写进 reason。'
470   - : '- **硬护栏(与 tdd 同款)**:任一被编辑文件以 `frontend/` 开头 → halt 并把 file 路径写进 reason。',
471 417 `- 修复后 commit:\`fix(<scope>): 修复 review must-fix ${fe ? `FE: ${id}` : `REQ: ${id}`}\`(不混合无关改动)。`,
472 418 '- 修复完成后本步骤即结束;上层 Workflow 会重新跑 verify + review(下一轮)。',
473 419 '',
... ... @@ -563,7 +509,7 @@ function currentBranchPromptM() {
563 509 microStepContract(),
564 510 '',
565 511 `跑 \`git -C ${ROOT} rev-parse --abbrev-ref HEAD\`。`,
566   - '## 输出(CURRENT_BRANCH_SCHEMA)',
  512 + '## 输出(BRANCH_NAME_SCHEMA)',
567 513 '- `{ "branch": "<stdout 第一行去空白>" }`',
568 514 ].join('\n')
569 515 }
... ... @@ -646,25 +592,24 @@ function executeMergePromptM(defaultBranch, branch, phaseId) {
646 592 }
647 593  
648 594 function readDocs08FieldPromptM(fe, id) {
649   - if (fe) {
650   - return [
651   - '# 读 docs/08 § 三 `整体里程碑:` 字段当前值',
652   - microStepContract(),
653   - '',
654   - `Read \`${ROOT}/docs/08-模块任务管理.md\`,定位 § 三(前端阶段)下的 \`- 整体里程碑: <value>\` 行。`,
655   - '## 输出(FIELD_VALUE_SCHEMA)',
656   - '- 命中:`{ "found": true, "value": "<冒号后去空白的当前值>", "lineNumber": <该行 1-based 行号> }`',
657   - '- § 三 或该行不存在:`{ "found": false, "value": "" }`',
658   - ].join('\n')
659   - }
  595 + const section = fe ? '§ 三' : '§ 二'
  596 + const title = fe
  597 + ? '# 读 docs/08 § 三 `整体里程碑:` 字段当前值'
  598 + : `# 读 docs/08 § 二 模块 \`${id}\` 的 \`里程碑:\` 字段当前值`
  599 + const locator = fe
  600 + ? `Read \`${ROOT}/docs/08-模块任务管理.md\`,定位 ${section}(前端阶段)下的 \`- 整体里程碑: <value>\` 行。`
  601 + : `Read \`${ROOT}/docs/08-模块任务管理.md\`,定位 ${section} 中 module id == \`${id}\` 的 bullet 段,取其 \` - 里程碑: <value>\` 子项。`
  602 + const missing = fe
  603 + ? '- § 三 或该行不存在:`{ "found": false, "value": "" }`'
  604 + : `- 模块 \`${id}\` 或该字段不存在:\`{ "found": false, "value": "" }\``
660 605 return [
661   - `# 读 docs/08 § 二 模块 \`${id}\` 的 \`里程碑:\` 字段当前值`,
  606 + title,
662 607 microStepContract(),
663 608 '',
664   - `Read \`${ROOT}/docs/08-模块任务管理.md\`,定位 § 二 中 module id == \`${id}\` 的 bullet 段,取其 \` - 里程碑: <value>\` 子项。`,
  609 + locator,
665 610 '## 输出(FIELD_VALUE_SCHEMA)',
666 611 '- 命中:`{ "found": true, "value": "<冒号后去空白的当前值>", "lineNumber": <行号> }`',
667   - `- 模块 \`${id}\` 或该字段不存在:\`{ "found": false, "value": "" }\``,
  612 + missing,
668 613 ].join('\n')
669 614 }
670 615  
... ... @@ -690,33 +635,22 @@ function writeDocs08FieldPromptM(fe, id, targetValue, phaseId, lineNumber) {
690 635 ].join('\n')
691 636 }
692 637  
693   -// ── 微步骤:docs/08 功能行 checkbox 勾选态(reviewer approve 后的可观测 side effect)──
694   -// 原 reviewPrompt 让 reviewer 顺手 flip checkbox:reviewer 失败时 router/进度判定看不到,且
695   -// reviewer agent 多了一项 file edit 副作用。拆为 read-then-write 两个 micro step:
696   -// reviewWithFixLoop approve → read → 若 unchecked → write → assert success;
697   -// 已 checked → 静默跳过(resume 幂等)。
  638 +// ── 微步骤:docs/08 功能行 checkbox(reviewer approve 后的可观测 side effect;read-then-write)──
698 639 function readDocs08CheckboxPromptM(fe, id) {
699   - if (fe) {
700   - return [
701   - `# 读 docs/08 § 三 功能 \`${id}\` 的勾选态(\`- [ ] ${id} ...\` / \`- [x] ${id} ...\`)`,
702   - microStepContract(),
703   - '',
704   - `Read \`${ROOT}/docs/08-模块任务管理.md\`,定位 § 三(前端阶段)下的 \`功能:\` 项,从中找以 \`- [ ] ${id} \` 或 \`- [x] ${id} \` 开头的行(注意 id 后必须紧跟空格,避免误中前缀同名)。`,
705   - '## 输出(CHECKBOX_STATE_SCHEMA)',
706   - `- 命中 \`- [x] ${id} ...\`:\`{ "found": true, "state": "checked", "lineNumber": <行号> }\``,
707   - `- 命中 \`- [ ] ${id} ...\`:\`{ "found": true, "state": "unchecked", "lineNumber": <行号> }\``,
708   - `- 找不到:\`{ "found": false }\``,
709   - ].join('\n')
710   - }
  640 + const section = fe ? '§ 三' : '§ 二'
  641 + const kind = fe ? '功能' : 'REQ'
  642 + const locator = fe
  643 + ? `Read \`${ROOT}/docs/08-模块任务管理.md\`,定位 ${section}(前端阶段)下的 \`功能:\` 项,从中找以 \`- [ ] ${id} \` 或 \`- [x] ${id} \` 开头的行(注意 id 后必须紧跟空格,避免误中前缀同名)。`
  644 + : `Read \`${ROOT}/docs/08-模块任务管理.md\`,定位 ${section},找以 \`- [ ] ${id} \` 或 \`- [x] ${id} \` 开头的行(id 后必须紧跟空格)。该行可能位于任一模块 bullet 下。`
711 645 return [
712   - `# 读 docs/08 § 二 REQ - `# 读 docs/08 § 二 REQ${id}- `# 读 docs/08 § 二 REQ 的勾选态(- `# 读 docs/08 § 二 REQ- [ ] ${id} ...- `# 读 docs/08 § 二 REQ / - `# 读 docs/08 § 二 REQ- [x] ${id} ...- `# 读 docs/08 § 二 REQ)`,
  646 + `# 读 docs/08 ${section} ${kind} + `# 读 docs/08 ${section} ${kind}${id}+ `# 读 docs/08 ${section} ${kind} 的勾选态(+ `# 读 docs/08 ${section} ${kind}- [ ] ${id} ...+ `# 读 docs/08 ${section} ${kind} / + `# 读 docs/08 ${section} ${kind}- [x] ${id} ...+ `# 读 docs/08 ${section} ${kind})`,
713 647 microStepContract(),
714 648 '',
715   - `Read \`${ROOT}/docs/08-模块任务管理.md\`,定位 § 二,找以 \`- [ ] ${id} \` 或 \`- [x] ${id} \` 开头的行(id 后必须紧跟空格)。该行可能位于任一模块 bullet 下。`,
  649 + locator,
716 650 '## 输出(CHECKBOX_STATE_SCHEMA)',
717   - `- \`- [x] ${id} ...\` → \`{ "found": true, "state": "checked", "lineNumber": <行号> }\``,
718   - `- \`- [ ] ${id} ...\` → \`{ "found": true, "state": "unchecked", "lineNumber": <行号> }\``,
719   - `- 找不到:\`{ "found": false }\``,
  651 + `- 命中 \`- [x] ${id} ...\`:\`{ "found": true, "state": "checked", "lineNumber": <行号> }\``,
  652 + `- 命中 \`- [ ] ${id} ...\`:\`{ "found": true, "state": "unchecked", "lineNumber": <行号> }\``,
  653 + '- 找不到:`{ "found": false }`',
720 654 ].join('\n')
721 655 }
722 656  
... ... @@ -940,7 +874,7 @@ async function runBranchSetup(module) {
940 874 if (!r.success) throw new Error(`HALT branchSetup-create ${branch}: ${r.error || ''}`)
941 875 }
942 876  
943   - const head = await agent(currentBranchPromptM(), {label: lbl('head'), phase: 'Milestone', schema: CURRENT_BRANCH_SCHEMA})
  877 + const head = await agent(currentBranchPromptM(), {label: lbl('head'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA})
944 878 if (head.branch !== branch) throw new Error(`HALT branchSetup-branch-mismatch ${branch}: HEAD on ${head.branch}`)
945 879  
946 880 log(`branch-setup: ${id} → ${branch}`)
... ... @@ -1093,11 +1027,7 @@ async function featureLoop(items, phase) {
1093 1027 }
1094 1028 }
1095 1029  
1096   -// 有界 5 轮修复;超出 → throw(终止态,非对话框)。
1097   -// fix 后再跑 reverify 让 fix-stage 的 commit 有机会被新一轮 verify 看到;
1098   -// verify 内部失败 throw 在顺序 for-await 下会冒泡到模块主循环 try。
1099   -// approve 后通过独立 micro step 把 docs/08 对应 checkbox flip 为 `[x]`(拆出 reviewer side-effect,
1100   -// 写失败可观测;幂等:已 checked 静默跳过)。
  1030 +// 有界 5 轮修复;超出 → throw(终止态,非对话框)。approve 后独立 micro step flip docs/08 checkbox。
1101 1031 async function reviewWithFixLoop(id, phase, verifyResult, specPath) {
1102 1032 const grp = phase === 'backend' ? 'Backend' : 'Frontend'
1103 1033 const fe = isFrontend(phase)
... ... @@ -1105,19 +1035,14 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) {
1105 1035 let lastIssuesCount = 0
1106 1036 for (let round = 1; round <= 5; round++) {
1107 1037 const lastVerifySummary = (lastVerify && (lastVerify.summary || lastVerify.reason)) || ''
1108   - // 命名碰撞警告:opts.phase = grp('Backend'/'Frontend'/'Milestone')是 harness 的 UI 分组,
1109   - // 与 code-reviewer.md 文档里的"domain phase = backend|frontend"是**同名不同物**。
1110   - // reviewer agent 从 prompt 正文 `**phase = ...**` 那一行读 domain phase,**不要**读 opts.phase。
1111   - // 见 agents/code-reviewer.md "Domain phase resolution" 段。
  1038 + // opts.phase = grp('Backend'/'Frontend')是 harness UI 分组;domain phase 见 agents/code-reviewer.md。
1112 1039 const r = await agent(
1113 1040 reviewPrompt(id, phase, round, lastVerifySummary, specPath),
1114 1041 {label:`review:${phase}:${id}:r${round}`, phase: grp, schema: REVIEW_SCHEMA, agentType:'code-reviewer'}
1115 1042 )
1116 1043 if (r.verdict === 'approve') {
1117   - // docs/08 checkbox flip(observable side effect,原 reviewer 隐式 Edit → micro step)
1118 1044 const cb = await agent(readDocs08CheckboxPromptM(fe, id), {label:`cb?:${phase}:${id}`, phase: grp, schema: CHECKBOX_STATE_SCHEMA})
1119 1045 if (!cb.found) throw new Error(`HALT docs08-checkbox-missing ${phase}:${id}: docs/08 ${fe?'§ 三':'§ 二'} 中找不到 \`- [ ] ${id} ...\` / \`- [x] ${id} ...\` 行`)
1120   - // 防御:即使 schema 已 require state,再做一次 JS 校验,杜绝"found:true 但 state 缺失/枚举外"静默走 checked 分支。
1121 1046 if (cb.state !== 'checked' && cb.state !== 'unchecked') {
1122 1047 throw new Error(`HALT docs08-checkbox-state-invalid ${phase}:${id}: cb.state = ${JSON.stringify(cb.state)}`)
1123 1048 }
... ... @@ -1125,7 +1050,6 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) {
1125 1050 const wr = await agent(writeDocs08CheckboxPromptM(fe, id, phase), {label:`cb:${phase}:${id}`, phase: grp, schema: ACTION_RESULT_SCHEMA})
1126 1051 if (!wr.success) throw new Error(`HALT docs08-checkbox-write ${phase}:${id}: ${wr.error || ''}`)
1127 1052 }
1128   - // cb.state === 'checked' → 静默跳过(resume 幂等)
1129 1053 return { id, phase, approved:true, rounds:round }
1130 1054 }
1131 1055 // request-changes 必须带 must-fix 清单(结构化对象数组);否则 fix 步无法定位 → 直接 halt 暴露 reviewer 契约违例。
... ... @@ -1164,12 +1088,7 @@ async function testGate(module, phase) {
1164 1088 phase('Router')
1165 1089 const routed = await agent(routerPrompt(ROOT), {label:'router', phase:'Router', schema: ROUTER_SCHEMA})
1166 1090  
1167   -// Router 语义运行时断言:后端模块 feItems 必空、frontend-phase 聚合模块 reqs 必空。
1168   -// schema 用 additionalProperties:false 但不强制互斥;这里把契约违例在最早处暴露而不是让错配 phase 静默跑下去。
1169   -//
1170   -// 同时硬约束所有 id 形状为 /^[A-Za-z0-9_-]+$/:下游 micro step prompt 大量把 id / branch / phaseId
1171   -// 模板进 `git ... ${id}` shell 命令字符串,未单引号包裹也未做字符校验。LLM 返回畸形 id(含 ;、`、
1172   -// $()、空格等)会改变子代理执行的命令;这里在 Router 出口一次性把关,让 fail-fast 比 shell 注入早。
  1091 +// Router 语义断言(feItems/reqs 互斥)+ id 形状硬约束(防 shell 注入:id 直接拼入 `git ... ${id}`)。
1173 1092 const ID_PATTERN = /^[A-Za-z0-9_-]+$/
1174 1093 function assertSafeId(kind, value) {
1175 1094 if (typeof value !== 'string' || !ID_PATTERN.test(value)) {
... ... @@ -1196,8 +1115,6 @@ const results = []
1196 1115 let haltedAtIdx = -1
1197 1116 for (const [idx, module] of todo.entries()) {
1198 1117 try {
1199   - // C1:进入模块前建/切功能分支(milestone 的 merge 源)。runBranchSetup 把"探测默认分支 /
1200   - // 校验工作树 / 切或新建分支 / 校验 HEAD"分解为 4-5 个微 agent,分支判定全在 JS 里。
1201 1118 phase('Milestone')
1202 1119 await runBranchSetup(module)
1203 1120 if (module.reqs.length) { // 后端段(frontend-phase 模块 reqs 为空 → 跳过)
... ... @@ -1217,8 +1134,6 @@ for (const [idx, module] of todo.entries()) {
1217 1134 phase('Milestone')
1218 1135 const rep = await agent(reportPrompt(module), {label:`report:${module.id}`, phase:'Milestone', schema: STAGE_RESULT_SCHEMA})
1219 1136 if (rep.status === 'halt') throw new Error(`HALT report ${module.id}: ${rep.reason || ''}`)
1220   - // runMilestone:原 6 步散文(worktree / 默认分支 / merge / docs/08 / tag / report)由 JS 编排,
1221   - // 每个"已是目标态则跳过"的条件由 JS 在 read 微 agent 的结构化返回上判定,跨重入幂等。
1222 1137 await runMilestone(module)
1223 1138 results.push({ module: module.id, status:'done' })
1224 1139 } catch (e) {
... ...