From ad94075f1112a5117b1c28a2ef1e161dba74e45d Mon Sep 17 00:00:00 2001 From: zichun <26684461+reporkey@users.noreply.github.com> Date: Thu, 11 Jun 2026 18:03:18 +0800 Subject: [PATCH] coding.mjs/add-req: 审计简化收敛——bestEffortAction、RESUME 去双写、单次 scan --- skills/plan/add-req/SKILL.md | 10 +++------- workflows/coding.mjs | 48 +++++++++++++++++++++++++++--------------------- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/skills/plan/add-req/SKILL.md b/skills/plan/add-req/SKILL.md index 5f03ad2..d34336c 100644 --- a/skills/plan/add-req/SKILL.md +++ b/skills/plan/add-req/SKILL.md @@ -47,15 +47,11 @@ node ${CLAUDE_PLUGIN_ROOT}/lib/req-ledger.mjs scan [ERP-HALT] 基线已建立,等待人工追加需求。 ``` (理由:首跑若把存量全当新增,会重复生成已有的 docs/03/05 工件并误重跑整仓。基线建立后,后续改动才能被准确 diff。) -- `true` → 进入步骤 1。 +- `true` → 直接用本次 scan 的输出进入步骤 1 解析(不再重跑 scan)。 ## 步骤 1:检测增量 -``` -node ${CLAUDE_PLUGIN_ROOT}/lib/req-ledger.mjs scan -``` - -解析 `new[]` / `changed[]` / `removed[]`(每项 `{id, kind}`,kind = `req` 后端卡片 / `fe` 前端功能行)。 +用步骤 0 已得到的 scan 输出解析 `new[]` / `changed[]` / `removed[]`(每项 `{id, kind}`,kind = `req` 后端卡片 / `fe` 前端功能行)。 - `new` 与 `changed` 均为空 → 打印 `[add-req] 无新增 / 变更需求,无需处理。[ERP-HALT]` **停下**(不提交台账)。 - `removed` 非空 → **仅提示、不自动删**:打印 `检测到台账登记但 docs 已移除的单元:<列出>。下线需求请人工同步 docs/02/03/05 并删除对应 tag,本 skill 不自动执行删除。` 然后继续处理 new/changed(removed 不写回台账,留待人工,下次仍会提示)。 @@ -126,7 +122,7 @@ node ${CLAUDE_PLUGIN_ROOT}/lib/req-ledger.mjs scan git -C commit -m "plan(add-req): 增量需求 <新增/变更 id 摘要>(docs/03+05+02+08 delta + V_n + 台账)" ``` - 覆盖范围 = 步骤 2~5 实际改动的全部文件:人工新写的 REQ 卡(docs/01)、新增 migration(sql/migrations/V_n)、同步的 docs/03、补的 docs/05/02 端点与顺序、docs/08 模块行/FE 行与复位的里程碑字段、`.req-ledger.json`。 - - 步骤 5 的 `git tag -d`(删 req-done/milestone)是对 ref 的操作,不产生工作树文件,不需进本 commit;但删 tag 应在本次 commit **之前或之后**都可(与文件提交独立)。 + - 步骤 5 的 `git tag -d`(删 req-done/milestone)是对 ref 的操作,不产生工作树文件,不需进本 commit。 - commit 后用 `git -C status --porcelain` 复核工作树**干净**(无残留);若仍有未跟踪/未提交项,排查后补提交,确保交给 coding-start 时是干净树。 3. 然后打印横幅并**停下**(不自动进编码,与「Plan 完不自动进 B」一致): diff --git a/workflows/coding.mjs b/workflows/coding.mjs index 029f7b5..f68ee14 100644 --- a/workflows/coding.mjs +++ b/workflows/coding.mjs @@ -924,6 +924,9 @@ const adjGuidance = (g) => g ? `\n\n## 仲裁返回的纠正指令(本次重 // 全流程自主决策日志:stage 缺值时不停而是挑默认/解读,登记在此,随结果回传供人工事后审阅。 const autonomousDecisions = [] +const decisionDigestMd = (list, emptyText) => list.length + ? list.map(d => ` - [\`${d.site}\`] ${d.question || '?'} → ${d.choice || '?'}(${d.confidence || '?'})`).join('\n') + : ` - ${emptyText}` function recordDecisions(site, decisions) { if (!Array.isArray(decisions)) return for (const d of decisions) { @@ -1036,6 +1039,18 @@ async function runAction(makePrompt, { site, grp, label, allowContinue = false } throw new Error(`HALT ${site}-adjudication-exhausted: ${ADJUDICATE_MAX} 轮仲裁仍未解决`) } +// best-effort 微步骤统一包裹:失败/异常只 log 绝不阻断主流程。返回是否成功(供水位线推进判断)。// ≠ runAction:本 helper 不经 adjudicate、绝不 halt。 +async function bestEffortAction(prompt, { label, phase: ph, okMsg, failTag, errTag = failTag }) { + try { + const r = await agent(prompt, { label, phase: ph, schema: ACTION_RESULT_SCHEMA }) + if (r && r.success) { log(okMsg); return true } + log(`${failTag}失败(不阻断):${(r && r.error) || ''}`) + } catch (e) { + log(`${errTag}异常(不阻断):${String(e?.message || e)}`) + } + return false +} + // ── 续跑 handoff(RESUME.md):halt / 全完成时追加一条跨运行记录 ───────────── // 进度真值仍是 git tag(milestone/req-done)+ 已 commit 的 module-reports / specs 工件—— // 这些已让硬中断后 Router 正确续跑、per-feature 决策也落在工件里。RESUME.md 补的是 @@ -1064,11 +1079,8 @@ function resumeJournalPromptM(sectionMd) { } async function recordResume(sectionMd) { // best-effort:续跑日志写失败绝不阻断主流程(它为 resume 而存在,不该反过来制造 halt)。 - try { - const r = await agent(resumeJournalPromptM(sectionMd), { label:'resume-journal', phase:'Milestone', schema: ACTION_RESULT_SCHEMA }) - if (r && r.success) log('resume-journal 已追加 RESUME.md') - else log(`resume-journal 写入失败(不阻断):${(r && r.error) || ''}`) - } catch (e) { log(`resume-journal 异常(不阻断):${String(e?.message || e)}`) } + return bestEffortAction(resumeJournalPromptM(sectionMd), + { label: 'resume-journal', phase: 'Milestone', okMsg: 'resume-journal 已追加 RESUME.md', failTag: 'resume-journal 写入', errTag: 'resume-journal ' }) } // ── 需求台账基线(P1#3):coding 首跑时若 .req-ledger.json 缺失,自动建立并提交 ────── @@ -1091,11 +1103,8 @@ function ledgerBaselinePromptM() { } async function ensureLedgerBaseline() { if (!PLUGIN) { log('req-ledger 基线跳过:未透传 pluginRoot(由 /add-req 首跑兜底)'); return } - try { - const r = await agent(ledgerBaselinePromptM(), { label:'req-ledger-baseline', phase:'Router', schema: ACTION_RESULT_SCHEMA }) - if (r && r.success) log('req-ledger 基线就绪') - else log(`req-ledger 基线失败(不阻断):${(r && r.error) || ''}`) - } catch (e) { log(`req-ledger 基线异常(不阻断):${String(e?.message || e)}`) } + return bestEffortAction(ledgerBaselinePromptM(), + { label: 'req-ledger-baseline', phase: 'Router', okMsg: 'req-ledger 基线就绪', failTag: 'req-ledger 基线' }) } function preflightPromptM() { return [ @@ -2141,6 +2150,7 @@ if (todo.length) { const results = [] let haltedAtIdx = -1 +let flushedCount = 0 for (const [idx, module] of todo.entries()) { const decStart = autonomousDecisions.length // 本模块自主决策水位线(P1#1 增量 flush 用) try { @@ -2187,16 +2197,14 @@ for (const [idx, module] of todo.entries()) { // P1#1 增量 flush:每个模块里程碑落定即追加 RESUME.md,使**硬中断**(进程被杀,到不了主循环末尾) // 也能复盘已完成到哪、各模块做过哪些自主假设——不必等 loop-end 才写。best-effort,绝不阻断。 { - const md = autonomousDecisions.slice(decStart) - const digest = md.length - ? md.map(d => ` - [\`${d.site}\`] ${d.question || '?'} → ${d.choice || '?'}(${d.confidence || '?'})`).join('\n') - : ' - (本模块无自主默认记录)' - await recordResume([ + const moduleDecisions = autonomousDecisions.slice(decStart) + const ok = await recordResume([ '## ✅ 模块完成 `' + module.id + '` → milestone/' + module.id + '()', '', '- **本模块自主默认决策**(缺值时自动取的解读,可能含错误假设):', - digest, + decisionDigestMd(moduleDecisions, '(本模块无自主默认记录)'), ].join('\n')) + if (ok) flushedCount = autonomousDecisions.length // 仅成功才推进水位线:flush 失败时 loop-end 兜底补记 } } catch (e) { const reason = String(e.message || e) @@ -2221,16 +2229,14 @@ const pending = haltedAtIdx >= 0 // 使下次 coding-start 重跑能复盘「上次为何 halt / 做过哪些自主假设 / 还剩哪些模块」。 // 硬中断(进程被杀,到不了这里)时不写——那种情况无 halt 原因可记,且 tag+工件已够 Router 续跑。 const halted = results.find(r => r.status === 'halted') -const decDigest = autonomousDecisions.length - ? autonomousDecisions.map(d => ` - [\`${d.site}\`] ${d.question || '?'} → ${d.choice || '?'}(${d.confidence || '?'})`).join('\n') - : ' - (本次运行无自主默认记录)' +const decDigest = decisionDigestMd(autonomousDecisions.slice(flushedCount), '(无未记录的自主默认;已完成模块的决策见上方各模块条目)') if (halted) { const pend = pending.length ? pending.map(p => `\`${p.module}\``).join('、') : '无' await recordResume([ '## ⛔ HALT — 模块 `' + halted.module + '`()', '', `- **halt 原因**:${halted.reason || '(空)'}`, - '- **本次自主默认决策**(缺值时自动取的解读,可能含错误假设,重跑前请复核):', + '- **本次自主默认决策**(仅未被逐模块条目覆盖的增量;已完成模块的决策见上方各模块条目,重跑前请复核):', decDigest, `- **halt 后未跑的待办模块**:${pend}`, '- **下一步**:人工修复阻塞点后重跑 `/erp-workflow:coding-start`;Router 按 git tag 续跑,已完成模块自动跳过。', @@ -2241,7 +2247,7 @@ if (halted) { '## ✅ 全部完成()', '', `- **本次完成模块**:${doneList}`, - '- **本次自主默认决策**:', + '- **本次自主默认决策**(仅未被逐模块条目覆盖的增量):', decDigest, ].join('\n')) } -- libgit2 0.22.2