Commit 29f3a2dee329d4e8672c371d1dc6f0e75cdef168

Authored by zichun
1 parent 095364cd

coding.mjs: halt 可见性——原因一律先 log 再抛 + agent() null 防御 + coding-start 步骤6 强制横幅

任何 halt 的原因必须直达终端,而不是埋在 Workflow 返回值/RESUME.md/TypeError 里:

- haltError():HALT 抛出前先 log(/workflows 进度叙述行可见)。启动期 4 处
  (invalid-projectRoot / assertSafeId / router-violation×2)换用;模块循环 catch 补
  「⛔ HALT — 模块 <id>:<原因>」;loop-end 补最终状态 log(halt 模块/原因/待跑清单)。
- agent() null 防御:断网/机器休眠时子代理返回 null,不再以 TypeError 形态 halt——
  adjudicate null→确定性默认 retry;runStage/runAction null→经仲裁重试(上限
  ADJUDICATE_MAX);其余 32 处直调点换 agentR()(null→带 label 的诊断 halt);
  flipDocs08Checkbox 保持裸 agent + null 容忍,守住其「绝不 halt」契约。
- coding-start 步骤 6:Workflow 通知返回后第一段输出必须是 halt/完成横幅
  (halt 原因全文不许截断或转述 + 已完成/pending 清单 + [ERP-HALT] 终止标记)。
