From 29f3a2dee329d4e8672c371d1dc6f0e75cdef168 Mon Sep 17 00:00:00 2001 From: zichun <26684461+reporkey@users.noreply.github.com> Date: Thu, 11 Jun 2026 18:02:26 +0800 Subject: [PATCH] coding.mjs: halt 可见性——原因一律先 log 再抛 + agent() null 防御 + coding-start 步骤6 强制横幅 --- skills/coding/coding-start/SKILL.md | 33 +++++++++++++++++++++++++++++++++ workflows/coding.mjs | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------ 2 files changed, 107 insertions(+), 36 deletions(-) diff --git a/skills/coding/coding-start/SKILL.md b/skills/coding/coding-start/SKILL.md index ff598a5..2dd7e82 100644 --- a/skills/coding/coding-start/SKILL.md +++ b/skills/coding/coding-start/SKILL.md @@ -129,6 +129,39 @@ Workflow({ > `[ERP-HALT]` 标记供 Stop 钩子(`hooks/scripts/auto-continue.sh`)识别:Workflow 一旦转后台,主循环应停下等通知,**不**被自动续跑机制误推去重启 coding-start。逐 REQ 手动推进的会话则无此标记,由 Stop 钩子自动续跑,直到打印 `[ERP-HALT]`(全部完成 / 硬护栏)才停。 +### 步骤 6:Workflow 完成/halt 通知返回后——先输出最终状态横幅 + +Workflow 的 task-notification 返回本会话后,**第一段输出必须是下面的横幅**(之后才允许做任何诊断动作——读 RESUME.md、读 output 文件、查代码都排在横幅之后),保证用户在终端第一眼看到 halt 原因全文: + +- 结果含 `status:'halted'` 模块: + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + [coding-start] ⛔ Coding Workflow HALT + + 模块: + 原因: + 已完成: + 未跑(pending): + + 下一步:按原因修复后重跑 /erp-workflow:coding-start(Router 按 tag 续跑)。 + [ERP-HALT] 已停在硬护栏点。 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +- 全部完成: + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + [coding-start] ✅ Coding 阶段全部完成 + + 完成模块:<列表> + 自主默认决策:<条数> 条(详见 docs/superpowers/RESUME.md 末尾条目,建议复核) + + [ERP-HALT] 编码阶段结束。 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + 本 skill 到此结束,**不调用任何下游 skill**。 ## 参考 diff --git a/workflows/coding.mjs b/workflows/coding.mjs index 6eb9194..8e605a9 100644 --- a/workflows/coding.mjs +++ b/workflows/coding.mjs @@ -198,7 +198,7 @@ const ARGS = typeof args === 'string' ? (() => { try { return JSON.parse(args) } const ROOT = ARGS?.projectRoot || '.' // ROOT 必须是绝对路径——相对 '.' 会绑定到子代理隐式 cwd,无保证。 if (ROOT === '.' || !(/^(?:\/|[A-Za-z]:[\\/])/.test(ROOT))) { - throw new Error(`HALT invalid-projectRoot: must be absolute, got ${JSON.stringify(ROOT)}. coding-start 必须把绝对路径传入 args.projectRoot。`) + throw haltError(`HALT invalid-projectRoot: must be absolute, got ${JSON.stringify(ROOT)}. coding-start 必须把绝对路径传入 args.projectRoot。`) } // 插件根(coding-start 透传):用于调用插件 lib/*.mjs(如 req-ledger 建需求台账基线)。 // 缺省(旧 coding-start 未传)则相关增强静默跳过——由 /add-req 首跑兜底建基线。 @@ -919,6 +919,7 @@ const ADJUDICATE_MAX = 3 // 单个 site 的仲裁轮上限 // - BEHAVIOR_ATTEMPT_MAX = 单个 behaviorRound 内的环境 race 重起上限(沿用 testGate attempt 1→2 思路)。 const BEHAVIOR_STAGE_MAX = 3 const BEHAVIOR_ATTEMPT_MAX = 2 +const NULL_AGENT_PROBLEM = 'agent() 返回 null(子代理终端 API 错误 / 被用户跳过)——典型为断网或机器休眠;可重试' const adjGuidance = (g) => g ? `\n\n## 仲裁返回的纠正指令(本次重跑必须遵守)\n${g}` : '' // 全流程自主决策日志:stage 缺值时不停而是挑默认/解读,登记在此,随结果回传供人工事后审阅。 @@ -932,6 +933,21 @@ function recordDecisions(site, decisions) { } } +// halt 可见性:HALT 在抛出前先 log(出现在 /workflows 进度叙述行与 transcript), +// 保证终端能直接看到 halt 原因,而不是只埋在 Workflow 返回值 / RESUME.md 里。 +// 顶层 function 声明会提升,:201 等早于本定义的调用点也可用。 +function haltError(reason) { + log(`⛔ ${reason}`) + return new Error(reason) +} + +// 直调 agent() 的非 runner 点位统一防御:null(断网/休眠/被跳过)→ 带诊断的 halt,而非下游 TypeError。 +async function agentR(prompt, opts) { + const r = await agent(prompt, opts) + if (!r) throw haltError(`HALT agent-null ${opts?.label || '?'}: ${NULL_AGENT_PROBLEM}`) + return r +} + function adjudicatePromptM(site, context) { const ctx = typeof context === 'string' ? context : JSON.stringify(context, null, 2) return [ @@ -960,6 +976,10 @@ function adjudicatePromptM(site, context) { async function adjudicate(site, context, grp, round) { const verdict = await agent(adjudicatePromptM(site, context), {label:`adjudicate:${site}:r${round}`, phase: grp, schema: ADJUDICATE_SCHEMA}) + if (!verdict) { // 仲裁子代理自身遭遇终端 API 错误 → 默认 retry(确定性,防 null 级联) + log(`adjudicate ${site} r${round}: 仲裁子代理无返回(终端 API 错误/被跳过),默认 retry`) + return { action: 'retry', guidance: '', rationale: 'adjudicate agent 无返回,默认 retry' } + } log(`adjudicate ${site} r${round}: ${verdict.action}${verdict.rationale ? ' — ' + verdict.rationale : ''}`) return verdict } @@ -974,6 +994,12 @@ async function runStage(makePrompt, { site, grp, label, validate, allowContinue let guidance = '' for (let round = 1; round <= ADJUDICATE_MAX; round++) { const res = await agent(makePrompt(adjGuidance(guidance)), {label, phase: grp, schema: STAGE_RESULT_SCHEMA}) + if (!res) { + const verdict = await adjudicate(site, { problem: NULL_AGENT_PROBLEM, allowContinue: false }, grp, round) + if (verdict.action !== 'retry') throw haltError(`HALT ${site}: ${NULL_AGENT_PROBLEM}`) + guidance = verdict.guidance || '' + continue + } recordDecisions(site, res.decisions) let problem = null if (res.status === 'halt') problem = `stage 返回 status:halt;reason: ${res.reason || '(空)'}` @@ -993,6 +1019,12 @@ async function runAction(makePrompt, { site, grp, label, allowContinue = false } let guidance = '' for (let round = 1; round <= ADJUDICATE_MAX; round++) { const r = await agent(makePrompt(adjGuidance(guidance)), {label, phase: grp, schema: ACTION_RESULT_SCHEMA}) + if (!r) { + const verdict = await adjudicate(site, { problem: NULL_AGENT_PROBLEM, allowContinue: false }, grp, round) + if (verdict.action !== 'retry') throw haltError(`HALT ${site}: ${NULL_AGENT_PROBLEM}`) + guidance = verdict.guidance || '' + continue + } if (r.success) return r const verdict = await adjudicate(site, { problem:`action 失败:${r.error || ''}${r.detail ? '\n' + r.detail : ''}`, allowContinue }, grp, round) @@ -1489,18 +1521,18 @@ async function runBranchSetup(module) { const branch = fe ? 'frontend-phase' : `module-${id}` const lbl = (k) => `branch:${k}:${id}` - const def = await agent(detectDefaultBranchPromptM(), {label: lbl('default'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) + const def = await agentR(detectDefaultBranchPromptM(), {label: lbl('default'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) // 工作树脏:先自主恢复(in-scope 残留 → 自动 commit);含越界改动则恢复失败 → halt(留给人工)。 - const wt = await agent(worktreeCleanPromptM(), {label: lbl('wt'), phase: 'Milestone', schema: WT_SCHEMA}) + const wt = await agentR(worktreeCleanPromptM(), {label: lbl('wt'), phase: 'Milestone', schema: WT_SCHEMA}) if (!wt.clean) { - const rec = await agent(recoverDirtyWorktreePromptM(wt.dirty, branch, `分支 setup 前置(目标分支 ${branch})。`), + const rec = await agentR(recoverDirtyWorktreePromptM(wt.dirty, branch, `分支 setup 前置(目标分支 ${branch})。`), {label: lbl('wt-recover'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA}) if (!rec.success) throw new Error(`HALT branchSetup-dirty-worktree ${branch}: ${rec.error || ''}${rec.detail ? '\n' + rec.detail : ''}`) log(`branch-setup: ${id} 自动提交脏工作树残留(${rec.detail || ''})`) } - const exists = await agent(checkBranchExistsPromptM(branch), {label: lbl('exists?'), phase: 'Milestone', schema: EXISTS_SCHEMA}) + const exists = await agentR(checkBranchExistsPromptM(branch), {label: lbl('exists?'), phase: 'Milestone', schema: EXISTS_SCHEMA}) if (exists.exists) { await runAction(g => checkoutExistingBranchPromptM(branch) + g, {site:`branchSetup-checkout:${branch}`, grp:'Milestone', label: lbl('checkout')}) } else { @@ -1508,14 +1540,14 @@ async function runBranchSetup(module) { } // HEAD 确认:不符则经仲裁重切(retry)或留人工(halt)。 - let head = await agent(currentBranchPromptM(), {label: lbl('head'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) + let head = await agentR(currentBranchPromptM(), {label: lbl('head'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) for (let adj = 1; head.branch !== branch && adj <= ADJUDICATE_MAX; adj++) { const verdict = await adjudicate(`branchSetup-branch-mismatch:${branch}`, { problem:`分支 setup 后 HEAD 在 ${head.branch},期望 ${branch}` }, 'Milestone', adj) if (verdict.action !== 'retry') throw new Error(`HALT branchSetup-branch-mismatch ${branch}: ${verdict.rationale || `HEAD on ${head.branch}`}`) await runAction(g => checkoutExistingBranchPromptM(branch) + g, {site:`branchSetup-recheckout:${branch}`, grp:'Milestone', label: lbl('recheckout')}) - head = await agent(currentBranchPromptM(), {label: lbl('head'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) + head = await agentR(currentBranchPromptM(), {label: lbl('head'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) } if (head.branch !== branch) throw new Error(`HALT branchSetup-branch-mismatch ${branch}: ${ADJUDICATE_MAX} 轮后 HEAD 仍在 ${head.branch}`) @@ -1533,32 +1565,32 @@ async function runMilestone(module) { const lbl = (k) => `milestone:${k}:${phaseId}` // step 1: worktree clean precondition(脏树先自主恢复 in-scope 残留;含越界改动则 halt 留人工) - const wt = await agent(worktreeCleanPromptM(), {label: lbl('wt'), phase: 'Milestone', schema: WT_SCHEMA}) + const wt = await agentR(worktreeCleanPromptM(), {label: lbl('wt'), phase: 'Milestone', schema: WT_SCHEMA}) if (!wt.clean) { - const rec = await agent(recoverDirtyWorktreePromptM(wt.dirty, branch, `里程碑前置(阶段 ${phaseId},应在功能分支 ${branch})。`), + const rec = await agentR(recoverDirtyWorktreePromptM(wt.dirty, branch, `里程碑前置(阶段 ${phaseId},应在功能分支 ${branch})。`), {label: lbl('wt-recover'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA}) if (!rec.success) throw new Error(`HALT milestone-dirty-worktree ${phaseId}: ${rec.error || ''}${rec.detail ? '\n' + rec.detail : ''}`) log(`milestone: ${phaseId} 自动提交脏工作树残留(${rec.detail || ''})`) } // step 2: detect default branch - const def = await agent(detectDefaultBranchPromptM(), {label: lbl('default'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) + const def = await agentR(detectDefaultBranchPromptM(), {label: lbl('default'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) // step 3: merge (idempotent — skip if already an ancestor) // merge 冲突保持**硬 halt**:自动 abort/stash/改文件均不安全,把树留给人工(设计原则不变)。 - const merged = await agent(checkAlreadyMergedPromptM(branch, def.branch), {label: lbl('merged?'), phase: 'Milestone', schema: ALREADY_MERGED_SCHEMA}) + const merged = await agentR(checkAlreadyMergedPromptM(branch, def.branch), {label: lbl('merged?'), phase: 'Milestone', schema: ALREADY_MERGED_SCHEMA}) if (!merged.alreadyMerged) { - const r = await agent(executeMergePromptM(def.branch, branch, phaseId), {label: lbl('merge'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA}) + const r = await agentR(executeMergePromptM(def.branch, branch, phaseId), {label: lbl('merge'), phase: 'Milestone', schema: ACTION_RESULT_SCHEMA}) if (!r.success) throw new Error(`HALT milestone-merge ${phaseId}: ${r.error || ''}${r.detail ? '\n' + r.detail : ''}`) } // step 4: docs/08 field (idempotent — read first, only write if at initial '—') - let field = await agent(readDocs08FieldPromptM(fe, id), {label: lbl('field?'), phase: 'Milestone', schema: FIELD_VALUE_SCHEMA}) + let field = await agentR(readDocs08FieldPromptM(fe, id), {label: lbl('field?'), phase: 'Milestone', schema: FIELD_VALUE_SCHEMA}) for (let adj = 1; !field.found && adj <= ADJUDICATE_MAX; adj++) { const verdict = await adjudicate(`milestone-docs08-missing:${phaseId}`, { problem:`docs/08 ${fe ? '§ 三' : `§ 二 模块 ${id}`} 里程碑字段未找到` }, 'Milestone', adj) if (verdict.action !== 'retry') throw new Error(`HALT milestone-docs08-missing ${phaseId}: ${verdict.rationale || '字段不存在'}`) - field = await agent(readDocs08FieldPromptM(fe, id), {label: lbl('field?'), phase: 'Milestone', schema: FIELD_VALUE_SCHEMA}) + field = await agentR(readDocs08FieldPromptM(fe, id), {label: lbl('field?'), phase: 'Milestone', schema: FIELD_VALUE_SCHEMA}) } if (!field.found) throw new Error(`HALT milestone-docs08-missing ${phaseId}: ${ADJUDICATE_MAX} 轮仲裁后仍未找到字段`) if (field.value === '—') { @@ -1575,12 +1607,12 @@ async function runMilestone(module) { // step 5: report § ⑫ FIRST(关键顺序:tag 必须指向"§ ⑫ 已落地"的 commit,否则 // `git checkout milestone/` 看到的报告 § ⑫ 仍是 placeholder。原版顺序 tag → § ⑫ 是已知 bug, // 此处显式倒过来;下面 step 6 的 tag 才会指向新鲜 commit。) - let rpt = await agent(findReportPromptM(phaseId), {label: lbl('report?'), phase: 'Milestone', schema: REPORT_PATH_SCHEMA}) + let rpt = await agentR(findReportPromptM(phaseId), {label: lbl('report?'), phase: 'Milestone', schema: REPORT_PATH_SCHEMA}) for (let adj = 1; !rpt.found && adj <= ADJUDICATE_MAX; adj++) { const verdict = await adjudicate(`milestone-report-missing:${phaseId}`, { problem:`未找到匹配 docs/superpowers/module-reports/*-${phaseId}.md 的报告文件` }, 'Milestone', adj) if (verdict.action !== 'retry') throw new Error(`HALT milestone-report-missing ${phaseId}: ${verdict.rationale || '报告文件缺失'}`) - rpt = await agent(findReportPromptM(phaseId), {label: lbl('report?'), phase: 'Milestone', schema: REPORT_PATH_SCHEMA}) + rpt = await agentR(findReportPromptM(phaseId), {label: lbl('report?'), phase: 'Milestone', schema: REPORT_PATH_SCHEMA}) } if (!rpt.found) throw new Error(`HALT milestone-report-missing ${phaseId}: ${ADJUDICATE_MAX} 轮仲裁后仍无报告文件`) if (rpt.currentTagValue === '{{milestone_tag}}') { @@ -1595,7 +1627,7 @@ async function runMilestone(module) { // else: 已是 targetTag → 静默跳过(resume 幂等) // step 6: annotated tag (idempotent — tag exists 时静默跳过) - const tag = await agent(checkTagExistsPromptM(targetTag), {label: lbl('tag?'), phase: 'Milestone', schema: EXISTS_SCHEMA}) + const tag = await agentR(checkTagExistsPromptM(targetTag), {label: lbl('tag?'), phase: 'Milestone', schema: EXISTS_SCHEMA}) if (!tag.exists) { await runAction(g => createTagPromptM(phaseId, fe) + g, {site:`milestone-tag:${phaseId}`, grp:'Milestone', label: lbl('tag')}) } @@ -1609,15 +1641,15 @@ async function runCrossModule(module) { const id = module?.id ?? '' const lbl = (k) => `xmod:${k}:${id}` - const def = await agent(detectDefaultBranchPromptM(), {label: lbl('default'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) + const def = await agentR(detectDefaultBranchPromptM(), {label: lbl('default'), phase: 'Milestone', schema: DEFAULT_BRANCH_SCHEMA}) - const changed = await agent(collectCrossModuleChangedPromptM(def.branch), {label: lbl('diff'), phase: 'Milestone', schema: CHANGED_FILES_SCHEMA}) + const changed = await agentR(collectCrossModuleChangedPromptM(def.branch), {label: lbl('diff'), phase: 'Milestone', schema: CHANGED_FILES_SCHEMA}) if (!changed.files.length) { log(`cross-module-log: 模块 ${id} 无文件改动,跳过`) return } - const classified = await agent(classifyCrossModulePromptM(id, changed.files), {label: lbl('classify'), phase: 'Milestone', schema: CROSS_CLASSIFY_SCHEMA}) + const classified = await agentR(classifyCrossModulePromptM(id, changed.files), {label: lbl('classify'), phase: 'Milestone', schema: CROSS_CLASSIFY_SCHEMA}) if (!classified.crossModule.length) { log(`cross-module-log: 模块 ${id} 无跨模块改动,跳过`) return @@ -1636,7 +1668,7 @@ async function runFrontendSkeleton(feItems) { const lbl = (k) => `fe-skeleton:${k}` // step 1: 检查 router 是否已声明全 FE 路由;已建则只确保补记 tag 存在。 - const state = await agent(frontendSkeletonStatePromptM(feItems), + const state = await agentR(frontendSkeletonStatePromptM(feItems), {label: lbl('state?'), phase: 'Frontend', schema: EXISTS_SCHEMA}) if (state.exists) { log('fe-skeleton: router 已声明全部 FE 路由,确保 fe-skeleton-done tag 存在') @@ -1682,7 +1714,7 @@ async function featureLoop(items, phase) { const grp = phase === 'backend' ? 'Backend' : 'Frontend' for (const id of items) { // 入口 dedup:req-done/ 已存在 → 已 approve,整段 skip。 - const done = await agent(checkReqDoneTagPromptM(id), {label:`donecheck:${phase}:${id}`, phase: grp, schema: EXISTS_SCHEMA}) + const done = await agentR(checkReqDoneTagPromptM(id), {label:`donecheck:${phase}:${id}`, phase: grp, schema: EXISTS_SCHEMA}) if (done.exists) { log(`featureLoop skip ${phase}:${id} — tag req-done/${id} 已存在`); continue } const spec = await runStage(g => deriveSpecPrompt(id, phase) + g, { @@ -1743,11 +1775,12 @@ const REVIEW_HARD_ROUNDS = 10 // flipDocs08Checkbox:approve 后把功能行 [ ]→[x]。纯可视化;任何缺失/异常/写失败都降级为日志,绝不 halt。 async function flipDocs08Checkbox(fe, id, phase, grp) { const cb = await agent(readDocs08CheckboxPromptM(fe, id), {label:`cb?:${phase}:${id}`, phase: grp, schema: CHECKBOX_STATE_SCHEMA}) + if (!cb) { log(`docs08-checkbox ${id}: 读取子代理无返回(不阻断)`); return } if (!cb.found) { log(`docs08-checkbox ${phase}:${id}: 未找到功能行,跳过可视化勾选(req-done tag 仍是完成真值)`); return } if (cb.state === 'checked') return if (cb.state !== 'unchecked') { log(`docs08-checkbox ${phase}:${id}: state 异常 (${JSON.stringify(cb.state)}),跳过勾选`); return } const wr = await agent(writeDocs08CheckboxPromptM(fe, id, phase, cb.lineNumber), {label:`cb:${phase}:${id}`, phase: grp, schema: ACTION_RESULT_SCHEMA}) - if (!wr.success) log(`docs08-checkbox ${phase}:${id}: 勾选写入失败(${wr.error || ''}),跳过——cosmetic,不阻断`) + if (!wr || !wr.success) log(`docs08-checkbox ${phase}:${id}: 勾选写入失败(${(wr && wr.error) || ''}),跳过——cosmetic,不阻断`) } async function reviewWithFixLoop(id, phase, verifyResult, specPath) { @@ -1759,7 +1792,7 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) { for (let round = 1; round <= REVIEW_HARD_ROUNDS; round++) { const lastVerifySummary = (lastVerify && (lastVerify.summary || lastVerify.reason)) || '' // opts.phase = grp('Backend'/'Frontend')是 harness UI 分组;domain phase 见 agents/code-reviewer.md。 - const r = await agent( + const r = await agentR( reviewPrompt(id, phase, round, lastVerifySummary, specPath) + adjGuidance(reviewGuidance), {label:`review:${phase}:${id}:r${round}`, phase: grp, schema: REVIEW_SCHEMA, agentType:'erp-workflow:code-reviewer'} ) @@ -1819,10 +1852,10 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath) { // red 是硬正确性边界——**绝不** continue 跳过;只让仲裁在"再跑一次辨 flake"与"确属真失败 → halt"间裁决。 async function testGate(module, phase) { let attempt = 1 - let g = await agent(gatePrompt(module, phase, attempt), {label:`gate:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA}) + let g = await agentR(gatePrompt(module, phase, attempt), {label:`gate:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA}) if (g.status === 'red') { // 自动重试 1 次(防 flaky) attempt = 2 - g = await agent(gatePrompt(module, phase, attempt), {label:`gate-retry:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA}) + g = await agentR(gatePrompt(module, phase, attempt), {label:`gate-retry:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA}) } // 仍 red:经仲裁辨识 flake。allowContinue:false → 红色不可跳过,仲裁只在 retry / halt 间选。 for (let adj = 1; g.status === 'red' && adj <= ADJUDICATE_MAX; adj++) { @@ -1831,7 +1864,7 @@ async function testGate(module, phase) { if (verdict.action !== 'retry') throw new Error(`HALT test-gate-red ${phase}:${module.id}: ${verdict.rationale || (g.failures||[]).join('; ')}`) attempt += 1 // retry:再跑一个独立 attempt 证据文件 - g = await agent(gatePrompt(module, phase, attempt), {label:`gate-retry:${phase}:${module.id}:a${attempt}`, phase:'Gate', schema: GATE_SCHEMA}) + g = await agentR(gatePrompt(module, phase, attempt), {label:`gate-retry:${phase}:${module.id}:a${attempt}`, phase:'Gate', schema: GATE_SCHEMA}) } if (g.status === 'red') throw new Error(`HALT test-gate-red ${phase}:${module.id}: ${ADJUDICATE_MAX} 轮仲裁后仍 red:${(g.failures||[]).join('; ')}`) return g @@ -1865,7 +1898,7 @@ const isBuildFailed = (r) => !!(r.envError && r.envError.kind === 'build-failed' async function runBehaviorGateOnce(feItems, behaviorRound) { const lbl = (a) => `behavior:frontend-phase:r${behaviorRound}:a${a}` let attempt = 1 - let bg = await agent(behaviorGatePrompt(feItems, behaviorRound, attempt), + let bg = await agentR(behaviorGatePrompt(feItems, behaviorRound, attempt), {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA}) recordDecisions('behavior:frontend-phase', bg.decisions) @@ -1873,7 +1906,7 @@ async function runBehaviorGateOnce(feItems, behaviorRound) { // build-failed 是确定性失败(重起不自愈)→ 跳过自动重起,直接进下方仲裁循环。 while (behaviorEnvBlocked(bg).blocked && !isBuildFailed(bg) && attempt < BEHAVIOR_ATTEMPT_MAX) { attempt += 1 - bg = await agent(behaviorGatePrompt(feItems, behaviorRound, attempt), + bg = await agentR(behaviorGatePrompt(feItems, behaviorRound, attempt), {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA}) recordDecisions('behavior:frontend-phase', bg.decisions) } @@ -1891,7 +1924,7 @@ async function runBehaviorGateOnce(feItems, behaviorRound) { allowContinue:false }, 'Behavior', adj) if (verdict.action !== 'retry') throw new Error(`HALT behavior-env frontend-phase: ${verdict.rationale || reason}`) attempt += 1 - bg = await agent(behaviorGatePrompt(feItems, behaviorRound, attempt), + bg = await agentR(behaviorGatePrompt(feItems, behaviorRound, attempt), {label: lbl(attempt), phase: 'Behavior', schema: BEHAVIOR_GATE_SCHEMA}) recordDecisions('behavior:frontend-phase', bg.decisions) envState = behaviorEnvBlocked(bg) @@ -2045,7 +2078,7 @@ phase('Router') const ID_PATTERN = /^[A-Za-z0-9_-]+$/ function assertSafeId(kind, value) { if (typeof value !== 'string' || !ID_PATTERN.test(value)) { - throw new Error(`HALT router-invalid-${kind}: ${JSON.stringify(value)}(必须匹配 /^[A-Za-z0-9_-]+$/,用于安全地拼入 git 命令)`) + throw haltError(`HALT router-invalid-${kind}: ${JSON.stringify(value)}(必须匹配 /^[A-Za-z0-9_-]+$/,用于安全地拼入 git 命令)`) } } function routerViolation(modules) { @@ -2059,16 +2092,16 @@ function routerViolation(modules) { return null } -let routed = await agent(routerPrompt(ROOT), {label:'router', phase:'Router', schema: ROUTER_SCHEMA}) +let routed = await agentR(routerPrompt(ROOT), {label:'router', phase:'Router', schema: ROUTER_SCHEMA}) for (let adj = 1; adj <= ADJUDICATE_MAX; adj++) { const violation = routerViolation(routed.modules) if (!violation) break const verdict = await adjudicate('router-violation', { problem: violation }, 'Router', adj) - if (verdict.action !== 'retry') throw new Error(`HALT router-violation: ${verdict.rationale || violation}`) - routed = await agent(routerPrompt(ROOT) + adjGuidance(verdict.guidance || ''), {label:`router:r${adj + 1}`, phase:'Router', schema: ROUTER_SCHEMA}) + if (verdict.action !== 'retry') throw haltError(`HALT router-violation: ${verdict.rationale || violation}`) + routed = await agentR(routerPrompt(ROOT) + adjGuidance(verdict.guidance || ''), {label:`router:r${adj + 1}`, phase:'Router', schema: ROUTER_SCHEMA}) } const finalViolation = routerViolation(routed.modules) -if (finalViolation) throw new Error(`HALT router-violation: ${ADJUDICATE_MAX} 轮仲裁后仍违例:${finalViolation}`) +if (finalViolation) throw haltError(`HALT router-violation: ${ADJUDICATE_MAX} 轮仲裁后仍违例:${finalViolation}`) // id 安全护栏:最终选定的 routed 必须全部通过(assertSafeId 硬 halt)。 for (const m of routed.modules) { assertSafeId('module-id', m.id) @@ -2142,7 +2175,9 @@ for (const [idx, module] of todo.entries()) { ].join('\n')) } } catch (e) { - results.push({ module: module.id, status:'halted', reason: String(e.message||e) }) + const reason = String(e.message || e) + log(`⛔ HALT — 模块 ${module.id}:${reason}`) + results.push({ module: module.id, status:'halted', reason }) haltedAtIdx = idx break // 整阶段 fail-fast:halt 后停,等人工修复后重跑 coding-start } @@ -2187,6 +2222,9 @@ if (halted) { ].join('\n')) } +if (halted) log(`⛔ 本次运行 halt — 模块 ${halted.module};原因:${halted.reason || '(空)'};待跑:${pending.map(p => p.module).join('、') || '无'}`) +else if (results.length) log(`✅ 本次运行完成 ${results.length} 个模块,无 halt`) + // 注:顶层 `return` 不是普通 Node ESM 语法;本文件由 Claude Workflow 运行时执行, // 运行时会把脚本体包进 async function,顶层 `return` 是 Workflow 的结果通道。 // 不要把本文件作为 `node workflows/coding.mjs` 直接运行,也不要改成 `export default {...}`, -- libgit2 0.22.2