From 1b944be85d7ebdfefbe5f02d680f888748063b51 Mon Sep 17 00:00:00 2001 From: zichun Date: Mon, 1 Jun 2026 10:55:55 +0800 Subject: [PATCH] coding.mjs: converge halts via adjudicator + in-stage decisions, keep hard safety boundaries --- workflows/coding.mjs | 452 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------------------------------------------------------- 1 file changed, 355 insertions(+), 97 deletions(-) diff --git a/workflows/coding.mjs b/workflows/coding.mjs index 726d7b5..20fd3fd 100644 --- a/workflows/coding.mjs +++ b/workflows/coding.mjs @@ -34,13 +34,32 @@ const REVIEW_SCHEMA = { type:'object', additionalProperties:false, locator:{type:'string'}, severity:{type:'string', enum:['blocker','high','medium','low']} } } } } } -// STAGE_RESULT_SCHEMA:派生 stage 统一返回,status=halt 时 JS 立即 throw HALT。 +// STAGE_RESULT_SCHEMA:派生 stage 统一返回。 +// status=halt 不再立即 fail-fast——先经 adjudicate() 仲裁(retry/continue/halt)才可能终止。 +// decisions[]:stage 自主决策日志(缺值时不再停下,而是挑最有依据的默认/解读并登记于此),上层汇总进结果供人工事后审阅。 const STAGE_RESULT_SCHEMA = { type:'object', additionalProperties:false, required:['status'], properties:{ status:{type:'string', enum:['ok','halt']}, reason:{type:'string'}, artifactPath:{type:'string'}, - summary:{type:'string'} } } + summary:{type:'string'}, + decisions:{ type:'array', items:{ type:'object', additionalProperties:false, + required:['question','choice','rationale'], + properties:{ + question:{type:'string'}, + choice:{type:'string'}, + rationale:{type:'string'}, + confidence:{type:'string', enum:['high','medium','low']} } } } } } + +// ADJUDICATE_SCHEMA:仲裁子代理在确定性 halt 之前的裁决—— +// retry = 失败疑似一次性/可纠正,携 guidance 重跑上游; +// continue = 缺陷不阻断正确性、可安全前进(降级为口头建议); +// halt = 确属不可恢复(结构性缺失无旁证 / git 树需人工 / 会污染源码或伪造业务语义)。 +const ADJUDICATE_SCHEMA = { type:'object', additionalProperties:false, + required:['action','rationale'], properties:{ + action:{type:'string', enum:['retry','continue','halt']}, + guidance:{type:'string'}, + rationale:{type:'string'} } } const GATE_SCHEMA = { type:'object', additionalProperties:false, required:['status'], properties:{ status:{type:'string',enum:['green','red']}, @@ -134,8 +153,10 @@ function featureStageContract(phase) { '## 硬约束(非交互子代理)', '- 你是 Workflow 派生的**非交互子代理**,物理上无法弹出 AskUserQuestion / 等待人类输入。**绝不要尝试问人**。', '- 缺值查找顺序:`config-vars.yaml` → `docs/04-技术规范.md` → `docs/05-API接口契约.md` → `prototype/`(前端布局/交互权威)→ `src/styles/tokens.css`(前端色值)→ `CLAUDE.md` → 现有代码。', - '- 仍查不到 → **不要编造、不要留 `【人工填写:】` / `TBD` / `TODO` 占位**;把具体阻塞点(缺哪个值、应在哪个 Plan 闸门锁定、为何无法继续)写进产物。', - '- 然后让本步骤以非零结果 / 显式 throw 结束,由上层 Workflow 转为带诊断的 halt(fail-fast)。', + '- 仍查不到时——**优先自主决策继续,不要停下**:基于现有代码约定 / 技术规范 / 同类实现,挑选**最有依据的解读或合理默认值**,把该决策写进产物显著位置,并在返回的 `decisions[]` 中逐条登记 `{question, choice, rationale, confidence}`(这是默认动作,项目目标是全自动静默、尽可能少 halt)。', + '- 红线:**绝不**留 `【人工填写:】` / `TBD` / `TODO` 占位;**绝不**编造与现有约定/技术规范冲突的"事实";自主默认必须可被现有证据支撑且记入 `decisions[]`。', + '- 仅当缺失的是**无法自洽决策的硬事实**(如某表结构 / 业务主键语义完全缺失且无任何旁证,任何默认都可能污染源码或伪造业务语义)时,才以 `status:halt` 结束并把阻塞点写清;上层会再经仲裁评估能否继续,halt 是最后手段而非首选。', + '- 输出纪律:本次若做过任何自主默认 / 解读,成功返回(status:ok)**必须**带 `decisions[]`(逐条 `{question,choice,rationale,confidence}`,与上面登记要求一致);完全没有自主决策时才可省略——别照抄"输出"段里不含 decisions 的最简示例而漏登记。', '- 全部输出文档**使用中文**。', `- **阶段 = ${fe ? '前端(frontend)' : '后端(backend)'}**。路径作用域:${fe ? '实现文件必须落在 `frontend/` 下;命中 `backend/` / `sql/` / `scripts/` 即越界,硬停。' @@ -463,6 +484,126 @@ function microStepContract() { ].join('\n') } +// ============================================================================ +// 仲裁 / 自主决策基础设施(halt 收敛) +// 设计:原先每个"缺值 / 结构违约 / 重试耗尽"点都直接 throw HALT 让整阶段 fail-fast。 +// 现在改为先经 adjudicate() 仲裁——retry(带 guidance 重跑)/ continue(降级前进)/ halt(确属不可恢复)。 +// stage 自身也被要求优先自主决策继续(见 featureStageContract),其默认/解读记入 decisions[] 汇总。 +// 仅 git 树冲突 / 配置错 / 安全护栏(assertSafeId)保持硬 halt——这些不可由 LLM 安全代决。 +// ============================================================================ + +const ADJUDICATE_MAX = 3 // 单个 site 的仲裁轮上限;超出则确定性 halt(防无限循环) +const adjGuidance = (g) => g ? `\n\n## 仲裁返回的纠正指令(本次重跑必须遵守)\n${g}` : '' + +// 全流程自主决策日志:stage 缺值时不停而是挑默认/解读,登记在此,随结果回传供人工事后审阅。 +const autonomousDecisions = [] +function recordDecisions(site, decisions) { + if (!Array.isArray(decisions)) return + for (const d of decisions) { + if (!d) continue + autonomousDecisions.push({ site, question:d.question, choice:d.choice, rationale:d.rationale, confidence:d.confidence }) + log(`decision ${site}: ${d.question || '?'} → ${d.choice || '?'} (${d.confidence || '?'})`) + } +} + +function adjudicatePromptM(site, context) { + const ctx = typeof context === 'string' ? context : JSON.stringify(context, null, 2) + return [ + `# 仲裁:\`${site}\` 触发潜在 halt,请裁决 retry / continue / halt`, + microStepContract(), + '', + '## 你的角色', + '你是 ERP 编码 Workflow 的**仲裁子代理**。某上游步骤触发了一个原本会让整阶段 fail-fast 停下的护栏。', + '项目目标是全自动静默、尽可能少停。请在**不损坏 git 工作树、不伪造业务事实、不污染源码**的前提下,尽量让流程继续。', + '', + '## 触发上下文', + '```', + ctx, + '```', + '', + '## 裁决口径', + '- `retry`:失败疑似一次性 / 可纠正(子代理输出不符 schema 约定、git 命令瞬时失败、上游漏给某字段)。**必须**在 `guidance` 写清"重跑时要修正什么",下游会把它原样注入重跑提示。', + '- `continue`:缺陷不阻断正确性、可安全前进(reviewer 的非必须建议 / 可降级为口头建议的 issue / 纯可视化副作用缺失 / 已可由后续 verify / test-gate 兜底的疑虑)。在 `rationale` 说明为何安全。', + '- `halt`:确属不可恢复——结构性缺失且无任何旁证、git 树冲突需人工、继续会污染源码 / 伪造业务语义。在 `rationale` 写清人工需要做什么。', + '- 若上下文含 `"allowContinue": false`,**不得**选 continue(如红色测试不可跳过),只在 retry / halt 间选。', + '## 输出(ADJUDICATE_SCHEMA)', + '- `{ "action": "retry|continue|halt", "guidance": "", "rationale": "<裁决理由>" }`', + ].join('\n') +} + +async function adjudicate(site, context, grp, round) { + const verdict = await agent(adjudicatePromptM(site, context), + {label:`adjudicate:${site}:r${round}`, phase: grp, schema: ADJUDICATE_SCHEMA}) + log(`adjudicate ${site} r${round}: ${verdict.action}${verdict.rationale ? ' — ' + verdict.rationale : ''}`) + return verdict +} + +// runStage:跑一个 STAGE_RESULT 派生 stage(spec/plan/tdd/verify/fix/report)。 +// ① 登记 decisions[];② status:halt 或 validate() 报结构问题 → 经 adjudicate 决定 retry/continue/halt。 +// makePrompt(guidanceTail) 接收仲裁追加指令串(adjGuidance 已格式化);validate(res) 返回 null=通过 / 问题串。 +// allowContinue=false:本 stage 的 halt 代表**硬正确性边界**(功能测试红色 verify/reverify、路径越界/卡死 tdd、 +// test-gate 红 report),仲裁只许 retry/halt,**绝不 continue 放行**残缺/越界状态去 approve / milestone。 +async function runStage(makePrompt, { site, grp, label, validate, allowContinue = true }) { + let guidance = '' + for (let round = 1; round <= ADJUDICATE_MAX; round++) { + const res = await agent(makePrompt(adjGuidance(guidance)), {label, phase: grp, schema: STAGE_RESULT_SCHEMA}) + recordDecisions(site, res.decisions) + let problem = null + if (res.status === 'halt') problem = `stage 返回 status:halt;reason: ${res.reason || '(空)'}` + else if (validate) { try { problem = validate(res) } catch (e) { problem = String(e?.message || e) } } + if (!problem) return res + const verdict = await adjudicate(site, { problem, stageResult: res, allowContinue }, grp, round) + if (verdict.action === 'continue' && allowContinue) return res + if (verdict.action !== 'retry') throw new Error(`HALT ${site}: ${verdict.rationale || problem}`) + guidance = verdict.guidance || '' // retry:带 guidance 重跑 + } + throw new Error(`HALT ${site}-adjudication-exhausted: ${ADJUDICATE_MAX} 轮仲裁仍未解决`) +} + +// runAction:跑一个 ACTION_RESULT 微步骤(git / 文件写),失败时经 adjudicate 决定 retry/continue/halt。 +// allowContinue=true 时 continue 视为"接受失败并前进"(仅用于纯可视化等可安全跳过的副作用)。 +async function runAction(makePrompt, { site, grp, label, allowContinue = false }) { + let guidance = '' + for (let round = 1; round <= ADJUDICATE_MAX; round++) { + const r = await agent(makePrompt(adjGuidance(guidance)), {label, phase: grp, schema: ACTION_RESULT_SCHEMA}) + if (r.success) return r + const verdict = await adjudicate(site, + { problem:`action 失败:${r.error || ''}${r.detail ? '\n' + r.detail : ''}`, allowContinue }, grp, round) + if (verdict.action === 'continue' && allowContinue) return r + if (verdict.action === 'halt' || verdict.action === 'continue') + throw new Error(`HALT ${site}: ${verdict.rationale || r.error || ''}`) + guidance = verdict.guidance || '' // retry:带 guidance 重跑 + } + throw new Error(`HALT ${site}-adjudication-exhausted: ${ADJUDICATE_MAX} 轮仲裁仍未解决`) +} + +// recoverDirtyWorktreePromptM:branchSetup / milestone 前置的"工作树干净"被打破时的自主恢复(class D 部分)。 +// 子代理检查脏文件——全是本阶段合法产物 → 自动 commit 后继续;含越界/不明改动 → 不提交、返回失败让上层 halt。 +// **分支护栏(branch)**:自动 commit 只允许发生在目标功能分支上。若当前 HEAD 不在 branch(如里程碑后 HEAD +// 停在默认分支、resume 时残留落在默认分支),绝不 add -A/commit——否则会把绕过 review/test-gate 的改动 +// 直接提交进默认分支,且该改动对模块 `...HEAD` 三点 diff 不可见(污染 cross-module / 完成报告)。 +function recoverDirtyWorktreePromptM(dirty, branch, scopeHint) { + const list = (dirty || []).map(p => `- ${p}`).join('\n') || '(调用方未给清单,请自行 `git status --porcelain` 复核)' + return [ + '# 工作树不干净——判定能否自主提交后继续', + microStepContract(), + '', + '## 背景', + `分支切换 / 里程碑前要求工作树干净,当前存在未提交改动。${scopeHint || ''}`, + '在**不丢失工作、不混入越界改动、不提交到错误分支**的前提下尽量让流程继续。', + '', + '## 脏文件清单', + list, + '', + '## 流程', + `0. **分支护栏(必须先做)**:跑 \`git -C ${ROOT} rev-parse --abbrev-ref HEAD\`。若当前分支 **!= \`${branch}\`**(目标功能分支),**绝不提交**——直接返回 \`{ "success": false, "error": "dirty-on-wrong-branch", "detail": "HEAD=<当前分支>, expected ${branch};拒绝把残留提交到非功能分支,留给人工" }\`。只有当前已在 \`${branch}\` 才继续 step 1。`, + `1. 逐一检查改动(\`git -C ${ROOT} status --porcelain\`,必要时 \`git -C ${ROOT} diff\`)。`, + `2. **全部都是本阶段合法产物**(spec/plan/verify/review/report/源码/migration,且落在当前阶段路径作用域内)→ \`git -C ${ROOT} add -A\` 后 \`git -C ${ROOT} commit -m "chore: 自动提交上一步残留改动"\`,返回 \`{ "success": true, "detail": "committed-in-scope" }\`。`, + '3. 含**越界 / 不明 / 与本阶段无关**的改动(手工临时文件、其它模块代码、构建产物等)→ **不要提交**,返回 `{ "success": false, "error": "dirty-out-of-scope", "detail": "<可疑文件 + 原因>" }`。', + '## 输出(ACTION_RESULT_SCHEMA)', + ].join('\n') +} + // ── 微步骤:可重用 read(多个 orchestrator 共用)── function detectDefaultBranchPromptM() { return [ @@ -845,21 +986,33 @@ async function runBranchSetup(module) { const def = await agent(detectDefaultBranchPromptM(), {label: lbl('default'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) + // 工作树脏:先自主恢复(in-scope 残留 → 自动 commit);含越界改动则恢复失败 → halt(留给人工)。 const wt = await agent(worktreeCleanPromptM(), {label: lbl('wt'), phase: 'Milestone', schema: WT_SCHEMA}) - if (!wt.clean) throw new Error(`HALT branchSetup-dirty-worktree ${branch}: ${(wt.dirty || []).join(', ')}`) + if (!wt.clean) { + const rec = await agent(recoverDirtyWorktreePromptM(wt.dirty, branch, `分支 setup 前置(目标分支 ${branch})。`), + {label: lbl('wt-recover'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA}) + if (!rec.success) throw new Error(`HALT branchSetup-dirty-worktree ${branch}: ${rec.error || ''}${rec.detail ? '\n' + rec.detail : ''}`) + log(`branch-setup: ${id} 自动提交脏工作树残留(${rec.detail || ''})`) + } const exists = await agent(checkBranchExistsPromptM(branch), {label: lbl('exists?'), phase: 'Milestone', schema: EXISTS_SCHEMA}) - if (exists.exists) { - const r = await agent(checkoutExistingBranchPromptM(branch), {label: lbl('checkout'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA}) - if (!r.success) throw new Error(`HALT branchSetup-checkout ${branch}: ${r.error || ''}`) + await runAction(g => checkoutExistingBranchPromptM(branch) + g, {site:`branchSetup-checkout:${branch}`, grp:'Milestone', label: lbl('checkout')}) } else { - const r = await agent(createBranchFromPromptM(def.branch, branch), {label: lbl('create'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA}) - if (!r.success) throw new Error(`HALT branchSetup-create ${branch}: ${r.error || ''}`) + await runAction(g => createBranchFromPromptM(def.branch, branch) + g, {site:`branchSetup-create:${branch}`, grp:'Milestone', label: lbl('create')}) } - 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}`) + // HEAD 确认:不符则经仲裁重切(retry)或留人工(halt)。 + let head = await agent(currentBranchPromptM(), {label: lbl('head'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) + for (let adj = 1; head.branch !== branch && adj <= ADJUDICATE_MAX; adj++) { + const verdict = await adjudicate(`branchSetup-branch-mismatch:${branch}`, + { problem:`分支 setup 后 HEAD 在 ${head.branch},期望 ${branch}` }, 'Milestone', adj) + if (verdict.action !== 'retry') + throw new Error(`HALT branchSetup-branch-mismatch ${branch}: ${verdict.rationale || `HEAD on ${head.branch}`}`) + await runAction(g => checkoutExistingBranchPromptM(branch) + g, {site:`branchSetup-recheckout:${branch}`, grp:'Milestone', label: lbl('recheckout')}) + 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}: ${ADJUDICATE_MAX} 轮后 HEAD 仍在 ${head.branch}`) log(`branch-setup: ${id} → ${branch}`) } @@ -874,14 +1027,20 @@ async function runMilestone(module) { const targetTag = `milestone/${phaseId}` const lbl = (k) => `milestone:${k}:${phaseId}` - // step 1: worktree clean precondition + // step 1: worktree clean precondition(脏树先自主恢复 in-scope 残留;含越界改动则 halt 留人工) const wt = await agent(worktreeCleanPromptM(), {label: lbl('wt'), phase: 'Milestone', schema: WT_SCHEMA}) - if (!wt.clean) throw new Error(`HALT milestone-dirty-worktree ${phaseId}: ${(wt.dirty || []).join(', ')}`) + if (!wt.clean) { + const rec = await agent(recoverDirtyWorktreePromptM(wt.dirty, branch, `里程碑前置(阶段 ${phaseId},应在功能分支 ${branch})。`), + {label: lbl('wt-recover'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA}) + if (!rec.success) throw new Error(`HALT milestone-dirty-worktree ${phaseId}: ${rec.error || ''}${rec.detail ? '\n' + rec.detail : ''}`) + log(`milestone: ${phaseId} 自动提交脏工作树残留(${rec.detail || ''})`) + } // step 2: detect default branch const def = await agent(detectDefaultBranchPromptM(), {label: lbl('default'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) // step 3: merge (idempotent — skip if already an ancestor) + // merge 冲突保持**硬 halt**:自动 abort/stash/改文件均不安全,把树留给人工(设计原则不变)。 const merged = await agent(checkAlreadyMergedPromptM(branch, def.branch), {label: lbl('merged?'), phase: 'Milestone', schema: ALREADY_MERGED_SCHEMA}) if (!merged.alreadyMerged) { const r = await agent(executeMergePromptM(def.branch, branch, phaseId), {label: lbl('merge'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA}) @@ -889,34 +1048,51 @@ async function runMilestone(module) { } // step 4: docs/08 field (idempotent — read first, only write if at initial '—') - const field = await agent(readDocs08FieldPromptM(fe, id), {label: lbl('field?'), phase: 'Milestone', schema: FIELD_VALUE_SCHEMA}) - if (!field.found) throw new Error(`HALT milestone-docs08-missing ${phaseId}: 字段不存在(docs/08 ${fe ? '§ 三' : `§ 二 模块 ${id}`})`) + let field = await agent(readDocs08FieldPromptM(fe, id), {label: lbl('field?'), phase: 'Milestone', schema: FIELD_VALUE_SCHEMA}) + for (let adj = 1; !field.found && adj <= ADJUDICATE_MAX; adj++) { + const verdict = await adjudicate(`milestone-docs08-missing:${phaseId}`, + { problem:`docs/08 ${fe ? '§ 三' : `§ 二 模块 ${id}`} 里程碑字段未找到` }, 'Milestone', adj) + if (verdict.action !== 'retry') throw new Error(`HALT milestone-docs08-missing ${phaseId}: ${verdict.rationale || '字段不存在'}`) + field = await agent(readDocs08FieldPromptM(fe, id), {label: lbl('field?'), phase: 'Milestone', schema: FIELD_VALUE_SCHEMA}) + } + if (!field.found) throw new Error(`HALT milestone-docs08-missing ${phaseId}: ${ADJUDICATE_MAX} 轮仲裁后仍未找到字段`) if (field.value === '—') { - const r = await agent(writeDocs08FieldPromptM(fe, id, targetTag, phaseId, field.lineNumber), {label: lbl('field-write'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA}) - if (!r.success) throw new Error(`HALT milestone-docs08-write ${phaseId}: ${r.error || ''}`) + await runAction(g => writeDocs08FieldPromptM(fe, id, targetTag, phaseId, field.lineNumber) + g, + {site:`milestone-docs08-write:${phaseId}`, grp:'Milestone', label: lbl('field-write')}) } else if (field.value !== targetTag) { - throw new Error(`HALT milestone-docs08-unexpected ${phaseId}: 字段当前 = ${JSON.stringify(field.value)}(行 ${field.lineNumber || '?'}),期望 '—' 或 '${targetTag}'`) + const verdict = await adjudicate(`milestone-docs08-unexpected:${phaseId}`, + { problem:`docs/08 里程碑字段当前 = ${JSON.stringify(field.value)}(行 ${field.lineNumber || '?'}),期望 '—' 或 '${targetTag}'`, allowContinue:true }, 'Milestone', 1) + if (verdict.action === 'halt') throw new Error(`HALT milestone-docs08-unexpected ${phaseId}: ${verdict.rationale || JSON.stringify(field.value)}`) + log(`milestone ${phaseId}: docs/08 字段非预期值(${JSON.stringify(field.value)}),仲裁判放行`) } // else: 已是 targetTag → 静默跳过(续跑场景) // step 5: report § ⑫ FIRST(关键顺序:tag 必须指向"§ ⑫ 已落地"的 commit,否则 // `git checkout milestone/` 看到的报告 § ⑫ 仍是 placeholder。原版顺序 tag → § ⑫ 是已知 bug, // 此处显式倒过来;下面 step 6 的 tag 才会指向新鲜 commit。) - const rpt = await agent(findReportPromptM(phaseId), {label: lbl('report?'), phase: 'Milestone', schema: REPORT_PATH_SCHEMA}) - if (!rpt.found) throw new Error(`HALT milestone-report-missing ${phaseId}: 没有找到匹配 docs/superpowers/module-reports/*-${phaseId}.md 的报告文件`) + let rpt = await agent(findReportPromptM(phaseId), {label: lbl('report?'), phase: 'Milestone', schema: REPORT_PATH_SCHEMA}) + for (let adj = 1; !rpt.found && adj <= ADJUDICATE_MAX; adj++) { + const verdict = await adjudicate(`milestone-report-missing:${phaseId}`, + { problem:`未找到匹配 docs/superpowers/module-reports/*-${phaseId}.md 的报告文件` }, 'Milestone', adj) + if (verdict.action !== 'retry') throw new Error(`HALT milestone-report-missing ${phaseId}: ${verdict.rationale || '报告文件缺失'}`) + rpt = await agent(findReportPromptM(phaseId), {label: lbl('report?'), phase: 'Milestone', schema: REPORT_PATH_SCHEMA}) + } + if (!rpt.found) throw new Error(`HALT milestone-report-missing ${phaseId}: ${ADJUDICATE_MAX} 轮仲裁后仍无报告文件`) if (rpt.currentTagValue === '{{milestone_tag}}') { - const r = await agent(updateReportPromptM(rpt.path, targetTag, phaseId), {label: lbl('report'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA}) - if (!r.success) throw new Error(`HALT milestone-report-update ${phaseId}: ${r.error || ''}`) + await runAction(g => updateReportPromptM(rpt.path, targetTag, phaseId) + g, + {site:`milestone-report-update:${phaseId}`, grp:'Milestone', label: lbl('report')}) } else if (rpt.currentTagValue !== targetTag) { - throw new Error(`HALT milestone-report-unexpected ${phaseId}: ${rpt.path} § ⑫ 当前 = ${JSON.stringify(rpt.currentTagValue)}`) + const verdict = await adjudicate(`milestone-report-unexpected:${phaseId}`, + { problem:`${rpt.path} § ⑫ 当前 = ${JSON.stringify(rpt.currentTagValue)},期望占位符 {{milestone_tag}} 或 ${targetTag}`, allowContinue:true }, 'Milestone', 1) + if (verdict.action === 'halt') throw new Error(`HALT milestone-report-unexpected ${phaseId}: ${verdict.rationale || JSON.stringify(rpt.currentTagValue)}`) + log(`milestone ${phaseId}: 报告 § ⑫ 非预期值(${JSON.stringify(rpt.currentTagValue)}),仲裁判放行`) } // else: 已是 targetTag → 静默跳过(resume 幂等) // step 6: annotated tag (idempotent — tag exists 时静默跳过) const tag = await agent(checkTagExistsPromptM(targetTag), {label: lbl('tag?'), phase: 'Milestone', schema: EXISTS_SCHEMA}) if (!tag.exists) { - const r = await agent(createTagPromptM(phaseId, fe), {label: lbl('tag'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA}) - if (!r.success) throw new Error(`HALT milestone-tag ${phaseId}: ${r.error || ''}`) + await runAction(g => createTagPromptM(phaseId, fe) + g, {site:`milestone-tag:${phaseId}`, grp:'Milestone', label: lbl('tag')}) } log(`milestone: ${phaseId} → ${targetTag}`) @@ -942,8 +1118,8 @@ async function runCrossModule(module) { return } - const r = await agent(writeCrossModuleLogPromptM(id, classified.crossModule), {label: lbl('write'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA}) - if (!r.success) throw new Error(`HALT crossModule-write ${id}: ${r.error || ''}`) + await runAction(g => writeCrossModuleLogPromptM(id, classified.crossModule) + g, + {site:`crossModule-write:${id}`, grp:'Milestone', label: lbl('write')}) log(`cross-module-log: 模块 ${id} 更新 ${classified.crossModule.length} 行`) } @@ -959,8 +1135,9 @@ async function runCrossModule(module) { // verify / tdd 的 HALT throw,让模块主循环 try/catch 捕获不到,残缺模块照样被推进到 milestone。 // 顺序 for-await 让 throw 自然冒泡到主循环 try → catch → break,使 fail-fast 真正生效。 // -// 派生 stage 全部 schema 化:spec/plan/tdd/verify/fix 共用 STAGE_RESULT_SCHEMA, -// sub-agent 写 `{status:'halt', reason}` 时 JS 立即抛 HALT,让"无法继续"不再混入"成功返回"。 +// 派生 stage 全部 schema 化:spec/plan/tdd/verify/fix 共用 STAGE_RESULT_SCHEMA,统一经 runStage 跑: +// stage 优先自主决策继续(缺值挑默认/解读并记入 decisions[]);返回 status:halt 或结构校验失败时不再立即 +// fail-fast,而是经 adjudicate 仲裁 retry/continue/halt(最多 ADJUDICATE_MAX 轮),把"无法继续"收敛为最后手段。 // 功能级 dedup 真值 = `req-done/` git tag:featureLoop 入口先 check,存在则 skip(Router 文档/ // LLM 自审失误不再导致已 approve 的 REQ 被重新 spec→plan→tdd 污染源码 / 撞 V)。 // @@ -976,113 +1153,188 @@ async function featureLoop(items, phase) { const done = await agent(checkReqDoneTagPromptM(id), {label:`donecheck:${phase}:${id}`, phase: grp, schema: EXISTS_SCHEMA}) if (done.exists) { log(`featureLoop skip ${phase}:${id} — tag req-done/${id} 已存在`); continue } - const spec = await agent(deriveSpecPrompt(id, phase), {label:`spec:${phase}:${id}`, phase: grp, schema: STAGE_RESULT_SCHEMA}) - if (spec.status === 'halt') throw new Error(`HALT spec ${phase}:${id}: ${spec.reason || ''}`) - if (!spec.artifactPath) throw new Error(`HALT spec-no-artifactPath ${phase}:${id}: spec returned ok but no artifactPath`) - // 日期一致性自校验:spec 文件名首段必须可被解析为 YYYY-MM-DD(dateFromArtifactPath 会抛)。 - dateFromArtifactPath(spec.artifactPath) - - const plan = await agent(planPrompt(id, phase, spec.artifactPath), {label:`plan:${phase}:${id}`, phase: grp, schema: STAGE_RESULT_SCHEMA}) - if (plan.status === 'halt') throw new Error(`HALT plan ${phase}:${id}: ${plan.reason || ''}`) - if (!plan.artifactPath) throw new Error(`HALT plan-no-artifactPath ${phase}:${id}`) - if (dateFromArtifactPath(plan.artifactPath) !== dateFromArtifactPath(spec.artifactPath)) { - throw new Error(`HALT plan-date-mismatch ${phase}:${id}: plan ${plan.artifactPath} 与 spec ${spec.artifactPath} 日期前缀不一致`) - } - - const impl = await agent(tddPrompt(id, phase, plan.artifactPath), {label:`tdd:${phase}:${id}`, phase: grp, schema: STAGE_RESULT_SCHEMA}) - if (impl.status === 'halt') throw new Error(`HALT tdd ${phase}:${id}: ${impl.reason || ''}`) - - const v0 = await agent(verifyPrompt(id, phase, impl.summary || '', spec.artifactPath, 0), {label:`verify:${phase}:${id}`, phase: grp, schema: STAGE_RESULT_SCHEMA}) - if (v0.status === 'halt') throw new Error(`HALT verify ${phase}:${id}: ${v0.reason || ''}`) + const spec = await runStage(g => deriveSpecPrompt(id, phase) + g, { + site:`spec:${phase}:${id}`, grp, label:`spec:${phase}:${id}`, + validate: r => { + if (!r.artifactPath) return 'spec 返回 ok 但缺 artifactPath(流程靠它定位 spec 并派生下游日期前缀)' + dateFromArtifactPath(r.artifactPath) // 文件名日期前缀非法 → 抛,被 runStage 捕获转为仲裁 + return null + }, + }) + // spec 经仲裁 continue 时 artifactPath 仍可能不带合法日期前缀——防御取值,避免重算抛出把 continue 变成隐式 halt。 + let specDate = null + try { specDate = dateFromArtifactPath(spec.artifactPath) } catch { specDate = null } + + const plan = await runStage(g => planPrompt(id, phase, spec.artifactPath) + g, { + site:`plan:${phase}:${id}`, grp, label:`plan:${phase}:${id}`, + validate: r => { + if (!r.artifactPath) return 'plan 返回 ok 但缺 artifactPath' + if (specDate && dateFromArtifactPath(r.artifactPath) !== specDate) + return `plan 日期前缀与 spec 不一致:plan=${r.artifactPath} / spec=${spec.artifactPath}` + return null + }, + }) + + // tdd allowContinue:false:tddPrompt 的 halt = 路径作用域越界护栏 / 同测试卡死 10 次——硬边界, + // 仲裁不得 continue 放行(越界把前端实现混进后端分支 / 卡死等于测试没真过)。 + const impl = await runStage(g => tddPrompt(id, phase, plan.artifactPath) + g, { + site:`tdd:${phase}:${id}`, grp, label:`tdd:${phase}:${id}`, allowContinue: false, + }) + + // verify allowContinue:false:verifyPrompt 的 halt = 功能测试红色(exit!=0 / failed>0)——与 test-gate 红同级硬边界, + // 绝不 continue 放行红色实现进 review→approve→打 req-done tag(否则红色功能被永久标记完成、resume 跳过)。 + const v0 = await runStage(g => verifyPrompt(id, phase, impl.summary || '', spec.artifactPath, 0) + g, { + site:`verify:${phase}:${id}`, grp, label:`verify:${phase}:${id}`, allowContinue: false, + }) const reviewResult = await reviewWithFixLoop(id, phase, v0, spec.artifactPath) log(`review approved ${phase}:${id} after ${reviewResult.rounds} round(s)`) - // approve 后落地 dedup 真值:req-done/ tag。 - const tagR = await agent(createReqDoneTagPromptM(id, phase), {label:`reqdone:${phase}:${id}`, phase: grp, schema: ACTION_RESULT_SCHEMA}) - if (!tagR.success) throw new Error(`HALT req-done-tag ${phase}:${id}: ${tagR.error || ''}`) + // approve 后落地 dedup 真值:req-done/ tag(失败经仲裁重试,确不可恢复才 halt)。 + await runAction(g => createReqDoneTagPromptM(id, phase) + g, { + site:`req-done-tag:${phase}:${id}`, grp, label:`reqdone:${phase}:${id}`, + }) } } -// 有界 5 轮修复;超出 → throw(终止态,非对话框)。approve 后独立 micro step flip docs/08 checkbox。 +// review→fix 循环。halt 收敛点: +// - 软上限 REVIEW_SOFT_ROUNDS 轮起每轮经仲裁决定**再延一轮(retry) 或 收尾(halt)**——禁止 continue(approve 只能来自 +// reviewer,仲裁不得在仍有未修 must-fix 时凌驾它放行);绝对硬上限 REVIEW_HARD_ROUNDS 防无限循环。 +// - reviewer 契约小瑕疵不再直接 halt:缺 locator 的 issue 降级为口头建议丢弃;若一条可定位 issue 都不剩(无可执行 +// must-fix),经仲裁决定 continue(视为无 must-fix → approve)/ retry(带 guidance 重判)/ halt。 +// - fix 经 runStage(默认仲裁,可 continue 跳过——未修的 must-fix 由后续 reviewer 重新 flag 兜底); +// reverify 经 runStage 但 allowContinue:false(复验红色 = 修复没生效,绝不放行)。 +// - approve 后的 docs/08 checkbox 是纯可视化副作用(req-done tag 才是完成真值),缺失/写失败一律 log 跳过不 halt。 +const REVIEW_SOFT_ROUNDS = 5 +const REVIEW_HARD_ROUNDS = 8 + +// flipDocs08Checkbox:approve 后把功能行 [ ]→[x]。纯可视化;任何缺失/异常/写失败都降级为日志,绝不 halt。 +async function flipDocs08Checkbox(fe, id, phase, grp) { + const cb = await agent(readDocs08CheckboxPromptM(fe, id), {label:`cb?:${phase}:${id}`, phase: grp, schema: CHECKBOX_STATE_SCHEMA}) + if (!cb.found) { log(`docs08-checkbox ${phase}:${id}: 未找到功能行,跳过可视化勾选(req-done tag 仍是完成真值)`); return } + if (cb.state === 'checked') return + if (cb.state !== 'unchecked') { log(`docs08-checkbox ${phase}:${id}: state 异常 (${JSON.stringify(cb.state)}),跳过勾选`); return } + const wr = await agent(writeDocs08CheckboxPromptM(fe, id, phase, cb.lineNumber), {label:`cb:${phase}:${id}`, phase: grp, schema: ACTION_RESULT_SCHEMA}) + if (!wr.success) log(`docs08-checkbox ${phase}:${id}: 勾选写入失败(${wr.error || ''}),跳过——cosmetic,不阻断`) +} + async function reviewWithFixLoop(id, phase, verifyResult, specPath) { const grp = phase === 'backend' ? 'Backend' : 'Frontend' const fe = isFrontend(phase) let lastVerify = verifyResult let lastIssuesCount = 0 - for (let round = 1; round <= 5; round++) { + let reviewGuidance = '' // 仲裁 retry 时注入下一轮 review 的纠正指令 + for (let round = 1; round <= REVIEW_HARD_ROUNDS; round++) { const lastVerifySummary = (lastVerify && (lastVerify.summary || lastVerify.reason)) || '' // opts.phase = grp('Backend'/'Frontend')是 harness UI 分组;domain phase 见 agents/code-reviewer.md。 const r = await agent( - reviewPrompt(id, phase, round, lastVerifySummary, specPath), + reviewPrompt(id, phase, round, lastVerifySummary, specPath) + adjGuidance(reviewGuidance), {label:`review:${phase}:${id}:r${round}`, phase: grp, schema: REVIEW_SCHEMA, agentType:'code-reviewer'} ) + reviewGuidance = '' // 已消费 + if (r.verdict === 'approve') { - 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} ...\` 行`) - if (cb.state !== 'checked' && cb.state !== 'unchecked') { - throw new Error(`HALT docs08-checkbox-state-invalid ${phase}:${id}: cb.state = ${JSON.stringify(cb.state)}`) - } - if (cb.state === 'unchecked') { - const wr = await agent(writeDocs08CheckboxPromptM(fe, id, phase, cb.lineNumber), {label:`cb:${phase}:${id}`, phase: grp, schema: ACTION_RESULT_SCHEMA}) - if (!wr.success) throw new Error(`HALT docs08-checkbox-write ${phase}:${id}: ${wr.error || ''}`) - } + await flipDocs08Checkbox(fe, id, phase, grp) return { id, phase, approved:true, rounds:round } } - // request-changes 必须带 must-fix 清单(结构化对象数组);否则 fix 步无法定位 → 直接 halt 暴露 reviewer 契约违例。 - if (!Array.isArray(r.issues) || r.issues.length === 0) { - throw new Error(`HALT review-empty-issues ${phase}:${id} r${round}: reviewer 返回 request-changes 但 issues 为空,无法驱动 fix 步`) - } - lastIssuesCount = r.issues.length - // 每个 issue 必须含 locator(locator 校验由 fix sub-agent 在 git cat-file 阶段再做一次硬把关)。 - const missingLocator = r.issues.filter(x => !x || typeof x.locator !== 'string' || !x.locator.trim()) - if (missingLocator.length) { - throw new Error(`HALT review-issue-no-locator ${phase}:${id} r${round}: ${missingLocator.length} 个 issue 缺 locator,reviewer 契约违例(issue summaries: ${missingLocator.map(x=>x?.summary||'').join(' | ')})`) + + // request-changes:保留带 locator 的 must-fix;缺 locator 的降级丢弃(fix 步无从定位)。 + const issues = Array.isArray(r.issues) ? r.issues.filter(x => x && typeof x.locator === 'string' && x.locator.trim()) : [] + const dropped = (Array.isArray(r.issues) ? r.issues.length : 0) - issues.length + if (dropped > 0) log(`review ${phase}:${id} r${round}: 丢弃 ${dropped} 个缺 locator 的 issue(降级为口头建议)`) + if (issues.length === 0) { + // 无任何可执行 must-fix(空 issues 或全缺 locator)→ 仲裁,而非直接 halt。 + const verdict = await adjudicate(`review-no-actionable:${phase}:${id}:r${round}`, + { problem:'reviewer 判 request-changes 但无任何带 locator 的可执行 must-fix(无法驱动 fix 步)', + reviewerIssues: r.issues || [] }, grp, round) + if (verdict.action === 'continue') { await flipDocs08Checkbox(fe, id, phase, grp); return { id, phase, approved:true, rounds:round } } + if (verdict.action === 'halt') throw new Error(`HALT review-no-actionable ${phase}:${id} r${round}: ${verdict.rationale || ''}`) + reviewGuidance = verdict.guidance || '' // retry:带 guidance 重判(进入下一轮) + continue } + lastIssuesCount = issues.length - const fixR = await agent(fixPrompt(id, phase, r.issues), {label:`fix:${phase}:${id}:r${round}`, phase: grp, schema: STAGE_RESULT_SCHEMA}) - if (fixR.status === 'halt') throw new Error(`HALT fix ${phase}:${id} r${round}: ${fixR.reason || ''}`) + await runStage(g => fixPrompt(id, phase, issues) + g, { + site:`fix:${phase}:${id}:r${round}`, grp, label:`fix:${phase}:${id}:r${round}`, + }) - lastVerify = await agent( - verifyPrompt(id, phase, `(第 ${round} 轮 fix 后复验,上轮 must-fix: ${r.issues.length} 项)`, specPath, round), - {label:`reverify:${phase}:${id}:r${round}`, phase: grp, schema: STAGE_RESULT_SCHEMA} + // reverify allowContinue:false:fix 后复验红色 = 修复没真正生效,绝不 continue 放行去 approve。 + lastVerify = await runStage( + g => verifyPrompt(id, phase, `(第 ${round} 轮 fix 后复验,上轮 must-fix: ${issues.length} 项)`, specPath, round) + g, + { site:`reverify:${phase}:${id}:r${round}`, grp, label:`reverify:${phase}:${id}:r${round}`, allowContinue: false }, ) - if (lastVerify.status === 'halt') throw new Error(`HALT reverify ${phase}:${id} r${round}: ${lastVerify.reason || ''}`) + + if (round >= REVIEW_SOFT_ROUNDS) { + // 软上限到顶:仲裁只在"再延一轮(retry) / 收尾(halt)"间选。**禁止 continue 放行 approve**—— + // 此处仍有 reviewer 认定的可定位 must-fix 未清,仲裁不得凌驾专用 code-reviewer 直接判 approve(approve 只能来自 reviewer 本身)。 + const verdict = await adjudicate(`review-extend:${phase}:${id}`, + { problem:`已 ${round} 轮 review 仍未 approve(上轮 ${lastIssuesCount} 项可定位 must-fix 未清)`, + lastVerify: lastVerify.summary || lastVerify.reason || '', allowContinue:false }, grp, round) + if (verdict.action !== 'retry') throw new Error(`HALT review-unresolved ${phase}:${id}: ${verdict.rationale || `${round} 轮仍有未修 must-fix`}`) + // retry → 继续跑到硬上限(approve 仍由后续轮的 reviewer 决定) + } } - throw new Error(`HALT review-unresolved ${phase}:${id}: 5 轮 review 仍未 approve(最后一次 reverify ${lastVerify?.status || '?'},最后一轮 must-fix ${lastIssuesCount} 项)`) + throw new Error(`HALT review-unresolved ${phase}:${id}: ${REVIEW_HARD_ROUNDS} 轮 review 仍未 approve(最后一次 reverify ${lastVerify?.status || '?'},最后一轮 must-fix ${lastIssuesCount} 项)`) } -// flake 重试 1 次:attempt=2 写到独立证据文件 `-test-gate-r2.md`,不覆盖 r1 的 red 证据(report § ⑤ 用得到)。 +// flake 重试:每个 attempt 写独立证据文件 `-test-gate-r.md`,不覆盖前一次 red 证据(report § ⑤ 用得到)。 +// red 是硬正确性边界——**绝不** continue 跳过;只让仲裁在"再跑一次辨 flake"与"确属真失败 → halt"间裁决。 async function testGate(module, phase) { - let g = await agent(gatePrompt(module, phase, 1), {label:`gate:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA}) - if (g.status === 'red') { // 自动重试 1 次(防 flaky) - g = await agent(gatePrompt(module, phase, 2), {label:`gate-retry:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA}) + let attempt = 1 + let g = await agent(gatePrompt(module, phase, attempt), {label:`gate:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA}) + if (g.status === 'red') { // 自动重试 1 次(防 flaky) + attempt = 2 + g = await agent(gatePrompt(module, phase, attempt), {label:`gate-retry:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA}) + } + // 仍 red:经仲裁辨识 flake。allowContinue:false → 红色不可跳过,仲裁只在 retry / halt 间选。 + for (let adj = 1; g.status === 'red' && adj <= ADJUDICATE_MAX; adj++) { + const verdict = await adjudicate(`test-gate-red:${phase}:${module.id}`, + { problem:`test-gate 第 ${attempt} 次仍 red`, failures: g.failures || [], allowContinue:false }, 'Gate', adj) + if (verdict.action !== 'retry') + throw new Error(`HALT test-gate-red ${phase}:${module.id}: ${verdict.rationale || (g.failures||[]).join('; ')}`) + attempt += 1 // retry:再跑一个独立 attempt 证据文件 + g = await agent(gatePrompt(module, phase, attempt), {label:`gate-retry:${phase}:${module.id}:a${attempt}`, phase:'Gate', schema: GATE_SCHEMA}) } - if (g.status === 'red') throw new Error(`HALT test-gate-red ${phase}:${module.id}: ${(g.failures||[]).join('; ')}`) + if (g.status === 'red') throw new Error(`HALT test-gate-red ${phase}:${module.id}: ${ADJUDICATE_MAX} 轮仲裁后仍 red:${(g.failures||[]).join('; ')}`) return g } phase('Router') -const routed = await agent(routerPrompt(ROOT), {label:'router', phase:'Router', schema: ROUTER_SCHEMA}) - // Router 语义断言(feItems/reqs 互斥)+ id 形状硬约束(防 shell 注入:id 直接拼入 `git ... ${id}`)。 +// id 形状(assertSafeId)是**安全护栏**——失败立即硬 halt,绝不重试绕过。 +// reqs/feItems 互斥违例可由仲裁带 guidance 重跑 router 纠正(绝对上限 ADJUDICATE_MAX)。 const ID_PATTERN = /^[A-Za-z0-9_-]+$/ function assertSafeId(kind, value) { if (typeof value !== 'string' || !ID_PATTERN.test(value)) { throw new Error(`HALT router-invalid-${kind}: ${JSON.stringify(value)}(必须匹配 /^[A-Za-z0-9_-]+$/,用于安全地拼入 git 命令)`) } } +function routerViolation(modules) { + for (const m of modules) { + const isFE = m.id === 'frontend-phase' + if (isFE && Array.isArray(m.reqs) && m.reqs.length) + return `frontend-phase 聚合模块的 reqs 必须为空,实测含 ${m.reqs.length} 项 (${m.reqs.join(',')})` + if (!isFE && Array.isArray(m.feItems) && m.feItems.length) + return `后端模块 ${m.id} 的 feItems 必须为空(前端只在 frontend-phase 聚合),实测含 ${m.feItems.length} 项 (${m.feItems.join(',')})` + } + return null +} + +let routed = await agent(routerPrompt(ROOT), {label:'router', phase:'Router', schema: ROUTER_SCHEMA}) +for (let adj = 1; adj <= ADJUDICATE_MAX; adj++) { + const violation = routerViolation(routed.modules) + if (!violation) break + const verdict = await adjudicate('router-violation', { problem: violation }, 'Router', adj) + if (verdict.action !== 'retry') throw new Error(`HALT router-violation: ${verdict.rationale || violation}`) + routed = await agent(routerPrompt(ROOT) + adjGuidance(verdict.guidance || ''), {label:`router:r${adj + 1}`, phase:'Router', schema: ROUTER_SCHEMA}) +} +const finalViolation = routerViolation(routed.modules) +if (finalViolation) throw new Error(`HALT router-violation: ${ADJUDICATE_MAX} 轮仲裁后仍违例:${finalViolation}`) +// id 安全护栏:最终选定的 routed 必须全部通过(assertSafeId 硬 halt)。 for (const m of routed.modules) { assertSafeId('module-id', m.id) for (const r of m.reqs || []) assertSafeId('req-id', r) for (const f of m.feItems || []) assertSafeId('fe-id', f) - const isFE = m.id === 'frontend-phase' - if (isFE && Array.isArray(m.reqs) && m.reqs.length) { - throw new Error(`HALT router-violation: frontend-phase 聚合模块的 reqs 必须为空,实测含 ${m.reqs.length} 项 (${m.reqs.join(',')})`) - } - if (!isFE && Array.isArray(m.feItems) && m.feItems.length) { - throw new Error(`HALT router-violation: 后端模块 ${m.id} 的 feItems 必须为空(前端只在 frontend-phase 聚合),实测含 ${m.feItems.length} 项 (${m.feItems.join(',')})`) - } } const todo = routed.modules.filter(m => !m.done) @@ -1109,8 +1361,11 @@ for (const [idx, module] of todo.entries()) { await testGate(module, 'frontend') } 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 || ''}`) + // report allowContinue:false:reportPrompt 的前置硬验证含"最后一次 test-gate 必须 green,红则 halt"—— + // 绝不 continue 放行去打 milestone(否则可能在红色测试上 milestone)。 + await runStage(g => reportPrompt(module) + g, { + site:`report:${module.id}`, grp:'Milestone', label:`report:${module.id}`, allowContinue: false, + }) await runMilestone(module) results.push({ module: module.id, status:'done' }) } catch (e) { @@ -1126,9 +1381,12 @@ const pending = haltedAtIdx >= 0 ? todo.slice(haltedAtIdx + 1).map(m => ({ module: m.id, status: 'pending' })) : [] -// Workflow 结果:跑完 / halt 的逐模块摘要 + halt 后未跑的 pending 模块列表。 +// Workflow 结果:跑完 / halt 的逐模块摘要 + halt 后未跑的 pending 模块列表 + 全流程自主决策日志 +// (decisions:stage 缺值时未停而自主取的默认/解读,供 coding-start / 人工事后审阅,可能含错误假设)。 +// 注:decisions 仅覆盖**本次运行实际新跑**的 stage;resume 时被 req-done/milestone tag 跳过的已完成功能, +// 其决策不会重新登记于此——需到对应 docs/superpowers/specs|plans/-.md 产物显著位置查阅。 // 注:顶层 `return` 不是普通 Node ESM 语法;本文件由 Claude Workflow 运行时执行, // 运行时会把脚本体包进 async function,顶层 `return` 是 Workflow 的结果通道。 // 不要把本文件作为 `node workflows/coding.mjs` 直接运行,也不要改成 `export default {...}`, // 否则 Workflow 拿不到 results / pending。 -return { results, pending } +return { results, pending, decisions: autonomousDecisions } -- libgit2 0.22.2