skills/coding/coding-start/SKILL.md
@@ -129,6 +129,39 @@ Workflow({ @@ -129,6 +129,39 @@ Workflow({
129 129
130 > `[ERP-HALT]` 标记供 Stop 钩子(`hooks/scripts/auto-continue.sh`)识别:Workflow 一旦转后台,主循环应停下等通知,**不**被自动续跑机制误推去重启 coding-start。逐 REQ 手动推进的会话则无此标记,由 Stop 钩子自动续跑,直到打印 `[ERP-HALT]`(全部完成 / 硬护栏)才停。 130 > `[ERP-HALT]` 标记供 Stop 钩子(`hooks/scripts/auto-continue.sh`)识别:Workflow 一旦转后台,主循环应停下等通知,**不**被自动续跑机制误推去重启 coding-start。逐 REQ 手动推进的会话则无此标记,由 Stop 钩子自动续跑,直到打印 `[ERP-HALT]`(全部完成 / 硬护栏)才停。
131 131
  132 +### 步骤 6:Workflow 完成/halt 通知返回后——先输出最终状态横幅
  133 +
  134 +Workflow 的 task-notification 返回本会话后,**第一段输出必须是下面的横幅**(之后才允许做任何诊断动作——读 RESUME.md、读 output 文件、查代码都排在横幅之后),保证用户在终端第一眼看到 halt 原因全文:
  135 +
  136 +- 结果含 `status:'halted'` 模块:
  137 +
  138 +```
  139 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  140 + [coding-start] ⛔ Coding Workflow HALT
  141 +
  142 + 模块:<halted.module>
  143 + 原因:<halted.reason 全文,不许截断或转述>
  144 + 已完成:<status done 的模块列表 或 无>
  145 + 未跑(pending):<pending 列表 或 无>
  146 +
  147 + 下一步:按原因修复后重跑 /erp-workflow:coding-start(Router 按 tag 续跑)。
  148 + [ERP-HALT] 已停在硬护栏点。
  149 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  150 +```
  151 +
  152 +- 全部完成:
  153 +
  154 +```
  155 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  156 + [coding-start] ✅ Coding 阶段全部完成
  157 +
  158 + 完成模块:<列表>
  159 + 自主默认决策:<条数> 条(详见 docs/superpowers/RESUME.md 末尾条目,建议复核)
  160 +
  161 + [ERP-HALT] 编码阶段结束。
  162 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  163 +```
  164 +
132 本 skill 到此结束,**不调用任何下游 skill**。 165 本 skill 到此结束,**不调用任何下游 skill**。
133 166
134 ## 参考 167 ## 参考
workflows/coding.mjs
@@ -198,7 +198,7 @@ const ARGS = typeof args === &#39;string&#39; ? (() =&gt; { try { return JSON.parse(args) } @@ -198,7 +198,7 @@ const ARGS = typeof args === &#39;string&#39; ? (() =&gt; { try { return JSON.parse(args) }
198 const ROOT = ARGS?.projectRoot || '.' 198 const ROOT = ARGS?.projectRoot || '.'
199 // ROOT 必须是绝对路径——相对 '.' 会绑定到子代理隐式 cwd,无保证。 199 // ROOT 必须是绝对路径——相对 '.' 会绑定到子代理隐式 cwd,无保证。
200 if (ROOT === '.' || !(/^(?:\/|[A-Za-z]:[\\/])/.test(ROOT))) { 200 if (ROOT === '.' || !(/^(?:\/|[A-Za-z]:[\\/])/.test(ROOT))) {
201 - throw new Error(`HALT invalid-projectRoot: must be absolute, got ${JSON.stringify(ROOT)}. coding-start 必须把绝对路径传入 args.projectRoot。`) 201 + throw haltError(`HALT invalid-projectRoot: must be absolute, got ${JSON.stringify(ROOT)}. coding-start 必须把绝对路径传入 args.projectRoot。`)
202 } 202 }
203 // 插件根(coding-start 透传):用于调用插件 lib/*.mjs(如 req-ledger 建需求台账基线)。 203 // 插件根(coding-start 透传):用于调用插件 lib/*.mjs(如 req-ledger 建需求台账基线)。
204 // 缺省(旧 coding-start 未传)则相关增强静默跳过——由 /add-req 首跑兜底建基线。 204 // 缺省(旧 coding-start 未传)则相关增强静默跳过——由 /add-req 首跑兜底建基线。
@@ -919,6 +919,7 @@ const ADJUDICATE_MAX = 3 // 单个 site 的仲裁轮上限 @@ -919,6 +919,7 @@ const ADJUDICATE_MAX = 3 // 单个 site 的仲裁轮上限
919 // - BEHAVIOR_ATTEMPT_MAX = 单个 behaviorRound 内的环境 race 重起上限(沿用 testGate attempt 1→2 思路)。 919 // - BEHAVIOR_ATTEMPT_MAX = 单个 behaviorRound 内的环境 race 重起上限(沿用 testGate attempt 1→2 思路)。
920 const BEHAVIOR_STAGE_MAX = 3 920 const BEHAVIOR_STAGE_MAX = 3
921 const BEHAVIOR_ATTEMPT_MAX = 2 921 const BEHAVIOR_ATTEMPT_MAX = 2
  922 +const NULL_AGENT_PROBLEM = 'agent() 返回 null(子代理终端 API 错误 / 被用户跳过)——典型为断网或机器休眠;可重试'
922 const adjGuidance = (g) => g ? `\n\n## 仲裁返回的纠正指令(本次重跑必须遵守)\n${g}` : '' 923 const adjGuidance = (g) => g ? `\n\n## 仲裁返回的纠正指令(本次重跑必须遵守)\n${g}` : ''
923 924
924 // 全流程自主决策日志:stage 缺值时不停而是挑默认/解读,登记在此,随结果回传供人工事后审阅。 925 // 全流程自主决策日志:stage 缺值时不停而是挑默认/解读,登记在此,随结果回传供人工事后审阅。
@@ -932,6 +933,21 @@ function recordDecisions(site, decisions) { @@ -932,6 +933,21 @@ function recordDecisions(site, decisions) {
932 } 933 }
933 } 934 }
934 935
  936 +// halt 可见性:HALT 在抛出前先 log(出现在 /workflows 进度叙述行与 transcript),
  937 +// 保证终端能直接看到 halt 原因,而不是只埋在 Workflow 返回值 / RESUME.md 里。
  938 +// 顶层 function 声明会提升,:201 等早于本定义的调用点也可用。
  939 +function haltError(reason) {
  940 + log(`⛔ ${reason}`)
  941 + return new Error(reason)
  942 +}
  943 +
  944 +// 直调 agent() 的非 runner 点位统一防御:null(断网/休眠/被跳过)→ 带诊断的 halt,而非下游 TypeError。
  945 +async function agentR(prompt, opts) {
  946 + const r = await agent(prompt, opts)
  947 + if (!r) throw haltError(`HALT agent-null ${opts?.label || '?'}: ${NULL_AGENT_PROBLEM}`)
  948 + return r
  949 +}
  950 +
935 function adjudicatePromptM(site, context) { 951 function adjudicatePromptM(site, context) {
936 const ctx = typeof context === 'string' ? context : JSON.stringify(context, null, 2) 952 const ctx = typeof context === 'string' ? context : JSON.stringify(context, null, 2)
937 return [ 953 return [
@@ -960,6 +976,10 @@ function adjudicatePromptM(site, context) { @@ -960,6 +976,10 @@ function adjudicatePromptM(site, context) {
960 async function adjudicate(site, context, grp, round) { 976 async function adjudicate(site, context, grp, round) {
961 const verdict = await agent(adjudicatePromptM(site, context), 977 const verdict = await agent(adjudicatePromptM(site, context),
962 {label:`adjudicate:${site}:r${round}`, phase: grp, schema: ADJUDICATE_SCHEMA}) 978 {label:`adjudicate:${site}:r${round}`, phase: grp, schema: ADJUDICATE_SCHEMA})
  979 + if (!verdict) { // 仲裁子代理自身遭遇终端 API 错误 → 默认 retry(确定性,防 null 级联)
  980 + log(`adjudicate ${site} r${round}: 仲裁子代理无返回(终端 API 错误/被跳过),默认 retry`)
  981 + return { action: 'retry', guidance: '', rationale: 'adjudicate agent 无返回,默认 retry' }
  982 + }
963 log(`adjudicate ${site} r${round}: ${verdict.action}${verdict.rationale ? ' — ' + verdict.rationale : ''}`) 983 log(`adjudicate ${site} r${round}: ${verdict.action}${verdict.rationale ? ' — ' + verdict.rationale : ''}`)
964 return verdict 984 return verdict
965 } 985 }
@@ -974,6 +994,12 @@ async function runStage(makePrompt, { site, grp, label, validate, allowContinue @@ -974,6 +994,12 @@ async function runStage(makePrompt, { site, grp, label, validate, allowContinue
974 let guidance = '' 994 let guidance = ''
975 for (let round = 1; round <= ADJUDICATE_MAX; round++) { 995 for (let round = 1; round <= ADJUDICATE_MAX; round++) {
976 const res = await agent(makePrompt(adjGuidance(guidance)), {label, phase: grp, schema: STAGE_RESULT_SCHEMA}) 996 const res = await agent(makePrompt(adjGuidance(guidance)), {label, phase: grp, schema: STAGE_RESULT_SCHEMA})
  997 + if (!res) {
  998 + const verdict = await adjudicate(site, { problem: NULL_AGENT_PROBLEM, allowContinue: false }, grp, round)
  999 + if (verdict.action !== 'retry') throw haltError(`HALT ${site}: ${NULL_AGENT_PROBLEM}`)
  1000 + guidance = verdict.guidance || ''
  1001 + continue
  1002 + }
977 recordDecisions(site, res.decisions) 1003 recordDecisions(site, res.decisions)
978 let problem = null 1004 let problem = null
979 if (res.status === 'halt') problem = `stage 返回 status:halt;reason: ${res.reason || '(空)'}` 1005 if (res.status === 'halt') problem = `stage 返回 status:halt;reason: ${res.reason || '(空)'}`
@@ -993,6 +1019,12 @@ async function runAction(makePrompt, { site, grp, label, allowContinue = false } @@ -993,6 +1019,12 @@ async function runAction(makePrompt, { site, grp, label, allowContinue = false }
993 let guidance = '' 1019 let guidance = ''
994 for (let round = 1; round <= ADJUDICATE_MAX; round++) { 1020 for (let round = 1; round <= ADJUDICATE_MAX; round++) {
995 const r = await agent(makePrompt(adjGuidance(guidance)), {label, phase: grp, schema: ACTION_RESULT_SCHEMA}) 1021 const r = await agent(makePrompt(adjGuidance(guidance)), {label, phase: grp, schema: ACTION_RESULT_SCHEMA})
  1022 + if (!r) {
  1023 + const verdict = await adjudicate(site, { problem: NULL_AGENT_PROBLEM, allowContinue: false }, grp, round)
  1024 + if (verdict.action !== 'retry') throw haltError(`HALT ${site}: ${NULL_AGENT_PROBLEM}`)
  1025 + guidance = verdict.guidance || ''
  1026 + continue
  1027 + }
996 if (r.success) return r 1028 if (r.success) return r
997 const verdict = await adjudicate(site, 1029 const verdict = await adjudicate(site,
998 { problem:`action 失败:${r.error || ''}${r.detail ? '\n' + r.detail : ''}`, allowContinue }, grp, round) 1030 { problem:`action 失败:${r.error || ''}${r.detail ? '\n' + r.detail : ''}`, allowContinue }, grp, round)
@@ -1489,18 +1521,18 @@ async function runBranchSetup(module) { @@ -1489,18 +1521,18 @@ async function runBranchSetup(module) {
1489 const branch = fe ? 'frontend-phase' : `module-${id}` 1521 const branch = fe ? 'frontend-phase' : `module-${id}`
1490 const lbl = (k) => `branch:${k}:${id}` 1522 const lbl = (k) => `branch:${k}:${id}`
1491 1523
1492 - const def = await agent(detectDefaultBranchPromptM(), {label: lbl('default'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) 1524 + const def = await agentR(detectDefaultBranchPromptM(), {label: lbl('default'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA})
1493 1525
1494 // 工作树脏:先自主恢复(in-scope 残留 → 自动 commit);含越界改动则恢复失败 → halt(留给人工)。 1526 // 工作树脏:先自主恢复(in-scope 残留 → 自动 commit);含越界改动则恢复失败 → halt(留给人工)。
1495 - const wt = await agent(worktreeCleanPromptM(), {label: lbl('wt'), phase: 'Milestone', schema: WT_SCHEMA}) 1527 + const wt = await agentR(worktreeCleanPromptM(), {label: lbl('wt'), phase: 'Milestone', schema: WT_SCHEMA})
1496 if (!wt.clean) { 1528 if (!wt.clean) {
1497 - const rec = await agent(recoverDirtyWorktreePromptM(wt.dirty, branch, `分支 setup 前置(目标分支 ${branch})。`), 1529 + const rec = await agentR(recoverDirtyWorktreePromptM(wt.dirty, branch, `分支 setup 前置(目标分支 ${branch})。`),
1498 {label: lbl('wt-recover'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA}) 1530 {label: lbl('wt-recover'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA})
1499 if (!rec.success) throw new Error(`HALT branchSetup-dirty-worktree ${branch}: ${rec.error || ''}${rec.detail ? '\n' + rec.detail : ''}`) 1531 if (!rec.success) throw new Error(`HALT branchSetup-dirty-worktree ${branch}: ${rec.error || ''}${rec.detail ? '\n' + rec.detail : ''}`)
1500 log(`branch-setup: ${id} 自动提交脏工作树残留(${rec.detail || ''})`) 1532 log(`branch-setup: ${id} 自动提交脏工作树残留(${rec.detail || ''})`)
1501 } 1533 }
1502 1534
1503 - const exists = await agent(checkBranchExistsPromptM(branch), {label: lbl('exists?'), phase: 'Milestone', schema: EXISTS_SCHEMA}) 1535 + const exists = await agentR(checkBranchExistsPromptM(branch), {label: lbl('exists?'), phase: 'Milestone', schema: EXISTS_SCHEMA})
1504 if (exists.exists) { 1536 if (exists.exists) {
1505 await runAction(g => checkoutExistingBranchPromptM(branch) + g, {site:`branchSetup-checkout:${branch}`, grp:'Milestone', label: lbl('checkout')}) 1537 await runAction(g => checkoutExistingBranchPromptM(branch) + g, {site:`branchSetup-checkout:${branch}`, grp:'Milestone', label: lbl('checkout')})
1506 } else { 1538 } else {
@@ -1508,14 +1540,14 @@ async function runBranchSetup(module) { @@ -1508,14 +1540,14 @@ async function runBranchSetup(module) {
1508 } 1540 }
1509 1541
1510 // HEAD 确认:不符则经仲裁重切(retry)或留人工(halt)。 1542 // HEAD 确认:不符则经仲裁重切(retry)或留人工(halt)。
1511 - let head = await agent(currentBranchPromptM(), {label: lbl('head'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) 1543 + let head = await agentR(currentBranchPromptM(), {label: lbl('head'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA})
1512 for (let adj = 1; head.branch !== branch && adj <= ADJUDICATE_MAX; adj++) { 1544 for (let adj = 1; head.branch !== branch && adj <= ADJUDICATE_MAX; adj++) {
1513 const verdict = await adjudicate(`branchSetup-branch-mismatch:${branch}`, 1545 const verdict = await adjudicate(`branchSetup-branch-mismatch:${branch}`,
1514 { problem:`分支 setup 后 HEAD 在 ${head.branch},期望 ${branch}` }, 'Milestone', adj) 1546 { problem:`分支 setup 后 HEAD 在 ${head.branch},期望 ${branch}` }, 'Milestone', adj)
1515 if (verdict.action !== 'retry') 1547 if (verdict.action !== 'retry')
1516 throw new Error(`HALT branchSetup-branch-mismatch ${branch}: ${verdict.rationale || `HEAD on ${head.branch}`}`) 1548 throw new Error(`HALT branchSetup-branch-mismatch ${branch}: ${verdict.rationale || `HEAD on ${head.branch}`}`)
1517 await runAction(g => checkoutExistingBranchPromptM(branch) + g, {site:`branchSetup-recheckout:${branch}`, grp:'Milestone', label: lbl('recheckout')}) 1549 await runAction(g => checkoutExistingBranchPromptM(branch) + g, {site:`branchSetup-recheckout:${branch}`, grp:'Milestone', label: lbl('recheckout')})
1518 - head = await agent(currentBranchPromptM(), {label: lbl('head'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) 1550 + head = await agentR(currentBranchPromptM(), {label: lbl('head'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA})
1519 } 1551 }
1520 if (head.branch !== branch) throw new Error(`HALT branchSetup-branch-mismatch ${branch}: ${ADJUDICATE_MAX} 轮后 HEAD 仍在 ${head.branch}`) 1552 if (head.branch !== branch) throw new Error(`HALT branchSetup-branch-mismatch ${branch}: ${ADJUDICATE_MAX} 轮后 HEAD 仍在 ${head.branch}`)
1521 1553
@@ -1533,32 +1565,32 @@ async function runMilestone(module) { @@ -1533,32 +1565,32 @@ async function runMilestone(module) {
1533 const lbl = (k) => `milestone:${k}:${phaseId}` 1565 const lbl = (k) => `milestone:${k}:${phaseId}`
1534 1566
1535 // step 1: worktree clean precondition(脏树先自主恢复 in-scope 残留;含越界改动则 halt 留人工) 1567 // step 1: worktree clean precondition(脏树先自主恢复 in-scope 残留;含越界改动则 halt 留人工)
1536 - const wt = await agent(worktreeCleanPromptM(), {label: lbl('wt'), phase: 'Milestone', schema: WT_SCHEMA}) 1568 + const wt = await agentR(worktreeCleanPromptM(), {label: lbl('wt'), phase: 'Milestone', schema: WT_SCHEMA})
1537 if (!wt.clean) { 1569 if (!wt.clean) {
1538 - const rec = await agent(recoverDirtyWorktreePromptM(wt.dirty, branch, `里程碑前置(阶段 ${phaseId},应在功能分支 ${branch})。`), 1570 + const rec = await agentR(recoverDirtyWorktreePromptM(wt.dirty, branch, `里程碑前置(阶段 ${phaseId},应在功能分支 ${branch})。`),
1539 {label: lbl('wt-recover'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA}) 1571 {label: lbl('wt-recover'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA})
1540 if (!rec.success) throw new Error(`HALT milestone-dirty-worktree ${phaseId}: ${rec.error || ''}${rec.detail ? '\n' + rec.detail : ''}`) 1572 if (!rec.success) throw new Error(`HALT milestone-dirty-worktree ${phaseId}: ${rec.error || ''}${rec.detail ? '\n' + rec.detail : ''}`)
1541 log(`milestone: ${phaseId} 自动提交脏工作树残留(${rec.detail || ''})`) 1573 log(`milestone: ${phaseId} 自动提交脏工作树残留(${rec.detail || ''})`)
1542 } 1574 }
1543 1575
1544 // step 2: detect default branch 1576 // step 2: detect default branch
1545 - const def = await agent(detectDefaultBranchPromptM(), {label: lbl('default'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) 1577 + const def = await agentR(detectDefaultBranchPromptM(), {label: lbl('default'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA})
1546 1578
1547 // step 3: merge (idempotent — skip if already an ancestor) 1579 // step 3: merge (idempotent — skip if already an ancestor)
1548 // merge 冲突保持**硬 halt**:自动 abort/stash/改文件均不安全,把树留给人工(设计原则不变)。 1580 // merge 冲突保持**硬 halt**:自动 abort/stash/改文件均不安全,把树留给人工(设计原则不变)。
1549 - const merged = await agent(checkAlreadyMergedPromptM(branch, def.branch), {label: lbl('merged?'), phase: 'Milestone', schema: ALREADY_MERGED_SCHEMA}) 1581 + const merged = await agentR(checkAlreadyMergedPromptM(branch, def.branch), {label: lbl('merged?'), phase: 'Milestone', schema: ALREADY_MERGED_SCHEMA})
1550 if (!merged.alreadyMerged) { 1582 if (!merged.alreadyMerged) {
1551 - const r = await agent(executeMergePromptM(def.branch, branch, phaseId), {label: lbl('merge'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA}) 1583 + const r = await agentR(executeMergePromptM(def.branch, branch, phaseId), {label: lbl('merge'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA})
1552 if (!r.success) throw new Error(`HALT milestone-merge ${phaseId}: ${r.error || ''}${r.detail ? '\n' + r.detail : ''}`) 1584 if (!r.success) throw new Error(`HALT milestone-merge ${phaseId}: ${r.error || ''}${r.detail ? '\n' + r.detail : ''}`)
1553 } 1585 }
1554 1586
1555 // step 4: docs/08 field (idempotent — read first, only write if at initial '—') 1587 // step 4: docs/08 field (idempotent — read first, only write if at initial '—')
1556 - let field = await agent(readDocs08FieldPromptM(fe, id), {label: lbl('field?'), phase: 'Milestone', schema: FIELD_VALUE_SCHEMA}) 1588 + let field = await agentR(readDocs08FieldPromptM(fe, id), {label: lbl('field?'), phase: 'Milestone', schema: FIELD_VALUE_SCHEMA})
1557 for (let adj = 1; !field.found && adj <= ADJUDICATE_MAX; adj++) { 1589 for (let adj = 1; !field.found && adj <= ADJUDICATE_MAX; adj++) {
1558 const verdict = await adjudicate(`milestone-docs08-missing:${phaseId}`, 1590 const verdict = await adjudicate(`milestone-docs08-missing:${phaseId}`,
1559 { problem:`docs/08 ${fe ? '§ 三' : `§ 二 模块 ${id}`} 里程碑字段未找到` }, 'Milestone', adj) 1591 { problem:`docs/08 ${fe ? '§ 三' : `§ 二 模块 ${id}`} 里程碑字段未找到` }, 'Milestone', adj)
1560 if (verdict.action !== 'retry') throw new Error(`HALT milestone-docs08-missing ${phaseId}: ${verdict.rationale || '字段不存在'}`) 1592 if (verdict.action !== 'retry') throw new Error(`HALT milestone-docs08-missing ${phaseId}: ${verdict.rationale || '字段不存在'}`)
1561 - field = await agent(readDocs08FieldPromptM(fe, id), {label: lbl('field?'), phase: 'Milestone', schema: FIELD_VALUE_SCHEMA}) 1593 + field = await agentR(readDocs08FieldPromptM(fe, id), {label: lbl('field?'), phase: 'Milestone', schema: FIELD_VALUE_SCHEMA})
1562 } 1594 }
1563 if (!field.found) throw new Error(`HALT milestone-docs08-missing ${phaseId}: ${ADJUDICATE_MAX} 轮仲裁后仍未找到字段`) 1595 if (!field.found) throw new Error(`HALT milestone-docs08-missing ${phaseId}: ${ADJUDICATE_MAX} 轮仲裁后仍未找到字段`)
1564 if (field.value === '—') { 1596 if (field.value === '—') {
@@ -1575,12 +1607,12 @@ async function runMilestone(module) { @@ -1575,12 +1607,12 @@ async function runMilestone(module) {
1575 // step 5: report § ⑫ FIRST(关键顺序:tag 必须指向"§ ⑫ 已落地"的 commit,否则 1607 // step 5: report § ⑫ FIRST(关键顺序:tag 必须指向"§ ⑫ 已落地"的 commit,否则
1576 // `git checkout milestone/<id>` 看到的报告 § ⑫ 仍是 placeholder。原版顺序 tag → § ⑫ 是已知 bug, 1608 // `git checkout milestone/<id>` 看到的报告 § ⑫ 仍是 placeholder。原版顺序 tag → § ⑫ 是已知 bug,
1577 // 此处显式倒过来;下面 step 6 的 tag 才会指向新鲜 commit。) 1609 // 此处显式倒过来;下面 step 6 的 tag 才会指向新鲜 commit。)
1578 - let rpt = await agent(findReportPromptM(phaseId), {label: lbl('report?'), phase: 'Milestone', schema: REPORT_PATH_SCHEMA}) 1610 + let rpt = await agentR(findReportPromptM(phaseId), {label: lbl('report?'), phase: 'Milestone', schema: REPORT_PATH_SCHEMA})
1579 for (let adj = 1; !rpt.found && adj <= ADJUDICATE_MAX; adj++) { 1611 for (let adj = 1; !rpt.found && adj <= ADJUDICATE_MAX; adj++) {
1580 const verdict = await adjudicate(`milestone-report-missing:${phaseId}`, 1612 const verdict = await adjudicate(`milestone-report-missing:${phaseId}`,
1581 { problem:`未找到匹配 docs/superpowers/module-reports/*-${phaseId}.md 的报告文件` }, 'Milestone', adj) 1613 { problem:`未找到匹配 docs/superpowers/module-reports/*-${phaseId}.md 的报告文件` }, 'Milestone', adj)
1582 if (verdict.action !== 'retry') throw new Error(`HALT milestone-report-missing ${phaseId}: ${verdict.rationale || '报告文件缺失'}`) 1614 if (verdict.action !== 'retry') throw new Error(`HALT milestone-report-missing ${phaseId}: ${verdict.rationale || '报告文件缺失'}`)
1583 - rpt = await agent(findReportPromptM(phaseId), {label: lbl('report?'), phase: 'Milestone', schema: REPORT_PATH_SCHEMA}) 1615 + rpt = await agentR(findReportPromptM(phaseId), {label: lbl('report?'), phase: 'Milestone', schema: REPORT_PATH_SCHEMA})
1584 } 1616 }
1585 if (!rpt.found) throw new Error(`HALT milestone-report-missing ${phaseId}: ${ADJUDICATE_MAX} 轮仲裁后仍无报告文件`) 1617 if (!rpt.found) throw new Error(`HALT milestone-report-missing ${phaseId}: ${ADJUDICATE_MAX} 轮仲裁后仍无报告文件`)
1586 if (rpt.currentTagValue === '{{milestone_tag}}') { 1618 if (rpt.currentTagValue === '{{milestone_tag}}') {
@@ -1595,7 +1627,7 @@ async function runMilestone(module) { @@ -1595,7 +1627,7 @@ async function runMilestone(module) {
1595 // else: 已是 targetTag → 静默跳过(resume 幂等) 1627 // else: 已是 targetTag → 静默跳过(resume 幂等)
1596 1628
1597 // step 6: annotated tag (idempotent — tag exists 时静默跳过) 1629 // step 6: annotated tag (idempotent — tag exists 时静默跳过)
1598 - const tag = await agent(checkTagExistsPromptM(targetTag), {label: lbl('tag?'), phase: 'Milestone', schema: EXISTS_SCHEMA}) 1630 + const tag = await agentR(checkTagExistsPromptM(targetTag), {label: lbl('tag?'), phase: 'Milestone', schema: EXISTS_SCHEMA})
1599 if (!tag.exists) { 1631 if (!tag.exists) {
1600 await runAction(g => createTagPromptM(phaseId, fe) + g, {site:`milestone-tag:${phaseId}`, grp:'Milestone', label: lbl('tag')}) 1632 await runAction(g => createTagPromptM(phaseId, fe) + g, {site:`milestone-tag:${phaseId}`, grp:'Milestone', label: lbl('tag')})
1601 } 1633 }
@@ -1609,15 +1641,15 @@ async function runCrossModule(module) { @@ -1609,15 +1641,15 @@ async function runCrossModule(module) {
1609 const id = module?.id ?? '<module>' 1641 const id = module?.id ?? '<module>'
1610 const lbl = (k) => `xmod:${k}:${id}` 1642 const lbl = (k) => `xmod:${k}:${id}`
1611 1643
1612 - const def = await agent(detectDefaultBranchPromptM(), {label: lbl('default'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) 1644 + const def = await agentR(detectDefaultBranchPromptM(), {label: lbl('default'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA})
1613 1645
1614 - const changed = await agent(collectCrossModuleChangedPromptM(def.branch), {label: lbl('diff'), phase: 'Milestone', schema: CHANGED_FILES_SCHEMA}) 1646 + const changed = await agentR(collectCrossModuleChangedPromptM(def.branch), {label: lbl('diff'), phase: 'Milestone', schema: CHANGED_FILES_SCHEMA})
1615 if (!changed.files.length) { 1647 if (!changed.files.length) {
1616 log(`cross-module-log: 模块 ${id} 无文件改动,跳过`) 1648 log(`cross-module-log: 模块 ${id} 无文件改动,跳过`)
1617 return 1649 return
1618 } 1650 }
1619 1651
1620 - const classified = await agent(classifyCrossModulePromptM(id, changed.files), {label: lbl('classify'), phase: 'Milestone', schema: CROSS_CLASSIFY_SCHEMA}) 1652 + const classified = await agentR(classifyCrossModulePromptM(id, changed.files), {label: lbl('classify'), phase: 'Milestone', schema: CROSS_CLASSIFY_SCHEMA})
1621 if (!classified.crossModule.length) { 1653 if (!classified.crossModule.length) {
1622 log(`cross-module-log: 模块 ${id} 无跨模块改动,跳过`) 1654 log(`cross-module-log: 模块 ${id} 无跨模块改动,跳过`)
1623 return 1655 return
@@ -1636,7 +1668,7 @@ async function runFrontendSkeleton(feItems) { @@ -1636,7 +1668,7 @@ async function runFrontendSkeleton(feItems) {
1636 const lbl = (k) => `fe-skeleton:${k}` 1668 const lbl = (k) => `fe-skeleton:${k}`
1637 1669
1638 // step 1: 检查 router 是否已声明全 FE 路由;已建则只确保补记 tag 存在。 1670 // step 1: 检查 router 是否已声明全 FE 路由;已建则只确保补记 tag 存在。
1639 - const state = await agent(frontendSkeletonStatePromptM(feItems), 1671 + const state = await agentR(frontendSkeletonStatePromptM(feItems),
1640 {label: lbl('state?'), phase: 'Frontend', schema: EXISTS_SCHEMA}) 1672 {label: lbl('state?'), phase: 'Frontend', schema: EXISTS_SCHEMA})
1641 if (state.exists) { 1673 if (state.exists) {
1642 log('fe-skeleton: router 已声明全部 FE 路由,确保 fe-skeleton-done tag 存在') 1674 log('fe-skeleton: router 已声明全部 FE 路由,确保 fe-skeleton-done tag 存在')
@@ -1682,7 +1714,7 @@ async function featureLoop(items, phase) { @@ -1682,7 +1714,7 @@ async function featureLoop(items, phase) {
1682 const grp = phase === 'backend' ? 'Backend' : 'Frontend' 1714 const grp = phase === 'backend' ? 'Backend' : 'Frontend'
1683 for (const id of items) { 1715 for (const id of items) {
1684 // 入口 dedup:req-done/<id> 已存在 → 已 approve,整段 skip。 1716 // 入口 dedup:req-done/<id> 已存在 → 已 approve,整段 skip。
1685 - const done = await agent(checkReqDoneTagPromptM(id), {label:`donecheck:${phase}:${id}`, phase: grp, schema: EXISTS_SCHEMA}) 1717 + const done = await agentR(checkReqDoneTagPromptM(id), {label:`donecheck:${phase}:${id}`, phase: grp, schema: EXISTS_SCHEMA})
1686 if (done.exists) { log(`featureLoop skip ${phase}:${id} — tag req-done/${id} 已存在`); continue } 1718 if (done.exists) { log(`featureLoop skip ${phase}:${id} — tag req-done/${id} 已存在`); continue }
1687 1719
1688 const spec = await runStage(g => deriveSpecPrompt(id, phase) + g, { 1720 const spec = await runStage(g => deriveSpecPrompt(id, phase) + g, {
@@ -1743,11 +1775,12 @@ const REVIEW_HARD_ROUNDS = 10 @@ -1743,11 +1775,12 @@ const REVIEW_HARD_ROUNDS = 10
1743 // flipDocs08Checkbox:approve 后把功能行 [ ]→[x]。纯可视化;任何缺失/异常/写失败都降级为日志,绝不 halt。 1775 // flipDocs08Checkbox:approve 后把功能行 [ ]→[x]。纯可视化;任何缺失/异常/写失败都降级为日志,绝不 halt。
1744 async function flipDocs08Checkbox(fe, id, phase, grp) { 1776 async function flipDocs08Checkbox(fe, id, phase, grp) {
1745 const cb = await agent(readDocs08CheckboxPromptM(fe, id), {label:`cb?:${phase}:${id}`, phase: grp, schema: CHECKBOX_STATE_SCHEMA}) 1777 const cb = await agent(readDocs08CheckboxPromptM(fe, id), {label:`cb?:${phase}:${id}`, phase: grp, schema: CHECKBOX_STATE_SCHEMA})
  1778 + if (!cb) { log(`docs08-checkbox ${id}: 读取子代理无返回(不阻断)`); return }
1746 if (!cb.found) { log(`docs08-checkbox ${phase}:${id}: 未找到功能行,跳过可视化勾选(req-done tag 仍是完成真值)`); return } 1779 if (!cb.found) { log(`docs08-checkbox ${phase}:${id}: 未找到功能行,跳过可视化勾选(req-done tag 仍是完成真值)`); return }
1747 if (cb.state === 'checked') return 1780 if (cb.state === 'checked') return
1748 if (cb.state !== 'unchecked') { log(`docs08-checkbox ${phase}:${id}: state 异常 (${JSON.stringify(cb.state)}),跳过勾选`); return } 1781 if (cb.state !== 'unchecked') { log(`docs08-checkbox ${phase}:${id}: state 异常 (${JSON.stringify(cb.state)}),跳过勾选`); return }
1749 const wr = await agent(writeDocs08CheckboxPromptM(fe, id, phase, cb.lineNumber), {label:`cb:${phase}:${id}`, phase: grp, schema: ACTION_RESULT_SCHEMA}) 1782 const wr = await agent(writeDocs08CheckboxPromptM(fe, id, phase, cb.lineNumber), {label:`cb:${phase}:${id}`, phase: grp, schema: ACTION_RESULT_SCHEMA})
1750 - if (!wr.success) log(`docs08-checkbox ${phase}:${id}: 勾选写入失败(${wr.error || ''}),跳过——cosmetic,不阻断`) 1783 + if (!wr || !wr.success) log(`docs08-checkbox ${phase}:${id}: 勾选写入失败(${(wr && wr.error) || ''}),跳过——cosmetic,不阻断`)
1751 } 1784 }
1752 1785
1753 async function reviewWithFixLoop(id, phase, verifyResult, specPath) { 1786 async function reviewWithFixLoop(id, phase, verifyResult, specPath) {
@@ -1759,7 +1792,7 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) { @@ -1759,7 +1792,7 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) {
1759 for (let round = 1; round <= REVIEW_HARD_ROUNDS; round++) { 1792 for (let round = 1; round <= REVIEW_HARD_ROUNDS; round++) {
1760 const lastVerifySummary = (lastVerify && (lastVerify.summary || lastVerify.reason)) || '' 1793 const lastVerifySummary = (lastVerify && (lastVerify.summary || lastVerify.reason)) || ''
1761 // opts.phase = grp('Backend'/'Frontend')是 harness UI 分组;domain phase 见 agents/code-reviewer.md。 1794 // opts.phase = grp('Backend'/'Frontend')是 harness UI 分组;domain phase 见 agents/code-reviewer.md。
1762 - const r = await agent( 1795 + const r = await agentR(
1763 reviewPrompt(id, phase, round, lastVerifySummary, specPath) + adjGuidance(reviewGuidance), 1796 reviewPrompt(id, phase, round, lastVerifySummary, specPath) + adjGuidance(reviewGuidance),
1764 {label:`review:${phase}:${id}:r${round}`, phase: grp, schema: REVIEW_SCHEMA, agentType:'erp-workflow:code-reviewer'} 1797 {label:`review:${phase}:${id}:r${round}`, phase: grp, schema: REVIEW_SCHEMA, agentType:'erp-workflow:code-reviewer'}
1765 ) 1798 )
@@ -1819,10 +1852,10 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) { @@ -1819,10 +1852,10 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) {
1819 // red 是硬正确性边界——**绝不** continue 跳过;只让仲裁在"再跑一次辨 flake"与"确属真失败 → halt"间裁决。 1852 // red 是硬正确性边界——**绝不** continue 跳过;只让仲裁在"再跑一次辨 flake"与"确属真失败 → halt"间裁决。
1820 async function testGate(module, phase) { 1853 async function testGate(module, phase) {
1821 let attempt = 1 1854 let attempt = 1
1822 - let g = await agent(gatePrompt(module, phase, attempt), {label:`gate:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA}) 1855 + let g = await agentR(gatePrompt(module, phase, attempt), {label:`gate:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA})
1823 if (g.status === 'red') { // 自动重试 1 次(防 flaky) 1856 if (g.status === 'red') { // 自动重试 1 次(防 flaky)
1824 attempt = 2 1857 attempt = 2
1825 - g = await agent(gatePrompt(module, phase, attempt), {label:`gate-retry:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA}) 1858 + g = await agentR(gatePrompt(module, phase, attempt), {label:`gate-retry:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA})
1826 } 1859 }
1827 // 仍 red:经仲裁辨识 flake。allowContinue:false → 红色不可跳过,仲裁只在 retry / halt 间选。 1860 // 仍 red:经仲裁辨识 flake。allowContinue:false → 红色不可跳过,仲裁只在 retry / halt 间选。
1828 for (let adj = 1; g.status === 'red' && adj <= ADJUDICATE_MAX; adj++) { 1861 for (let adj = 1; g.status === 'red' && adj <= ADJUDICATE_MAX; adj++) {
@@ -1831,7 +1864,7 @@ async function testGate(module, phase) { @@ -1831,7 +1864,7 @@ async function testGate(module, phase) {
1831 if (verdict.action !== 'retry') 1864 if (verdict.action !== 'retry')
1832 throw new Error(`HALT test-gate-red ${phase}:${module.id}: ${verdict.rationale || (g.failures||[]).join('; ')}`) 1865 throw new Error(`HALT test-gate-red ${phase}:${module.id}: ${verdict.rationale || (g.failures||[]).join('; ')}`)
1833 attempt += 1 // retry:再跑一个独立 attempt 证据文件 1866 attempt += 1 // retry:再跑一个独立 attempt 证据文件
1834 - g = await agent(gatePrompt(module, phase, attempt), {label:`gate-retry:${phase}:${module.id}:a${attempt}`, phase:'Gate', schema: GATE_SCHEMA}) 1867 + g = await agentR(gatePrompt(module, phase, attempt), {label:`gate-retry:${phase}:${module.id}:a${attempt}`, phase:'Gate', schema: GATE_SCHEMA})
1835 } 1868 }
1836 if (g.status === 'red') throw new Error(`HALT test-gate-red ${phase}:${module.id}: ${ADJUDICATE_MAX} 轮仲裁后仍 red:${(g.failures||[]).join('; ')}`) 1869 if (g.status === 'red') throw new Error(`HALT test-gate-red ${phase}:${module.id}: ${ADJUDICATE_MAX} 轮仲裁后仍 red:${(g.failures||[]).join('; ')}`)
1837 return g 1870 return g
@@ -1865,7 +1898,7 @@ const isBuildFailed = (r) =&gt; !!(r.envError &amp;&amp; r.envError.kind === &#39;build-failed&#39; @@ -1865,7 +1898,7 @@ const isBuildFailed = (r) =&gt; !!(r.envError &amp;&amp; r.envError.kind === &#39;build-failed&#39;
1865 async function runBehaviorGateOnce(feItems, behaviorRound) { 1898 async function runBehaviorGateOnce(feItems, behaviorRound) {
1866 const lbl = (a) => `behavior:frontend-phase:r${behaviorRound}:a${a}` 1899 const lbl = (a) => `behavior:frontend-phase:r${behaviorRound}:a${a}`
1867 let attempt = 1 1900 let attempt = 1
1868 - let bg = await agent(behaviorGatePrompt(feItems, behaviorRound, attempt), 1901 + let bg = await agentR(behaviorGatePrompt(feItems, behaviorRound, attempt),
1869 {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA}) 1902 {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA})
1870 recordDecisions('behavior:frontend-phase', bg.decisions) 1903 recordDecisions('behavior:frontend-phase', bg.decisions)
1871 1904
@@ -1873,7 +1906,7 @@ async function runBehaviorGateOnce(feItems, behaviorRound) { @@ -1873,7 +1906,7 @@ async function runBehaviorGateOnce(feItems, behaviorRound) {
1873 // build-failed 是确定性失败(重起不自愈)→ 跳过自动重起,直接进下方仲裁循环。 1906 // build-failed 是确定性失败(重起不自愈)→ 跳过自动重起,直接进下方仲裁循环。
1874 while (behaviorEnvBlocked(bg).blocked && !isBuildFailed(bg) && attempt < BEHAVIOR_ATTEMPT_MAX) { 1907 while (behaviorEnvBlocked(bg).blocked && !isBuildFailed(bg) && attempt < BEHAVIOR_ATTEMPT_MAX) {
1875 attempt += 1 1908 attempt += 1
1876 - bg = await agent(behaviorGatePrompt(feItems, behaviorRound, attempt), 1909 + bg = await agentR(behaviorGatePrompt(feItems, behaviorRound, attempt),
1877 {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA}) 1910 {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA})
1878 recordDecisions('behavior:frontend-phase', bg.decisions) 1911 recordDecisions('behavior:frontend-phase', bg.decisions)
1879 } 1912 }
@@ -1891,7 +1924,7 @@ async function runBehaviorGateOnce(feItems, behaviorRound) { @@ -1891,7 +1924,7 @@ async function runBehaviorGateOnce(feItems, behaviorRound) {
1891 allowContinue:false }, 'Behavior', adj) 1924 allowContinue:false }, 'Behavior', adj)
1892 if (verdict.action !== 'retry') throw new Error(`HALT behavior-env frontend-phase: ${verdict.rationale || reason}`) 1925 if (verdict.action !== 'retry') throw new Error(`HALT behavior-env frontend-phase: ${verdict.rationale || reason}`)
1893 attempt += 1 1926 attempt += 1
1894 - bg = await agent(behaviorGatePrompt(feItems, behaviorRound, attempt), 1927 + bg = await agentR(behaviorGatePrompt(feItems, behaviorRound, attempt),
1895 {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA}) 1928 {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA})
1896 recordDecisions('behavior:frontend-phase', bg.decisions) 1929 recordDecisions('behavior:frontend-phase', bg.decisions)
1897 envState = behaviorEnvBlocked(bg) 1930 envState = behaviorEnvBlocked(bg)
@@ -2045,7 +2078,7 @@ phase(&#39;Router&#39;) @@ -2045,7 +2078,7 @@ phase(&#39;Router&#39;)
2045 const ID_PATTERN = /^[A-Za-z0-9_-]+$/ 2078 const ID_PATTERN = /^[A-Za-z0-9_-]+$/
2046 function assertSafeId(kind, value) { 2079 function assertSafeId(kind, value) {
2047 if (typeof value !== 'string' || !ID_PATTERN.test(value)) { 2080 if (typeof value !== 'string' || !ID_PATTERN.test(value)) {
2048 - throw new Error(`HALT router-invalid-${kind}: ${JSON.stringify(value)}(必须匹配 /^[A-Za-z0-9_-]+$/,用于安全地拼入 git 命令)`) 2081 + throw haltError(`HALT router-invalid-${kind}: ${JSON.stringify(value)}(必须匹配 /^[A-Za-z0-9_-]+$/,用于安全地拼入 git 命令)`)
2049 } 2082 }
2050 } 2083 }
2051 function routerViolation(modules) { 2084 function routerViolation(modules) {
@@ -2059,16 +2092,16 @@ function routerViolation(modules) { @@ -2059,16 +2092,16 @@ function routerViolation(modules) {
2059 return null 2092 return null
2060 } 2093 }
2061 2094
2062 -let routed = await agent(routerPrompt(ROOT), {label:'router', phase:'Router', schema: ROUTER_SCHEMA}) 2095 +let routed = await agentR(routerPrompt(ROOT), {label:'router', phase:'Router', schema: ROUTER_SCHEMA})
2063 for (let adj = 1; adj <= ADJUDICATE_MAX; adj++) { 2096 for (let adj = 1; adj <= ADJUDICATE_MAX; adj++) {
2064 const violation = routerViolation(routed.modules) 2097 const violation = routerViolation(routed.modules)
2065 if (!violation) break 2098 if (!violation) break
2066 const verdict = await adjudicate('router-violation', { problem: violation }, 'Router', adj) 2099 const verdict = await adjudicate('router-violation', { problem: violation }, 'Router', adj)
2067 - if (verdict.action !== 'retry') throw new Error(`HALT router-violation: ${verdict.rationale || violation}`)  
2068 - routed = await agent(routerPrompt(ROOT) + adjGuidance(verdict.guidance || ''), {label:`router:r${adj + 1}`, phase:'Router', schema: ROUTER_SCHEMA}) 2100 + if (verdict.action !== 'retry') throw haltError(`HALT router-violation: ${verdict.rationale || violation}`)
  2101 + routed = await agentR(routerPrompt(ROOT) + adjGuidance(verdict.guidance || ''), {label:`router:r${adj + 1}`, phase:'Router', schema: ROUTER_SCHEMA})
2069 } 2102 }
2070 const finalViolation = routerViolation(routed.modules) 2103 const finalViolation = routerViolation(routed.modules)
2071 -if (finalViolation) throw new Error(`HALT router-violation: ${ADJUDICATE_MAX} 轮仲裁后仍违例:${finalViolation}`) 2104 +if (finalViolation) throw haltError(`HALT router-violation: ${ADJUDICATE_MAX} 轮仲裁后仍违例:${finalViolation}`)
2072 // id 安全护栏:最终选定的 routed 必须全部通过(assertSafeId 硬 halt)。 2105 // id 安全护栏:最终选定的 routed 必须全部通过(assertSafeId 硬 halt)。
2073 for (const m of routed.modules) { 2106 for (const m of routed.modules) {
2074 assertSafeId('module-id', m.id) 2107 assertSafeId('module-id', m.id)
@@ -2142,7 +2175,9 @@ for (const [idx, module] of todo.entries()) { @@ -2142,7 +2175,9 @@ for (const [idx, module] of todo.entries()) {
2142 ].join('\n')) 2175 ].join('\n'))
2143 } 2176 }
2144 } catch (e) { 2177 } catch (e) {
2145 - results.push({ module: module.id, status:'halted', reason: String(e.message||e) }) 2178 + const reason = String(e.message || e)
  2179 + log(`⛔ HALT — 模块 ${module.id}:${reason}`)
  2180 + results.push({ module: module.id, status:'halted', reason })
2146 haltedAtIdx = idx 2181 haltedAtIdx = idx
2147 break // 整阶段 fail-fast:halt 后停,等人工修复后重跑 coding-start 2182 break // 整阶段 fail-fast:halt 后停,等人工修复后重跑 coding-start
2148 } 2183 }
@@ -2187,6 +2222,9 @@ if (halted) { @@ -2187,6 +2222,9 @@ if (halted) {
2187 ].join('\n')) 2222 ].join('\n'))
2188 } 2223 }
2189 2224
  2225 +if (halted) log(`⛔ 本次运行 halt — 模块 ${halted.module};原因:${halted.reason || '(空)'};待跑:${pending.map(p => p.module).join('、') || '无'}`)
  2226 +else if (results.length) log(`✅ 本次运行完成 ${results.length} 个模块,无 halt`)
  2227 +
2190 // 注:顶层 `return` 不是普通 Node ESM 语法;本文件由 Claude Workflow 运行时执行, 2228 // 注:顶层 `return` 不是普通 Node ESM 语法;本文件由 Claude Workflow 运行时执行,
2191 // 运行时会把脚本体包进 async function,顶层 `return` 是 Workflow 的结果通道。 2229 // 运行时会把脚本体包进 async function,顶层 `return` 是 Workflow 的结果通道。
2192 // 不要把本文件作为 `node workflows/coding.mjs` 直接运行,也不要改成 `export default {...}`, 2230 // 不要把本文件作为 `node workflows/coding.mjs` 直接运行,也不要改成 `export default {...}`,