Commit 29f3a2dee329d4e8672c371d1dc6f0e75cdef168
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] 终止标记)。
Showing
2 changed files
with
107 additions
and
36 deletions
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 === 'string' ? (() => { try { return JSON.parse(args) } | @@ -198,7 +198,7 @@ const ARGS = typeof args === 'string' ? (() => { 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) => !!(r.envError && r.envError.kind === 'build-failed' | @@ -1865,7 +1898,7 @@ const isBuildFailed = (r) => !!(r.envError && r.envError.kind === 'build-failed' | ||
| 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('Router') | @@ -2045,7 +2078,7 @@ phase('Router') | ||
| 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 {...}`, |