diff --git a/workflows/coding.mjs b/workflows/coding.mjs index be14804..6e5128b 100644 --- a/workflows/coding.mjs +++ b/workflows/coding.mjs @@ -1,25 +1,8 @@ // workflows/coding.mjs // // 整个 ERP Coding(B 阶段)= 一个静默、全自动的 Workflow 脚本。 -// -// 设计原则(详见仓库根 README.md 「阶段 B」 节 与 「设计原则」 节): -// - 所有 stage 都是 agent() 子代理,物理上无法 AskUserQuestion → 编码期结构性静默。 -// - 缺值不再问人:派生 stage 把具体阻塞点写进产物并 throw(fail-fast,合法 halt → 终止态,非对话框)。 -// - 后端 / 前端功能循环由同一份 featureLoop(items, phase) 驱动;phase 切换 reviewer checklist、 -// 测试命令、路径作用域(backend/ vs frontend/)、id 格式(REQ-XXX-NNN vs FE-NN)。 -// - **featureLoop 采用顺序 for-await**(不是 pipeline)。两条理由: -// (1) tdd/fix stage 会在共享工作树 + 同一功能分支上 git commit / 编辑源码;并发会争 .git/index.lock -// 并撞 migration 版本号; -// (2) pipeline 的语义是"stage 抛异常 → 该 item 掉 null、pipeline 永不 reject",会把 -// reviewWithFixLoop / verify / tdd 的 HALT throw 静默吞掉,使 fail-fast 在功能链层级失效, -// 残缺模块照样会被 testGate/report/milestone 推进。顺序 for-await 让 throw 自然冒泡到 -// 模块主循环的 try/catch,被捕获后整阶段 fail-fast break。 -// - 状态账本 = docs/08 §二/§三 + git tag;halt 后重跑 coding-start,router 从账本+tag 重算进度。 -// - reviewer 统一为 agents/code-reviewer.md,review stage 用 agentType:'code-reviewer'。 -// -// 运行时约束:Workflow 运行时禁用非确定性内建(取当天日期 / 随机数的 API)。本脚本不调用它们; -// 凡需要"当天日期"的产物路径(-.md),一律由子代理在其自身上下文中解析并落盘, -// 脚本只负责编排,不计算日期 / 随机数。 +// 设计原则见仓库根 README.md「阶段 B」与「设计原则」节;featureLoop 顺序 for-await 的取舍 +// 详见 featureLoop 函数处的注释。运行时禁用日期 / 随机数 builtin,所有"今天"由子代理解析。 export const meta = { name: 'erp-coding', @@ -38,12 +21,7 @@ const ROUTER_SCHEMA = { type:'object', additionalProperties:false, reqs:{type:'array',items:{type:'string'}}, feItems:{type:'array',items:{type:'string'}} } } } } } -// REVIEW_SCHEMA:reviewer 返回的裁决。issues 改为结构化对象,避免"模糊一行 must-fix" -// 让 fix stage 无从下手就空转 5 轮(见 reviewWithFixLoop 的 must-fix 闸门)。 -// - summary:人类可读的问题摘要(一句)。 -// - locator:必须能让 fix stage 定位到文件(含 `` 或 `:`), -// 否则在 reviewWithFixLoop 里直接判违约 HALT。 -// - severity:blocker/high/medium/low,方便后续把 low/medium 降级为 suggestion 而不卡循环。 +// REVIEW_SCHEMA:reviewer 裁决;issues 结构化对象(summary/locator/severity)驱动 fix。 const REVIEW_SCHEMA = { type:'object', additionalProperties:false, required:['verdict','round','issues'], properties:{ verdict:{type:'string',enum:['approve','request-changes']}, @@ -56,13 +34,7 @@ const REVIEW_SCHEMA = { type:'object', additionalProperties:false, locator:{type:'string'}, severity:{type:'string', enum:['blocker','high','medium','low']} } } } } } -// STAGE_RESULT_SCHEMA:派生 stage(spec/plan/tdd/verify/fix/report)的统一结构化返回。 -// - status=ok:本步骤产出可用,artifactPath 必填(spec/plan/verify/report 的落盘文件), -// summary 可放给下游用作 prompt 上下文。tdd/fix 没有单一 artifact,artifactPath 可省。 -// - status=halt:sub-agent 已经决定无法继续(缺值 / 越界 / 重试到顶),把阻塞点写进 reason, -// JS 端读到立即 throw `HALT …`,让 fail-fast 顺序 for-await 冒泡到模块主循环 try。 -// - 无 schema 时,sub-agent 可以"写一段散文说我跑不下去了,但仍然算成功返回"——这是真正的 -// fail-fast 漏洞;加 schema 后所有派生 stage 都有显式 halt 通道。 +// STAGE_RESULT_SCHEMA:派生 stage 统一返回,status=halt 时 JS 立即 throw HALT。 const STAGE_RESULT_SCHEMA = { type:'object', additionalProperties:false, required:['status'], properties:{ status:{type:'string', enum:['ok','halt']}, @@ -75,11 +47,6 @@ const GATE_SCHEMA = { type:'object', additionalProperties:false, failures:{type:'array',items:{type:'string'}} } } // ── 微步骤 schemas(runBranchSetup / runMilestone / runCrossModule 用)───────── -// 这三个阶段是纯机械的 git/文件操作 + 条件跳过;与其让子代理读"1. 2. 3. 若 X 则跳过"的散文 -// 流程,不如把"observe → JS branch → execute"切成多个 agent 微步骤,每步带强 schema 返回。 -// 这样:(a) 跳过/分支条件由 JS 判定(不再依赖 LLM 读散文条件),idempotency 一致; -// (b) 每步语义单一、prompt 短,schema 校验阻断畸形返回; -// (c) action 步统一返回 ACTION_RESULT_SCHEMA(success/error),失败可由 JS 抛错 halt。 const WT_SCHEMA = { type:'object', additionalProperties:false, required:['clean'], properties:{ clean:{type:'boolean'}, @@ -91,30 +58,20 @@ const DEFAULT_BRANCH_SCHEMA = { type:'object', additionalProperties:false, const EXISTS_SCHEMA = { type:'object', additionalProperties:false, required:['exists'], properties:{ exists:{type:'boolean'} } } -const CURRENT_BRANCH_SCHEMA = { type:'object', additionalProperties:false, - required:['branch'], properties:{ branch:{type:'string'} } } - const FIELD_VALUE_SCHEMA = { type:'object', additionalProperties:false, required:['found','value'], properties:{ found:{type:'boolean'}, value:{type:'string'}, lineNumber:{type:'integer'} } } -// CHECKBOX_STATE_SCHEMA:docs/08 中 `- [ ] REQ-XXX-NNN ...` / `- [x] FE-NN ...` 这类功能行 -// 的勾选态。把"审阅 approve 后 flip checkbox"从 reviewPrompt 的隐式 side-effect 改为可观测 -// 的 read-then-write micro step(参见 reviewWithFixLoop 的 approve 分支)。 -// 注意:state 必填——schema 只 require found 时,sub-agent 返回 {found:true} 而漏掉 state 仍合法, -// 上层 if (cb.state === 'unchecked') 会静默落入"已 checked"分支,docs/08 与 review 裁决悄悄背离。 +// CHECKBOX_STATE_SCHEMA:docs/08 功能行勾选态;state 必填——只 require found 时 cb.state 缺失会静默走 checked 分支。 const CHECKBOX_STATE_SCHEMA = { type:'object', additionalProperties:false, required:['found','state'], properties:{ found:{type:'boolean'}, state:{type:'string', enum:['checked','unchecked']}, lineNumber:{type:'integer'} } } -// TAG_REPORT_FRESHNESS_SCHEMA:当 milestone tag 已存在(resume 或前一轮残留)时,校验 -// tag 指向的 commit 是否包含已落地的 § ⑫ 值。旧版 bug:tag → § ⑫ commit 的错序会让 tag -// 指向 \`{{milestone_tag}}\` 占位符 commit;新顺序(report → tag)下不会再产生,但保留 -// freshness 自检以发现历史残留。 +// TAG_REPORT_FRESHNESS_SCHEMA:保留以发现旧版 tag → § ⑫ 错序 bug 残留(tag 指向占位符 commit)。 const TAG_REPORT_FRESHNESS_SCHEMA = { type:'object', additionalProperties:false, required:['fresh'], properties:{ fresh:{type:'boolean'}, @@ -150,20 +107,12 @@ const ACTION_RESULT_SCHEMA = { type:'object', additionalProperties:false, detail:{type:'string'} } } const ROOT = args?.projectRoot || '.' -// 子代理在 ${ROOT} 路径上跑 git -C / Read / Edit。相对路径 '.' 会绑定到子代理隐式 cwd,无保证。 -// 必须由 coding-start 显式传绝对路径;否则 fail-fast 让人工修复入口而不是在错路径上静默打 tag。 +// ROOT 必须是绝对路径——相对 '.' 会绑定到子代理隐式 cwd,无保证。 if (ROOT === '.' || !(/^(?:\/|[A-Za-z]:[\\/])/.test(ROOT))) { throw new Error(`HALT invalid-projectRoot: must be absolute, got ${JSON.stringify(ROOT)}. coding-start 必须把绝对路径传入 args.projectRoot。`) } -// ============================================================================ -// Stage prompt builders(纯字符串构造;只用 ROOT / id / phase / 入参,不触非确定性内建) -// -// 每个 prompt 的共同契约(见 featureStageContract): -// - 子代理是非交互的,物理上无法弹窗;缺任何值都不要"问人"——把具体阻塞点写进产物并失败。 -// - phase=backend 与 phase=frontend 的差异(路径作用域 / id 形态 / 测试命令来源)逐条写明。 -// - 所有输出文档用中文。 -// ============================================================================ +// ── Feature-loop stage prompts(共享非交互契约见 featureStageContract)── function isFrontend(phase) { return phase === 'frontend' } @@ -190,7 +139,9 @@ function featureStageContract(phase) { return [ '## 硬约束(非交互子代理)', '- 你是 Workflow 派生的**非交互子代理**,物理上无法弹出 AskUserQuestion / 等待人类输入。**绝不要尝试问人**。', - '- 需要具体值时,按此顺序自行获取:(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。', + '- 缺值查找顺序:`.env.local` → `docs/07-环境配置.md` → `docs/04-技术规范.md` → `docs/05-API接口契约.md` → `docs/06-UI交互规范.md` → `CLAUDE.md` → 现有代码。', + '- 仍查不到 → **不要编造、不要留 `【人工填写:】` / `TBD` / `TODO` 占位**;把具体阻塞点(缺哪个值、应在哪个 Plan 闸门锁定、为何无法继续)写进产物。', + '- 然后让本步骤以非零结果 / 显式 throw 结束,由上层 Workflow 转为带诊断的 halt(fail-fast)。', '- 全部输出文档**使用中文**。', `- **阶段 = ${fe ? '前端(frontend)' : '后端(backend)'}**。路径作用域:${fe ? '实现文件必须落在 `frontend/`(或 `docs/09-项目目录结构.md § 前端目录结构` 声明的前端根)下;命中 `backend/` / `sql/` / `scripts/` 即越界,硬停。' @@ -199,6 +150,17 @@ function featureStageContract(phase) { ].join('\n') } +// commitBlock:spec/plan/verify/review 共用的"写完 → add → commit → 失败 halt"四行块。 +function commitBlock(addPath, msg, tail = '- commit 失败 → halt,把 stderr 摘要写进 reason。') { + return [ + '## commit', + `- 写完后必须 commit(milestone 的 worktree-clean 前置依赖此 commit):`, + ` 1. \`git -C ${ROOT} add ${addPath}\``, + ` 2. \`git -C ${ROOT} commit -m "${msg}"\``, + tail, + ].join('\n') +} + // Router:读 docs/08 §二/§三 + git tag,重算进度,返回 ROUTER_SCHEMA。 function routerPrompt(root) { return [ @@ -264,11 +226,7 @@ function deriveSpecPrompt(id, phase) { ? '- 规格至少含:关联 REQ + 关联原型;组件树(按页面 / 区域分块,推导自 prototype DOM);页面状态机(loading / empty / error / 正常 / 表单提交中 至少 5 态);消费的后端端点(对齐 docs/05);业务规则前端复刻清单(逐条:规则 / 触发时机 / 报错文案 / 来源 REQ);Design Tokens 引用清单(`var(--color-*)`)。' : '- 规格覆盖:goal / 输入输出 / 业务规则 / 约束 / schema / API 引用 / acceptance criteria。', '', - '## commit', - `- 写完 spec 后必须 commit(milestone 的 worktree-clean 前置依赖此 commit):`, - ` 1. \`git -C ${ROOT} add \``, - ` 2. \`git -C ${ROOT} commit -m "docs(spec:${id}): 派生规格"\``, - '- commit 失败 → halt,把 stderr 摘要写进 reason。', + commitBlock('', `docs(spec:${id}): 派生规格`), '', '## 自审(inline 修,无须等待)', `- 占位符扫描:\`TBD\` / \`TODO\` / \`【人工填写:】\`${fe ? ' / `controller` / `service` / `SQL` / `migration`(前端 spec 不应出现后端字样)' : ''} → 命中即修;修不掉的缺值按硬约束失败。`, @@ -315,11 +273,7 @@ function planPrompt(id, phase, specPath) { `- 落盘路径:\`docs/superpowers/plans/<同 spec 的 YYYY-MM-DD>-${id}.md\`,文件头含 Goal / Architecture / Tech Stack + checkbox 任务。`, '- 自审:占位符扫描(按硬约束清单);spec coverage(spec 每节至少指向一个 task,补 gap);类型一致性(签名 / 方法名 / 错误码 / props 一致)。', '', - '## commit', - `- 写完 plan 后必须 commit(milestone 的 worktree-clean 前置依赖此 commit):`, - ` 1. \`git -C ${ROOT} add \``, - ` 2. \`git -C ${ROOT} commit -m "docs(plan:${id}): 任务级 TDD 计划"\``, - '- commit 失败 → halt,把 stderr 摘要写进 reason。', + commitBlock('', `docs(plan:${id}): 任务级 TDD 计划`), '', '## 输出(必须符合下发的 STAGE_RESULT JSON schema)', '- 成功:`{ "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) { ].join('\n'), `- 证据落盘路径固定为 \`docs/superpowers/reviews/<同 spec 的 YYYY-MM-DD>-${id}-${suffix}.md\`(与 review 报告同目录;round=0 → \`-verify.md\`;round>=1 → \`-verify-r.md\`,**每轮独立文件不覆盖前轮**)。同时把核心结构化结果摘要打印到会话便于上层 review stage 引用,**不要**自行另起目录或自由命名文件。`, '', - '## commit', - `- 写完证据后必须 commit(milestone 的 worktree-clean 前置依赖此 commit):`, - ` 1. \`git -C ${ROOT} add <证据 artifactPath>\``, - ` 2. \`git -C ${ROOT} commit -m "docs(verify:${id}${round > 0 ? `:r${round}` : ''}): 证据验证"\``, - '- commit 失败 → halt,把 stderr 摘要写进 reason(仍要返回已写入的证据路径)。', + commitBlock('<证据 artifactPath>', `docs(verify:${id}${round > 0 ? `:r${round}` : ''}): 证据验证`, + '- commit 失败 → halt,把 stderr 摘要写进 reason(仍要返回已写入的证据路径)。'), '', '## 输出(必须符合下发的 STAGE_RESULT JSON schema)', `- 全部通过:\`{ "status": "ok", "artifactPath": "docs/superpowers/reviews/YYYY-MM-DD-${id}-${suffix}.md", "summary": "" }\`。`, @@ -439,11 +390,9 @@ function reviewPrompt(id, phase, round, lastVerifySummary, specPath) { `- **不要**在本步骤里编辑 docs/08 的 \`- [ ]\` checkbox——该 side effect 由上层 Workflow 的 micro step 在 approve 后另行落盘(你只负责裁决)。`, '- 不要返回额外字段(schema 是 `additionalProperties:false`)。', '', - '## commit', - `- 写完审阅报告后必须 commit(milestone 的 worktree-clean 前置依赖此 commit;该 commit 与 verdict 无关,approve 或 request-changes 都要 commit 报告本身):`, - ` 1. \`git -C ${ROOT} add docs/superpowers/reviews/<同 spec 的 YYYY-MM-DD>-${id}.md\``, - ` 2. \`git -C ${ROOT} commit -m "docs(review:${id}:r${round}): "\``, - '- commit 失败时仍按 schema 返回 verdict / issues;commit 错误信息打印到日志即可(不要在 schema 中夹带额外字段)。', + commitBlock(`docs/superpowers/reviews/<同 spec 的 YYYY-MM-DD>-${id}.md`, + `docs(review:${id}:r${round}): `, + '- commit 失败时仍按 schema 返回 verdict / issues;commit 错误信息打印到日志即可(不要在 schema 中夹带额外字段)。'), ].filter(Boolean).join('\n') } @@ -465,9 +414,6 @@ function fixPrompt(id, phase, issues) { '## 流程', '- 逐项编辑 locator 指向的代码文件(遵守阶段路径作用域护栏)。', `- 编辑前必须先校验 locator 文件存在:跑 \`git -C ${ROOT} cat-file -e HEAD:\`(locator 形如 \`path:line\` 时取 \`path\`)。文件不存在 → halt,把 locator 写进 reason,不要"修一个不存在的文件"。`, - fe - ? '- **硬护栏(与 tdd 同款)**:命中 `backend/` / `sql/` / `scripts/` → halt 并把 file 路径写进 reason。' - : '- **硬护栏(与 tdd 同款)**:任一被编辑文件以 `frontend/` 开头 → halt 并把 file 路径写进 reason。', `- 修复后 commit:\`fix(): 修复 review must-fix ${fe ? `FE: ${id}` : `REQ: ${id}`}\`(不混合无关改动)。`, '- 修复完成后本步骤即结束;上层 Workflow 会重新跑 verify + review(下一轮)。', '', @@ -563,7 +509,7 @@ function currentBranchPromptM() { microStepContract(), '', `跑 \`git -C ${ROOT} rev-parse --abbrev-ref HEAD\`。`, - '## 输出(CURRENT_BRANCH_SCHEMA)', + '## 输出(BRANCH_NAME_SCHEMA)', '- `{ "branch": "" }`', ].join('\n') } @@ -646,25 +592,24 @@ function executeMergePromptM(defaultBranch, branch, phaseId) { } function readDocs08FieldPromptM(fe, id) { - if (fe) { - return [ - '# 读 docs/08 § 三 `整体里程碑:` 字段当前值', - microStepContract(), - '', - `Read \`${ROOT}/docs/08-模块任务管理.md\`,定位 § 三(前端阶段)下的 \`- 整体里程碑: \` 行。`, - '## 输出(FIELD_VALUE_SCHEMA)', - '- 命中:`{ "found": true, "value": "<冒号后去空白的当前值>", "lineNumber": <该行 1-based 行号> }`', - '- § 三 或该行不存在:`{ "found": false, "value": "" }`', - ].join('\n') - } + const section = fe ? '§ 三' : '§ 二' + const title = fe + ? '# 读 docs/08 § 三 `整体里程碑:` 字段当前值' + : `# 读 docs/08 § 二 模块 \`${id}\` 的 \`里程碑:\` 字段当前值` + const locator = fe + ? `Read \`${ROOT}/docs/08-模块任务管理.md\`,定位 ${section}(前端阶段)下的 \`- 整体里程碑: \` 行。` + : `Read \`${ROOT}/docs/08-模块任务管理.md\`,定位 ${section} 中 module id == \`${id}\` 的 bullet 段,取其 \` - 里程碑: \` 子项。` + const missing = fe + ? '- § 三 或该行不存在:`{ "found": false, "value": "" }`' + : `- 模块 \`${id}\` 或该字段不存在:\`{ "found": false, "value": "" }\`` return [ - `# 读 docs/08 § 二 模块 \`${id}\` 的 \`里程碑:\` 字段当前值`, + title, microStepContract(), '', - `Read \`${ROOT}/docs/08-模块任务管理.md\`,定位 § 二 中 module id == \`${id}\` 的 bullet 段,取其 \` - 里程碑: \` 子项。`, + locator, '## 输出(FIELD_VALUE_SCHEMA)', '- 命中:`{ "found": true, "value": "<冒号后去空白的当前值>", "lineNumber": <行号> }`', - `- 模块 \`${id}\` 或该字段不存在:\`{ "found": false, "value": "" }\``, + missing, ].join('\n') } @@ -690,33 +635,22 @@ function writeDocs08FieldPromptM(fe, id, targetValue, phaseId, lineNumber) { ].join('\n') } -// ── 微步骤:docs/08 功能行 checkbox 勾选态(reviewer approve 后的可观测 side effect)── -// 原 reviewPrompt 让 reviewer 顺手 flip checkbox:reviewer 失败时 router/进度判定看不到,且 -// reviewer agent 多了一项 file edit 副作用。拆为 read-then-write 两个 micro step: -// reviewWithFixLoop approve → read → 若 unchecked → write → assert success; -// 已 checked → 静默跳过(resume 幂等)。 +// ── 微步骤:docs/08 功能行 checkbox(reviewer approve 后的可观测 side effect;read-then-write)── function readDocs08CheckboxPromptM(fe, id) { - if (fe) { - return [ - `# 读 docs/08 § 三 功能 \`${id}\` 的勾选态(\`- [ ] ${id} ...\` / \`- [x] ${id} ...\`)`, - microStepContract(), - '', - `Read \`${ROOT}/docs/08-模块任务管理.md\`,定位 § 三(前端阶段)下的 \`功能:\` 项,从中找以 \`- [ ] ${id} \` 或 \`- [x] ${id} \` 开头的行(注意 id 后必须紧跟空格,避免误中前缀同名)。`, - '## 输出(CHECKBOX_STATE_SCHEMA)', - `- 命中 \`- [x] ${id} ...\`:\`{ "found": true, "state": "checked", "lineNumber": <行号> }\``, - `- 命中 \`- [ ] ${id} ...\`:\`{ "found": true, "state": "unchecked", "lineNumber": <行号> }\``, - `- 找不到:\`{ "found": false }\``, - ].join('\n') - } + const section = fe ? '§ 三' : '§ 二' + const kind = fe ? '功能' : 'REQ' + const locator = fe + ? `Read \`${ROOT}/docs/08-模块任务管理.md\`,定位 ${section}(前端阶段)下的 \`功能:\` 项,从中找以 \`- [ ] ${id} \` 或 \`- [x] ${id} \` 开头的行(注意 id 后必须紧跟空格,避免误中前缀同名)。` + : `Read \`${ROOT}/docs/08-模块任务管理.md\`,定位 ${section},找以 \`- [ ] ${id} \` 或 \`- [x] ${id} \` 开头的行(id 后必须紧跟空格)。该行可能位于任一模块 bullet 下。` return [ - `# 读 docs/08 § 二 REQ \`${id}\` 的勾选态(\`- [ ] ${id} ...\` / \`- [x] ${id} ...\`)`, + `# 读 docs/08 ${section} ${kind} \`${id}\` 的勾选态(\`- [ ] ${id} ...\` / \`- [x] ${id} ...\`)`, microStepContract(), '', - `Read \`${ROOT}/docs/08-模块任务管理.md\`,定位 § 二,找以 \`- [ ] ${id} \` 或 \`- [x] ${id} \` 开头的行(id 后必须紧跟空格)。该行可能位于任一模块 bullet 下。`, + locator, '## 输出(CHECKBOX_STATE_SCHEMA)', - `- \`- [x] ${id} ...\` → \`{ "found": true, "state": "checked", "lineNumber": <行号> }\``, - `- \`- [ ] ${id} ...\` → \`{ "found": true, "state": "unchecked", "lineNumber": <行号> }\``, - `- 找不到:\`{ "found": false }\``, + `- 命中 \`- [x] ${id} ...\`:\`{ "found": true, "state": "checked", "lineNumber": <行号> }\``, + `- 命中 \`- [ ] ${id} ...\`:\`{ "found": true, "state": "unchecked", "lineNumber": <行号> }\``, + '- 找不到:`{ "found": false }`', ].join('\n') } @@ -940,7 +874,7 @@ async function runBranchSetup(module) { if (!r.success) throw new Error(`HALT branchSetup-create ${branch}: ${r.error || ''}`) } - const head = await agent(currentBranchPromptM(), {label: lbl('head'), phase: 'Milestone', schema: CURRENT_BRANCH_SCHEMA}) + const head = await agent(currentBranchPromptM(), {label: lbl('head'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) if (head.branch !== branch) throw new Error(`HALT branchSetup-branch-mismatch ${branch}: HEAD on ${head.branch}`) log(`branch-setup: ${id} → ${branch}`) @@ -1093,11 +1027,7 @@ async function featureLoop(items, phase) { } } -// 有界 5 轮修复;超出 → throw(终止态,非对话框)。 -// fix 后再跑 reverify 让 fix-stage 的 commit 有机会被新一轮 verify 看到; -// verify 内部失败 throw 在顺序 for-await 下会冒泡到模块主循环 try。 -// approve 后通过独立 micro step 把 docs/08 对应 checkbox flip 为 `[x]`(拆出 reviewer side-effect, -// 写失败可观测;幂等:已 checked 静默跳过)。 +// 有界 5 轮修复;超出 → throw(终止态,非对话框)。approve 后独立 micro step flip docs/08 checkbox。 async function reviewWithFixLoop(id, phase, verifyResult, specPath) { const grp = phase === 'backend' ? 'Backend' : 'Frontend' const fe = isFrontend(phase) @@ -1105,19 +1035,14 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) { let lastIssuesCount = 0 for (let round = 1; round <= 5; round++) { const lastVerifySummary = (lastVerify && (lastVerify.summary || lastVerify.reason)) || '' - // 命名碰撞警告:opts.phase = grp('Backend'/'Frontend'/'Milestone')是 harness 的 UI 分组, - // 与 code-reviewer.md 文档里的"domain phase = backend|frontend"是**同名不同物**。 - // reviewer agent 从 prompt 正文 `**phase = ...**` 那一行读 domain phase,**不要**读 opts.phase。 - // 见 agents/code-reviewer.md "Domain phase resolution" 段。 + // opts.phase = grp('Backend'/'Frontend')是 harness UI 分组;domain phase 见 agents/code-reviewer.md。 const r = await agent( reviewPrompt(id, phase, round, lastVerifySummary, specPath), {label:`review:${phase}:${id}:r${round}`, phase: grp, schema: REVIEW_SCHEMA, agentType:'code-reviewer'} ) if (r.verdict === 'approve') { - // docs/08 checkbox flip(observable side effect,原 reviewer 隐式 Edit → micro step) const cb = await agent(readDocs08CheckboxPromptM(fe, id), {label:`cb?:${phase}:${id}`, phase: grp, schema: CHECKBOX_STATE_SCHEMA}) if (!cb.found) throw new Error(`HALT docs08-checkbox-missing ${phase}:${id}: docs/08 ${fe?'§ 三':'§ 二'} 中找不到 \`- [ ] ${id} ...\` / \`- [x] ${id} ...\` 行`) - // 防御:即使 schema 已 require state,再做一次 JS 校验,杜绝"found:true 但 state 缺失/枚举外"静默走 checked 分支。 if (cb.state !== 'checked' && cb.state !== 'unchecked') { throw new Error(`HALT docs08-checkbox-state-invalid ${phase}:${id}: cb.state = ${JSON.stringify(cb.state)}`) } @@ -1125,7 +1050,6 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) { const wr = await agent(writeDocs08CheckboxPromptM(fe, id, phase), {label:`cb:${phase}:${id}`, phase: grp, schema: ACTION_RESULT_SCHEMA}) if (!wr.success) throw new Error(`HALT docs08-checkbox-write ${phase}:${id}: ${wr.error || ''}`) } - // cb.state === 'checked' → 静默跳过(resume 幂等) return { id, phase, approved:true, rounds:round } } // request-changes 必须带 must-fix 清单(结构化对象数组);否则 fix 步无法定位 → 直接 halt 暴露 reviewer 契约违例。 @@ -1164,12 +1088,7 @@ async function testGate(module, phase) { phase('Router') const routed = await agent(routerPrompt(ROOT), {label:'router', phase:'Router', schema: ROUTER_SCHEMA}) -// Router 语义运行时断言:后端模块 feItems 必空、frontend-phase 聚合模块 reqs 必空。 -// schema 用 additionalProperties:false 但不强制互斥;这里把契约违例在最早处暴露而不是让错配 phase 静默跑下去。 -// -// 同时硬约束所有 id 形状为 /^[A-Za-z0-9_-]+$/:下游 micro step prompt 大量把 id / branch / phaseId -// 模板进 `git ... ${id}` shell 命令字符串,未单引号包裹也未做字符校验。LLM 返回畸形 id(含 ;、`、 -// $()、空格等)会改变子代理执行的命令;这里在 Router 出口一次性把关,让 fail-fast 比 shell 注入早。 +// Router 语义断言(feItems/reqs 互斥)+ id 形状硬约束(防 shell 注入:id 直接拼入 `git ... ${id}`)。 const ID_PATTERN = /^[A-Za-z0-9_-]+$/ function assertSafeId(kind, value) { if (typeof value !== 'string' || !ID_PATTERN.test(value)) { @@ -1196,8 +1115,6 @@ const results = [] let haltedAtIdx = -1 for (const [idx, module] of todo.entries()) { try { - // C1:进入模块前建/切功能分支(milestone 的 merge 源)。runBranchSetup 把"探测默认分支 / - // 校验工作树 / 切或新建分支 / 校验 HEAD"分解为 4-5 个微 agent,分支判定全在 JS 里。 phase('Milestone') await runBranchSetup(module) if (module.reqs.length) { // 后端段(frontend-phase 模块 reqs 为空 → 跳过) @@ -1217,8 +1134,6 @@ for (const [idx, module] of todo.entries()) { phase('Milestone') const rep = await agent(reportPrompt(module), {label:`report:${module.id}`, phase:'Milestone', schema: STAGE_RESULT_SCHEMA}) if (rep.status === 'halt') throw new Error(`HALT report ${module.id}: ${rep.reason || ''}`) - // runMilestone:原 6 步散文(worktree / 默认分支 / merge / docs/08 / tag / report)由 JS 编排, - // 每个"已是目标态则跳过"的条件由 JS 在 read 微 agent 的结构化返回上判定,跨重入幂等。 await runMilestone(module) results.push({ module: module.id, status:'done' }) } catch (e) {