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 | 129 | |
| 130 | 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 | 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 | 198 | const ROOT = ARGS?.projectRoot || '.' |
| 199 | 199 | // ROOT 必须是绝对路径——相对 '.' 会绑定到子代理隐式 cwd,无保证。 |
| 200 | 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 | 203 | // 插件根(coding-start 透传):用于调用插件 lib/*.mjs(如 req-ledger 建需求台账基线)。 |
| 204 | 204 | // 缺省(旧 coding-start 未传)则相关增强静默跳过——由 /add-req 首跑兜底建基线。 |
| ... | ... | @@ -919,6 +919,7 @@ const ADJUDICATE_MAX = 3 // 单个 site 的仲裁轮上限 |
| 919 | 919 | // - BEHAVIOR_ATTEMPT_MAX = 单个 behaviorRound 内的环境 race 重起上限(沿用 testGate attempt 1→2 思路)。 |
| 920 | 920 | const BEHAVIOR_STAGE_MAX = 3 |
| 921 | 921 | const BEHAVIOR_ATTEMPT_MAX = 2 |
| 922 | +const NULL_AGENT_PROBLEM = 'agent() 返回 null(子代理终端 API 错误 / 被用户跳过)——典型为断网或机器休眠;可重试' | |
| 922 | 923 | const adjGuidance = (g) => g ? `\n\n## 仲裁返回的纠正指令(本次重跑必须遵守)\n${g}` : '' |
| 923 | 924 | |
| 924 | 925 | // 全流程自主决策日志:stage 缺值时不停而是挑默认/解读,登记在此,随结果回传供人工事后审阅。 |
| ... | ... | @@ -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 | 951 | function adjudicatePromptM(site, context) { |
| 936 | 952 | const ctx = typeof context === 'string' ? context : JSON.stringify(context, null, 2) |
| 937 | 953 | return [ |
| ... | ... | @@ -960,6 +976,10 @@ function adjudicatePromptM(site, context) { |
| 960 | 976 | async function adjudicate(site, context, grp, round) { |
| 961 | 977 | const verdict = await agent(adjudicatePromptM(site, context), |
| 962 | 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 | 983 | log(`adjudicate ${site} r${round}: ${verdict.action}${verdict.rationale ? ' — ' + verdict.rationale : ''}`) |
| 964 | 984 | return verdict |
| 965 | 985 | } |
| ... | ... | @@ -974,6 +994,12 @@ async function runStage(makePrompt, { site, grp, label, validate, allowContinue |
| 974 | 994 | let guidance = '' |
| 975 | 995 | for (let round = 1; round <= ADJUDICATE_MAX; round++) { |
| 976 | 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 | 1003 | recordDecisions(site, res.decisions) |
| 978 | 1004 | let problem = null |
| 979 | 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 | 1019 | let guidance = '' |
| 994 | 1020 | for (let round = 1; round <= ADJUDICATE_MAX; round++) { |
| 995 | 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 | 1028 | if (r.success) return r |
| 997 | 1029 | const verdict = await adjudicate(site, |
| 998 | 1030 | { problem:`action 失败:${r.error || ''}${r.detail ? '\n' + r.detail : ''}`, allowContinue }, grp, round) |
| ... | ... | @@ -1489,18 +1521,18 @@ async function runBranchSetup(module) { |
| 1489 | 1521 | const branch = fe ? 'frontend-phase' : `module-${id}` |
| 1490 | 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 | 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 | 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 | 1530 | {label: lbl('wt-recover'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA}) |
| 1499 | 1531 | if (!rec.success) throw new Error(`HALT branchSetup-dirty-worktree ${branch}: ${rec.error || ''}${rec.detail ? '\n' + rec.detail : ''}`) |
| 1500 | 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 | 1536 | if (exists.exists) { |
| 1505 | 1537 | await runAction(g => checkoutExistingBranchPromptM(branch) + g, {site:`branchSetup-checkout:${branch}`, grp:'Milestone', label: lbl('checkout')}) |
| 1506 | 1538 | } else { |
| ... | ... | @@ -1508,14 +1540,14 @@ async function runBranchSetup(module) { |
| 1508 | 1540 | } |
| 1509 | 1541 | |
| 1510 | 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 | 1544 | for (let adj = 1; head.branch !== branch && adj <= ADJUDICATE_MAX; adj++) { |
| 1513 | 1545 | const verdict = await adjudicate(`branchSetup-branch-mismatch:${branch}`, |
| 1514 | 1546 | { problem:`分支 setup 后 HEAD 在 ${head.branch},期望 ${branch}` }, 'Milestone', adj) |
| 1515 | 1547 | if (verdict.action !== 'retry') |
| 1516 | 1548 | throw new Error(`HALT branchSetup-branch-mismatch ${branch}: ${verdict.rationale || `HEAD on ${head.branch}`}`) |
| 1517 | 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 | 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 | 1565 | const lbl = (k) => `milestone:${k}:${phaseId}` |
| 1534 | 1566 | |
| 1535 | 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 | 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 | 1571 | {label: lbl('wt-recover'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA}) |
| 1540 | 1572 | if (!rec.success) throw new Error(`HALT milestone-dirty-worktree ${phaseId}: ${rec.error || ''}${rec.detail ? '\n' + rec.detail : ''}`) |
| 1541 | 1573 | log(`milestone: ${phaseId} 自动提交脏工作树残留(${rec.detail || ''})`) |
| 1542 | 1574 | } |
| 1543 | 1575 | |
| 1544 | 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 | 1579 | // step 3: merge (idempotent — skip if already an ancestor) |
| 1548 | 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 | 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 | 1584 | if (!r.success) throw new Error(`HALT milestone-merge ${phaseId}: ${r.error || ''}${r.detail ? '\n' + r.detail : ''}`) |
| 1553 | 1585 | } |
| 1554 | 1586 | |
| 1555 | 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 | 1589 | for (let adj = 1; !field.found && adj <= ADJUDICATE_MAX; adj++) { |
| 1558 | 1590 | const verdict = await adjudicate(`milestone-docs08-missing:${phaseId}`, |
| 1559 | 1591 | { problem:`docs/08 ${fe ? '§ 三' : `§ 二 模块 ${id}`} 里程碑字段未找到` }, 'Milestone', adj) |
| 1560 | 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 | 1595 | if (!field.found) throw new Error(`HALT milestone-docs08-missing ${phaseId}: ${ADJUDICATE_MAX} 轮仲裁后仍未找到字段`) |
| 1564 | 1596 | if (field.value === '—') { |
| ... | ... | @@ -1575,12 +1607,12 @@ async function runMilestone(module) { |
| 1575 | 1607 | // step 5: report § ⑫ FIRST(关键顺序:tag 必须指向"§ ⑫ 已落地"的 commit,否则 |
| 1576 | 1608 | // `git checkout milestone/<id>` 看到的报告 § ⑫ 仍是 placeholder。原版顺序 tag → § ⑫ 是已知 bug, |
| 1577 | 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 | 1611 | for (let adj = 1; !rpt.found && adj <= ADJUDICATE_MAX; adj++) { |
| 1580 | 1612 | const verdict = await adjudicate(`milestone-report-missing:${phaseId}`, |
| 1581 | 1613 | { problem:`未找到匹配 docs/superpowers/module-reports/*-${phaseId}.md 的报告文件` }, 'Milestone', adj) |
| 1582 | 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 | 1617 | if (!rpt.found) throw new Error(`HALT milestone-report-missing ${phaseId}: ${ADJUDICATE_MAX} 轮仲裁后仍无报告文件`) |
| 1586 | 1618 | if (rpt.currentTagValue === '{{milestone_tag}}') { |
| ... | ... | @@ -1595,7 +1627,7 @@ async function runMilestone(module) { |
| 1595 | 1627 | // else: 已是 targetTag → 静默跳过(resume 幂等) |
| 1596 | 1628 | |
| 1597 | 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 | 1631 | if (!tag.exists) { |
| 1600 | 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 | 1641 | const id = module?.id ?? '<module>' |
| 1610 | 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 | 1647 | if (!changed.files.length) { |
| 1616 | 1648 | log(`cross-module-log: 模块 ${id} 无文件改动,跳过`) |
| 1617 | 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 | 1653 | if (!classified.crossModule.length) { |
| 1622 | 1654 | log(`cross-module-log: 模块 ${id} 无跨模块改动,跳过`) |
| 1623 | 1655 | return |
| ... | ... | @@ -1636,7 +1668,7 @@ async function runFrontendSkeleton(feItems) { |
| 1636 | 1668 | const lbl = (k) => `fe-skeleton:${k}` |
| 1637 | 1669 | |
| 1638 | 1670 | // step 1: 检查 router 是否已声明全 FE 路由;已建则只确保补记 tag 存在。 |
| 1639 | - const state = await agent(frontendSkeletonStatePromptM(feItems), | |
| 1671 | + const state = await agentR(frontendSkeletonStatePromptM(feItems), | |
| 1640 | 1672 | {label: lbl('state?'), phase: 'Frontend', schema: EXISTS_SCHEMA}) |
| 1641 | 1673 | if (state.exists) { |
| 1642 | 1674 | log('fe-skeleton: router 已声明全部 FE 路由,确保 fe-skeleton-done tag 存在') |
| ... | ... | @@ -1682,7 +1714,7 @@ async function featureLoop(items, phase) { |
| 1682 | 1714 | const grp = phase === 'backend' ? 'Backend' : 'Frontend' |
| 1683 | 1715 | for (const id of items) { |
| 1684 | 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 | 1718 | if (done.exists) { log(`featureLoop skip ${phase}:${id} — tag req-done/${id} 已存在`); continue } |
| 1687 | 1719 | |
| 1688 | 1720 | const spec = await runStage(g => deriveSpecPrompt(id, phase) + g, { |
| ... | ... | @@ -1743,11 +1775,12 @@ const REVIEW_HARD_ROUNDS = 10 |
| 1743 | 1775 | // flipDocs08Checkbox:approve 后把功能行 [ ]→[x]。纯可视化;任何缺失/异常/写失败都降级为日志,绝不 halt。 |
| 1744 | 1776 | async function flipDocs08Checkbox(fe, id, phase, grp) { |
| 1745 | 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 | 1779 | if (!cb.found) { log(`docs08-checkbox ${phase}:${id}: 未找到功能行,跳过可视化勾选(req-done tag 仍是完成真值)`); return } |
| 1747 | 1780 | if (cb.state === 'checked') return |
| 1748 | 1781 | if (cb.state !== 'unchecked') { log(`docs08-checkbox ${phase}:${id}: state 异常 (${JSON.stringify(cb.state)}),跳过勾选`); return } |
| 1749 | 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 | 1786 | async function reviewWithFixLoop(id, phase, verifyResult, specPath) { |
| ... | ... | @@ -1759,7 +1792,7 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) { |
| 1759 | 1792 | for (let round = 1; round <= REVIEW_HARD_ROUNDS; round++) { |
| 1760 | 1793 | const lastVerifySummary = (lastVerify && (lastVerify.summary || lastVerify.reason)) || '' |
| 1761 | 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 | 1796 | reviewPrompt(id, phase, round, lastVerifySummary, specPath) + adjGuidance(reviewGuidance), |
| 1764 | 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 | 1852 | // red 是硬正确性边界——**绝不** continue 跳过;只让仲裁在"再跑一次辨 flake"与"确属真失败 → halt"间裁决。 |
| 1820 | 1853 | async function testGate(module, phase) { |
| 1821 | 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 | 1856 | if (g.status === 'red') { // 自动重试 1 次(防 flaky) |
| 1824 | 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 | 1860 | // 仍 red:经仲裁辨识 flake。allowContinue:false → 红色不可跳过,仲裁只在 retry / halt 间选。 |
| 1828 | 1861 | for (let adj = 1; g.status === 'red' && adj <= ADJUDICATE_MAX; adj++) { |
| ... | ... | @@ -1831,7 +1864,7 @@ async function testGate(module, phase) { |
| 1831 | 1864 | if (verdict.action !== 'retry') |
| 1832 | 1865 | throw new Error(`HALT test-gate-red ${phase}:${module.id}: ${verdict.rationale || (g.failures||[]).join('; ')}`) |
| 1833 | 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 | 1869 | if (g.status === 'red') throw new Error(`HALT test-gate-red ${phase}:${module.id}: ${ADJUDICATE_MAX} 轮仲裁后仍 red:${(g.failures||[]).join('; ')}`) |
| 1837 | 1870 | return g |
| ... | ... | @@ -1865,7 +1898,7 @@ const isBuildFailed = (r) => !!(r.envError && r.envError.kind === 'build-failed' |
| 1865 | 1898 | async function runBehaviorGateOnce(feItems, behaviorRound) { |
| 1866 | 1899 | const lbl = (a) => `behavior:frontend-phase:r${behaviorRound}:a${a}` |
| 1867 | 1900 | let attempt = 1 |
| 1868 | - let bg = await agent(behaviorGatePrompt(feItems, behaviorRound, attempt), | |
| 1901 | + let bg = await agentR(behaviorGatePrompt(feItems, behaviorRound, attempt), | |
| 1869 | 1902 | {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA}) |
| 1870 | 1903 | recordDecisions('behavior:frontend-phase', bg.decisions) |
| 1871 | 1904 | |
| ... | ... | @@ -1873,7 +1906,7 @@ async function runBehaviorGateOnce(feItems, behaviorRound) { |
| 1873 | 1906 | // build-failed 是确定性失败(重起不自愈)→ 跳过自动重起,直接进下方仲裁循环。 |
| 1874 | 1907 | while (behaviorEnvBlocked(bg).blocked && !isBuildFailed(bg) && attempt < BEHAVIOR_ATTEMPT_MAX) { |
| 1875 | 1908 | attempt += 1 |
| 1876 | - bg = await agent(behaviorGatePrompt(feItems, behaviorRound, attempt), | |
| 1909 | + bg = await agentR(behaviorGatePrompt(feItems, behaviorRound, attempt), | |
| 1877 | 1910 | {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA}) |
| 1878 | 1911 | recordDecisions('behavior:frontend-phase', bg.decisions) |
| 1879 | 1912 | } |
| ... | ... | @@ -1891,7 +1924,7 @@ async function runBehaviorGateOnce(feItems, behaviorRound) { |
| 1891 | 1924 | allowContinue:false }, 'Behavior', adj) |
| 1892 | 1925 | if (verdict.action !== 'retry') throw new Error(`HALT behavior-env frontend-phase: ${verdict.rationale || reason}`) |
| 1893 | 1926 | attempt += 1 |
| 1894 | - bg = await agent(behaviorGatePrompt(feItems, behaviorRound, attempt), | |
| 1927 | + bg = await agentR(behaviorGatePrompt(feItems, behaviorRound, attempt), | |
| 1895 | 1928 | {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA}) |
| 1896 | 1929 | recordDecisions('behavior:frontend-phase', bg.decisions) |
| 1897 | 1930 | envState = behaviorEnvBlocked(bg) |
| ... | ... | @@ -2045,7 +2078,7 @@ phase('Router') |
| 2045 | 2078 | const ID_PATTERN = /^[A-Za-z0-9_-]+$/ |
| 2046 | 2079 | function assertSafeId(kind, value) { |
| 2047 | 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 | 2084 | function routerViolation(modules) { |
| ... | ... | @@ -2059,16 +2092,16 @@ function routerViolation(modules) { |
| 2059 | 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 | 2096 | for (let adj = 1; adj <= ADJUDICATE_MAX; adj++) { |
| 2064 | 2097 | const violation = routerViolation(routed.modules) |
| 2065 | 2098 | if (!violation) break |
| 2066 | 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 | 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 | 2105 | // id 安全护栏:最终选定的 routed 必须全部通过(assertSafeId 硬 halt)。 |
| 2073 | 2106 | for (const m of routed.modules) { |
| 2074 | 2107 | assertSafeId('module-id', m.id) |
| ... | ... | @@ -2142,7 +2175,9 @@ for (const [idx, module] of todo.entries()) { |
| 2142 | 2175 | ].join('\n')) |
| 2143 | 2176 | } |
| 2144 | 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 | 2181 | haltedAtIdx = idx |
| 2147 | 2182 | break // 整阶段 fail-fast:halt 后停,等人工修复后重跑 coding-start |
| 2148 | 2183 | } |
| ... | ... | @@ -2187,6 +2222,9 @@ if (halted) { |
| 2187 | 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 | 2228 | // 注:顶层 `return` 不是普通 Node ESM 语法;本文件由 Claude Workflow 运行时执行, |
| 2191 | 2229 | // 运行时会把脚本体包进 async function,顶层 `return` 是 Workflow 的结果通道。 |
| 2192 | 2230 | // 不要把本文件作为 `node workflows/coding.mjs` 直接运行,也不要改成 `export default {...}`, | ... | ... |