Commit ad94075f1112a5117b1c28a2ef1e161dba74e45d
1 parent
0956071b
coding.mjs/add-req: 审计简化收敛——bestEffortAction、RESUME 去双写、单次 scan
- bestEffortAction(prompt, {label, phase, okMsg, failTag, errTag}):收敛 recordResume /
ensureLedgerBaseline 逐行同构的 try/catch 包裹(失败/异常只 log 绝不阻断主流程;
≠ runAction:不经 adjudicate、绝不 halt)。日志文案与重构前逐字一致(errTag 区分
失败/异常两条前缀),返回布尔供水位线判断。
- decisionDigestMd helper 消除决策摘要模板的字节级双份拷贝;flushedCount 水位线
(仅 per-module flush 成功才推进)使 loop-end 条目只补「未被逐模块条目覆盖的增量」
——N 模块全绿不再每条决策在 RESUME.md 写两遍;flush 失败时 loop-end 兜底补记保留。
- add-req 步骤 0/1 合并为单次 scan(scan 输出本就同时含 ledgerExists 与
new/changed/removed,重复扫描纯冗余);删步骤 6 无操作性说明尾句。
Showing
2 changed files
with
30 additions
and
28 deletions
skills/plan/add-req/SKILL.md
| @@ -47,15 +47,11 @@ node ${CLAUDE_PLUGIN_ROOT}/lib/req-ledger.mjs scan <root> | @@ -47,15 +47,11 @@ node ${CLAUDE_PLUGIN_ROOT}/lib/req-ledger.mjs scan <root> | ||
| 47 | [ERP-HALT] 基线已建立,等待人工追加需求。 | 47 | [ERP-HALT] 基线已建立,等待人工追加需求。 |
| 48 | ``` | 48 | ``` |
| 49 | (理由:首跑若把存量全当新增,会重复生成已有的 docs/03/05 工件并误重跑整仓。基线建立后,后续改动才能被准确 diff。) | 49 | (理由:首跑若把存量全当新增,会重复生成已有的 docs/03/05 工件并误重跑整仓。基线建立后,后续改动才能被准确 diff。) |
| 50 | -- `true` → 进入步骤 1。 | 50 | +- `true` → 直接用本次 scan 的输出进入步骤 1 解析(不再重跑 scan)。 |
| 51 | 51 | ||
| 52 | ## 步骤 1:检测增量 | 52 | ## 步骤 1:检测增量 |
| 53 | 53 | ||
| 54 | -``` | ||
| 55 | -node ${CLAUDE_PLUGIN_ROOT}/lib/req-ledger.mjs scan <root> | ||
| 56 | -``` | ||
| 57 | - | ||
| 58 | -解析 `new[]` / `changed[]` / `removed[]`(每项 `{id, kind}`,kind = `req` 后端卡片 / `fe` 前端功能行)。 | 54 | +用步骤 0 已得到的 scan 输出解析 `new[]` / `changed[]` / `removed[]`(每项 `{id, kind}`,kind = `req` 后端卡片 / `fe` 前端功能行)。 |
| 59 | 55 | ||
| 60 | - `new` 与 `changed` 均为空 → 打印 `[add-req] 无新增 / 变更需求,无需处理。[ERP-HALT]` **停下**(不提交台账)。 | 56 | - `new` 与 `changed` 均为空 → 打印 `[add-req] 无新增 / 变更需求,无需处理。[ERP-HALT]` **停下**(不提交台账)。 |
| 61 | - `removed` 非空 → **仅提示、不自动删**:打印 `检测到台账登记但 docs 已移除的单元:<列出>。下线需求请人工同步 docs/02/03/05 并删除对应 tag,本 skill 不自动执行删除。` 然后继续处理 new/changed(removed 不写回台账,留待人工,下次仍会提示)。 | 57 | - `removed` 非空 → **仅提示、不自动删**:打印 `检测到台账登记但 docs 已移除的单元:<列出>。下线需求请人工同步 docs/02/03/05 并删除对应 tag,本 skill 不自动执行删除。` 然后继续处理 new/changed(removed 不写回台账,留待人工,下次仍会提示)。 |
| @@ -126,7 +122,7 @@ node ${CLAUDE_PLUGIN_ROOT}/lib/req-ledger.mjs scan <root> | @@ -126,7 +122,7 @@ node ${CLAUDE_PLUGIN_ROOT}/lib/req-ledger.mjs scan <root> | ||
| 126 | git -C <root> commit -m "plan(add-req): 增量需求 <新增/变更 id 摘要>(docs/03+05+02+08 delta + V_n + 台账)" | 122 | git -C <root> commit -m "plan(add-req): 增量需求 <新增/变更 id 摘要>(docs/03+05+02+08 delta + V_n + 台账)" |
| 127 | ``` | 123 | ``` |
| 128 | - 覆盖范围 = 步骤 2~5 实际改动的全部文件:人工新写的 REQ 卡(docs/01)、新增 migration(sql/migrations/V_n)、同步的 docs/03、补的 docs/05/02 端点与顺序、docs/08 模块行/FE 行与复位的里程碑字段、`.req-ledger.json`。 | 124 | - 覆盖范围 = 步骤 2~5 实际改动的全部文件:人工新写的 REQ 卡(docs/01)、新增 migration(sql/migrations/V_n)、同步的 docs/03、补的 docs/05/02 端点与顺序、docs/08 模块行/FE 行与复位的里程碑字段、`.req-ledger.json`。 |
| 129 | - - 步骤 5 的 `git tag -d`(删 req-done/milestone)是对 ref 的操作,不产生工作树文件,不需进本 commit;但删 tag 应在本次 commit **之前或之后**都可(与文件提交独立)。 | 125 | + - 步骤 5 的 `git tag -d`(删 req-done/milestone)是对 ref 的操作,不产生工作树文件,不需进本 commit。 |
| 130 | - commit 后用 `git -C <root> status --porcelain` 复核工作树**干净**(无残留);若仍有未跟踪/未提交项,排查后补提交,确保交给 coding-start 时是干净树。 | 126 | - commit 后用 `git -C <root> status --porcelain` 复核工作树**干净**(无残留);若仍有未跟踪/未提交项,排查后补提交,确保交给 coding-start 时是干净树。 |
| 131 | 127 | ||
| 132 | 3. 然后打印横幅并**停下**(不自动进编码,与「Plan 完不自动进 B」一致): | 128 | 3. 然后打印横幅并**停下**(不自动进编码,与「Plan 完不自动进 B」一致): |
workflows/coding.mjs
| @@ -924,6 +924,9 @@ const adjGuidance = (g) => g ? `\n\n## 隞脰迤誘嚗甈⊿ | @@ -924,6 +924,9 @@ const adjGuidance = (g) => g ? `\n\n## 隞脰迤誘嚗甈⊿ | ||
| 924 | 924 | ||
| 925 | // 瘚銝餃蝑敹tage 蝻箏潭銝恕/閫粉嚗霈啣甇歹犖撌乩恣 | 925 | // 瘚銝餃蝑敹tage 蝻箏潭銝恕/閫粉嚗霈啣甇歹犖撌乩恣 |
| 926 | const autonomousDecisions = [] | 926 | const autonomousDecisions = [] |
| 927 | +const decisionDigestMd = (list, emptyText) => list.length | ||
| 928 | + ? list.map(d => ` - [\`${d.site}\`] ${d.question || '?'} ${d.choice || '?'}嚗${d.confidence || '?'}嚗).join('\n') | ||
| 929 | + : ` - ${emptyText}` | ||
| 927 | function recordDecisions(site, decisions) { | 930 | function recordDecisions(site, decisions) { |
| 928 | if (!Array.isArray(decisions)) return | 931 | if (!Array.isArray(decisions)) return |
| 929 | for (const d of decisions) { | 932 | for (const d of decisions) { |
| @@ -1036,6 +1039,18 @@ async function runAction(makePrompt, { site, grp, label, allowContinue = false } | @@ -1036,6 +1039,18 @@ async function runAction(makePrompt, { site, grp, label, allowContinue = false } | ||
| 1036 | throw new Error(`HALT ${site}-adjudication-exhausted: ${ADJUDICATE_MAX} 頧桐輔鋆閫`) | 1039 | throw new Error(`HALT ${site}-adjudication-exhausted: ${ADJUDICATE_MAX} 頧桐輔鋆閫`) |
| 1037 | } | 1040 | } |
| 1038 | 1041 | ||
| 1042 | +// best-effort 敺格郊撉斤ㄨ嚗仃韐/撘虜 log 蝏銝餅偌雿瑪餈嚗// runAction嚗 helper 銝 adjudicate halt | ||
| 1043 | +async function bestEffortAction(prompt, { label, phase: ph, okMsg, failTag, errTag = failTag }) { | ||
| 1044 | + try { | ||
| 1045 | + const r = await agent(prompt, { label, phase: ph, schema: ACTION_RESULT_SCHEMA }) | ||
| 1046 | + if (r && r.success) { log(okMsg); return true } | ||
| 1047 | + log(`${failTag}憭梯揖嚗嚗${(r && r.error) || ''}`) | ||
| 1048 | + } catch (e) { | ||
| 1049 | + log(`${errTag}撘虜嚗嚗${String(e?.message || e)}`) | ||
| 1050 | + } | ||
| 1051 | + return false | ||
| 1052 | +} | ||
| 1053 | + | ||
| 1039 | // 蝏剛 handoff嚗ESUME.md嚗alt / 摰餈賢頝刻扇敶 | 1054 | // 蝏剛 handoff嚗ESUME.md嚗alt / 摰餈賢頝刻扇敶 |
| 1040 | // 餈漲潔 git tag嚗ilestone/req-done嚗+ 撌 commit module-reports / specs 撌乩辣 | 1055 | // 餈漲潔 git tag嚗ilestone/req-done嚗+ 撌 commit module-reports / specs 撌乩辣 |
| 1041 | // 餈歇霈拍′銝剜 Router 甇&蝏剛er-feature 蝑撌乩辣ESUME.md 銵亦 | 1056 | // 餈歇霈拍′銝剜 Router 甇&蝏剛er-feature 蝑撌乩辣ESUME.md 銵亦 |
| @@ -1064,11 +1079,8 @@ function resumeJournalPromptM(sectionMd) { | @@ -1064,11 +1079,8 @@ function resumeJournalPromptM(sectionMd) { | ||
| 1064 | } | 1079 | } |
| 1065 | async function recordResume(sectionMd) { | 1080 | async function recordResume(sectionMd) { |
| 1066 | // best-effort嚗賒頝敹仃韐亦銝餅蛹 resume 嚗砲 halt嚗 | 1081 | // best-effort嚗賒頝敹仃韐亦銝餅蛹 resume 嚗砲 halt嚗 |
| 1067 | - try { | ||
| 1068 | - const r = await agent(resumeJournalPromptM(sectionMd), { label:'resume-journal', phase:'Milestone', schema: ACTION_RESULT_SCHEMA }) | ||
| 1069 | - if (r && r.success) log('resume-journal 撌脰蕭 RESUME.md') | ||
| 1070 | - else log(`resume-journal 憭梯揖嚗嚗${(r && r.error) || ''}`) | ||
| 1071 | - } catch (e) { log(`resume-journal 撘虜嚗嚗${String(e?.message || e)}`) } | 1082 | + return bestEffortAction(resumeJournalPromptM(sectionMd), |
| 1083 | + { label: 'resume-journal', phase: 'Milestone', okMsg: 'resume-journal 撌脰蕭 RESUME.md', failTag: 'resume-journal ', errTag: 'resume-journal ' }) | ||
| 1072 | } | 1084 | } |
| 1073 | 1085 | ||
| 1074 | // 瘙韐血蝥選1#3嚗oding 擐 .req-ledger.json 蝻箏仃嚗撱箇僎漱 | 1086 | // 瘙韐血蝥選1#3嚗oding 擐 .req-ledger.json 蝻箏仃嚗撱箇僎漱 |
| @@ -1091,11 +1103,8 @@ function ledgerBaselinePromptM() { | @@ -1091,11 +1103,8 @@ function ledgerBaselinePromptM() { | ||
| 1091 | } | 1103 | } |
| 1092 | async function ensureLedgerBaseline() { | 1104 | async function ensureLedgerBaseline() { |
| 1093 | if (!PLUGIN) { log('req-ledger 蝥輯歲餈 pluginRoot嚗 /add-req 擐'); return } | 1105 | if (!PLUGIN) { log('req-ledger 蝥輯歲餈 pluginRoot嚗 /add-req 擐'); return } |
| 1094 | - try { | ||
| 1095 | - const r = await agent(ledgerBaselinePromptM(), { label:'req-ledger-baseline', phase:'Router', schema: ACTION_RESULT_SCHEMA }) | ||
| 1096 | - if (r && r.success) log('req-ledger 蝥踹停蝏') | ||
| 1097 | - else log(`req-ledger 蝥踹仃韐伐嚗${(r && r.error) || ''}`) | ||
| 1098 | - } catch (e) { log(`req-ledger 蝥踹虜嚗嚗${String(e?.message || e)}`) } | 1106 | + return bestEffortAction(ledgerBaselinePromptM(), |
| 1107 | + { label: 'req-ledger-baseline', phase: 'Router', okMsg: 'req-ledger 蝥踹停蝏', failTag: 'req-ledger 蝥' }) | ||
| 1099 | } | 1108 | } |
| 1100 | function preflightPromptM() { | 1109 | function preflightPromptM() { |
| 1101 | return [ | 1110 | return [ |
| @@ -2141,6 +2150,7 @@ if (todo.length) { | @@ -2141,6 +2150,7 @@ if (todo.length) { | ||
| 2141 | 2150 | ||
| 2142 | const results = [] | 2151 | const results = [] |
| 2143 | let haltedAtIdx = -1 | 2152 | let haltedAtIdx = -1 |
| 2153 | +let flushedCount = 0 | ||
| 2144 | for (const [idx, module] of todo.entries()) { | 2154 | for (const [idx, module] of todo.entries()) { |
| 2145 | const decStart = autonomousDecisions.length // 璅∪銝餃蝑偌雿瑪嚗1#1 憓 flush 嚗 | 2155 | const decStart = autonomousDecisions.length // 璅∪銝餃蝑偌雿瑪嚗1#1 憓 flush 嚗 |
| 2146 | try { | 2156 | try { |
| @@ -2187,16 +2197,14 @@ for (const [idx, module] of todo.entries()) { | @@ -2187,16 +2197,14 @@ for (const [idx, module] of todo.entries()) { | ||
| 2187 | // P1#1 憓 flush嚗葵璅∪摰餈賢 RESUME.md嚗蝙**蝖砌葉**嚗◤嚗銝蜓敺芰撠橘 | 2197 | // P1#1 憓 flush嚗葵璅∪摰餈賢 RESUME.md嚗蝙**蝖砌葉**嚗◤嚗銝蜓敺芰撠橘 |
| 2188 | // 銋憭歇摰芋鈭銝餃挽 loop-end est-effort嚗 | 2198 | // 銋憭歇摰芋鈭銝餃挽 loop-end est-effort嚗 |
| 2189 | { | 2199 | { |
| 2190 | - const md = autonomousDecisions.slice(decStart) | ||
| 2191 | - const digest = md.length | ||
| 2192 | - ? md.map(d => ` - [\`${d.site}\`] ${d.question || '?'} ${d.choice || '?'}嚗${d.confidence || '?'}嚗).join('\n') | ||
| 2193 | - : ' - 嚗璅∪銝駁恕霈啣' | ||
| 2194 | - await recordResume([ | 2200 | + const moduleDecisions = autonomousDecisions.slice(decStart) |
| 2201 | + const ok = await recordResume([ | ||
| 2195 | '## 璅∪ `' + module.id + '` milestone/' + module.id + '嚗<ts>嚗', | 2202 | '## 璅∪ `' + module.id + '` milestone/' + module.id + '嚗<ts>嚗', |
| 2196 | '', | 2203 | '', |
| 2197 | '- **璅∪銝駁恕蝑**嚗撩潭圾霂鳴秤挽嚗', | 2204 | '- **璅∪銝駁恕蝑**嚗撩潭圾霂鳴秤挽嚗', |
| 2198 | - digest, | 2205 | + decisionDigestMd(moduleDecisions, '嚗璅∪銝駁恕霈啣'), |
| 2199 | ].join('\n')) | 2206 | ].join('\n')) |
| 2207 | + if (ok) flushedCount = autonomousDecisions.length // 隞餈偌雿瑪嚗lush 憭梯揖 loop-end ‘霈 | ||
| 2200 | } | 2208 | } |
| 2201 | } catch (e) { | 2209 | } catch (e) { |
| 2202 | const reason = String(e.message || e) | 2210 | const reason = String(e.message || e) |
| @@ -2221,16 +2229,14 @@ const pending = haltedAtIdx >= 0 | @@ -2221,16 +2229,14 @@ const pending = haltedAtIdx >= 0 | ||
| 2221 | // 雿蹂活 coding-start 憭活銝箔 halt / 鈭銝餃挽 / 餈鈭芋 | 2229 | // 雿蹂活 coding-start 憭活銝箔 halt / 鈭銝餃挽 / 餈鈭芋 |
| 2222 | // 蝖砌葉嚗◤嚗銝銝蝘 halt 霈堆 tag+撌乩辣撌脣 Router 蝏剛 | 2230 | // 蝖砌葉嚗◤嚗銝銝蝘 halt 霈堆 tag+撌乩辣撌脣 Router 蝏剛 |
| 2223 | const halted = results.find(r => r.status === 'halted') | 2231 | const halted = results.find(r => r.status === 'halted') |
| 2224 | -const decDigest = autonomousDecisions.length | ||
| 2225 | - ? autonomousDecisions.map(d => ` - [\`${d.site}\`] ${d.question || '?'} ${d.choice || '?'}嚗${d.confidence || '?'}嚗).join('\n') | ||
| 2226 | - : ' - 嚗甈∟銝駁恕霈啣' | 2232 | +const decDigest = decisionDigestMd(autonomousDecisions.slice(flushedCount), '嚗霈啣銝駁恕嚗歇摰芋蝑芋嚗') |
| 2227 | if (halted) { | 2233 | if (halted) { |
| 2228 | const pend = pending.length ? pending.map(p => `\`${p.module}\``).join('') : '' | 2234 | const pend = pending.length ? pending.map(p => `\`${p.module}\``).join('') : '' |
| 2229 | await recordResume([ | 2235 | await recordResume([ |
| 2230 | '## HALT 璅∪ `' + halted.module + '`嚗<ts>嚗', | 2236 | '## HALT 璅∪ `' + halted.module + '`嚗<ts>嚗', |
| 2231 | '', | 2237 | '', |
| 2232 | `- **halt **嚗${halted.reason || '(蝛)'}`, | 2238 | `- **halt **嚗${halted.reason || '(蝛)'}`, |
| 2233 | - '- **甈∟銝駁恕蝑**嚗撩潭圾霂鳴秤挽嚗窈憭嚗', | 2239 | + '- **甈∟銝駁恕蝑**嚗鋡恍芋閬歇摰芋蝑芋嚗窈憭嚗', |
| 2234 | decDigest, | 2240 | decDigest, |
| 2235 | `- **halt 頝芋**嚗${pend}`, | 2241 | `- **halt 頝芋**嚗${pend}`, |
| 2236 | '- **銝甇**嚗犖撌乩耨憭憛 `/erp-workflow:coding-start`嚗outer git tag 蝏剛歇摰芋頝唾', | 2242 | '- **銝甇**嚗犖撌乩耨憭憛 `/erp-workflow:coding-start`嚗outer git tag 蝏剛歇摰芋頝唾', |
| @@ -2241,7 +2247,7 @@ if (halted) { | @@ -2241,7 +2247,7 @@ if (halted) { | ||
| 2241 | '## 摰<ts>嚗', | 2247 | '## 摰<ts>嚗', |
| 2242 | '', | 2248 | '', |
| 2243 | `- **甈∪芋**嚗${doneList}`, | 2249 | `- **甈∪芋**嚗${doneList}`, |
| 2244 | - '- **甈∟銝駁恕蝑**嚗', | 2250 | + '- **甈∟銝駁恕蝑**嚗鋡恍芋閬', |
| 2245 | decDigest, | 2251 | decDigest, |
| 2246 | ].join('\n')) | 2252 | ].join('\n')) |
| 2247 | } | 2253 | } |