Commit c684f5c0584ee6ff173f47a52cd028767ddeb08e

Authored by yanghl
1 parent f659a376

coding.mjs: 决策上下文持久化(RESUME.md 续跑 handoff)

中断/halt 后重跑能复盘上次状态,不只恢复进度。编码阶段保持全自动静默不变。

- coding.mjs:新增 resumeJournalPromptM + recordResume(best-effort,写失败绝不阻断/
  掩盖主流程)。主循环结束(halt 或全完成)时向 docs/superpowers/RESUME.md 追加一条:
  halt 原因 + 本次自主默认假设(decisions 摘要) + 待跑模块 + 下一步
- coding-start:新增步骤 3.5,重跑时读 RESUME.md 末尾条目向用户复盘上次 halt/完成状态
- 进度真值仍是 git tag(milestone/req-done)+ committed 工件——硬中断后 Router 据此续跑;
  RESUME.md 补的是跨会话会丢失的「为何停/做过哪些假设」定向信息(GSD continue-here.md 类比)
- 硬中断(进程被杀)到不了写入点:那种情况无 halt 原因可记,且 tag+工件已够续跑,无信息损失

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
README.md
@@ -61,6 +61,10 @@ Claude Code 插件:ERP / 后端管理系统全流程开发框架。 @@ -61,6 +61,10 @@ Claude Code 插件:ERP / 后端管理系统全流程开发框架。
61 → runMilestone(milestone/frontend-phase) 61 → runMilestone(milestone/frontend-phase)
62 62
63 子代理无法弹窗 → 缺值即写阻塞点并 halt(终止态,非对话框);fail-fast 后等人工修复重跑 coding-start 63 子代理无法弹窗 → 缺值即写阻塞点并 halt(终止态,非对话框);fail-fast 后等人工修复重跑 coding-start
  64 +
  65 + 续跑 handoff:主循环结束(halt 或全完成)时 best-effort 追加 docs/superpowers/RESUME.md
  66 + (上次 halt 原因 + 本次自主默认假设 + 待跑模块),供下次 coding-start 步骤 3.5 复盘;
  67 + 进度真值仍是 git tag,RESUME.md 仅定向,写失败绝不阻断主流程
64 ``` 68 ```
65 69
66 ## 首次使用 70 ## 首次使用
@@ -128,7 +132,7 @@ erp-workflow-plugin/ @@ -128,7 +132,7 @@ erp-workflow-plugin/
128 | Skill | 作用 | 谁调用 | 132 | Skill | 作用 | 谁调用 |
129 |---|---|---| 133 |---|---|---|
130 | `plan-start` | **A 阶段入口 + Plan 终结硬闸**。读 docs/08 § 一 找第一个未勾 A 子项 → 派发对应 A skill;A 全部完成时校验 4 项前移闸门(REQ 真实数据、`config-vars.yaml` 全部配置(含 DB 凭据 / 密钥)全锁、docs/04 § 零 命令齐、docs/05+02 已评审),全过才提示运行 `/erp-workflow:coding-start`,否则指出缺口不放行 | **用户手动** `/erp-workflow:plan-start` | 134 | `plan-start` | **A 阶段入口 + Plan 终结硬闸**。读 docs/08 § 一 找第一个未勾 A 子项 → 派发对应 A skill;A 全部完成时校验 4 项前移闸门(REQ 真实数据、`config-vars.yaml` 全部配置(含 DB 凭据 / 密钥)全锁、docs/04 § 零 命令齐、docs/05+02 已评审),全过才提示运行 `/erp-workflow:coding-start`,否则指出缺口不放行 | **用户手动** `/erp-workflow:plan-start` |
131 -| `coding-start` | **B 阶段瘦入口**(`allowed-tools: Read Glob Workflow Bash(git ...)`)。校验 Plan 完成态(docs/08 § 一 全勾)+ 取得 projectRoot(git 就绪由 coding.mjs runBranchSetup 在运行时把守)→ 读 docs/08 § 二/§ 三 + `git tag -l 'milestone/*'` 概述阶段进度(Workflow Router 再用 `req-done/*` 判定功能级 resume)→ 调用 `Workflow({scriptPath:"${CLAUDE_PLUGIN_ROOT}/workflows/coding.mjs", args:{projectRoot}})` 启动整个编码阶段 → 告知"已在后台启动" | **用户手动** `/erp-workflow:coding-start` | 135 +| `coding-start` | **B 阶段瘦入口**(`allowed-tools: Read Glob Workflow Bash(git ...)`)。校验 Plan 完成态(docs/08 § 一 全勾)+ 取得 projectRoot(git 就绪由 coding.mjs runBranchSetup 在运行时把守)→ 读 docs/08 § 二/§ 三 + `git tag -l 'milestone/*'` 概述阶段进度(Workflow Router 再用 `req-done/*` 判定功能级 resume)→ 若有 `docs/superpowers/RESUME.md` 则读末尾条目复盘上次 halt 原因/待跑模块 → 调用 `Workflow({scriptPath:"${CLAUDE_PLUGIN_ROOT}/workflows/coding.mjs", args:{projectRoot}})` 启动整个编码阶段 → 告知"已在后台启动" | **用户手动** `/erp-workflow:coding-start` |
132 | `add-req` | **增量需求入口**(Plan 完结后追加 / 修改需求时用)。`node lib/req-ledger.mjs scan` 用内容哈希台账(`.req-ledger.json`)识别 docs/01 REQ 卡片 + docs/08 §三 FE 行的**新增 / 变更**;只对增量做 **A3-delta**(写新 `V_n` migration + 同步 docs/03,**绝不改 V1**)+ **A5-delta**(补 docs/05 端点 / docs/02 顺序 / docs/08 模块行 / FE 行),并 `git tag -d` 作废**变更**单元已有的 `req-done/*`(及所属 `milestone/*`,复位 docs/08 里程碑字段),使 coding.mjs Router 只重跑增量。首跑无台账则先建基线。 | **用户手动** `/erp-workflow:add-req` | 136 | `add-req` | **增量需求入口**(Plan 完结后追加 / 修改需求时用)。`node lib/req-ledger.mjs scan` 用内容哈希台账(`.req-ledger.json`)识别 docs/01 REQ 卡片 + docs/08 §三 FE 行的**新增 / 变更**;只对增量做 **A3-delta**(写新 `V_n` migration + 同步 docs/03,**绝不改 V1**)+ **A5-delta**(补 docs/05 端点 / docs/02 顺序 / docs/08 模块行 / FE 行),并 `git tag -d` 作废**变更**单元已有的 `req-done/*`(及所属 `milestone/*`,复位 docs/08 里程碑字段),使 coding.mjs Router 只重跑增量。首跑无台账则先建基线。 | **用户手动** `/erp-workflow:add-req` |
133 137
134 ### Plan 阶段 A skill(A0~A5,共 6 个) 138 ### Plan 阶段 A skill(A0~A5,共 6 个)
skills/coding/coding-start/SKILL.md
@@ -77,6 +77,16 @@ allowed-tools: Read Glob Workflow Bash(git rev-parse *) Bash(git tag *) @@ -77,6 +77,16 @@ allowed-tools: Read Glob Workflow Bash(git rev-parse *) Bash(git tag *)
77 77
78 向用户简述「已完成 N 个模块 / 待跑 M 个模块;前端阶段:已完成 / 待跑」。 78 向用户简述「已完成 N 个模块 / 待跑 M 个模块;前端阶段:已完成 / 待跑」。
79 79
  80 +### 步骤 3.5:复盘上次运行(续跑 handoff,若有)
  81 +
  82 +用 `Glob` 检查 `docs/superpowers/RESUME.md`(由 coding.mjs 在每次 halt / 全部完成时追加,最新条目在末尾):
  83 +
  84 +- 不存在 → 跳过本步(首次运行 / 尚无记录)。
  85 +- 存在 → `Read` 其**末尾最新条目**,向用户复述上次运行状态,帮助决定是否直接重跑:
  86 + - 末尾是 `## ⛔ HALT` → 提示「上次在模块 `<id>` halt,原因:`<reason>`;待跑模块:`<list>`;上次做过的自主默认假设见该条目(重跑前可复核)」。
  87 + - 末尾是 `## ✅ 全部完成` → 提示「上次已全部完成;本次重跑通常无新增模块(除非用 /add-req 加了增量需求)」。
  88 +- RESUME.md 仅作**定向参考与人工复盘**,进度真值仍以 git tag(步骤 3)为准;不因它阻断启动。
  89 +
80 ### 步骤 4:启动 Coding Workflow 90 ### 步骤 4:启动 Coding Workflow
81 91
82 用 `Workflow` 工具调用编码编排脚本。`projectRoot` 必须使用步骤 2 里 `git rev-parse --show-toplevel` 得到的绝对路径(coding.mjs 顶部对相对路径硬校验,传 `.` 会立即 halt)。 92 用 `Workflow` 工具调用编码编排脚本。`projectRoot` 必须使用步骤 2 里 `git rev-parse --show-toplevel` 得到的绝对路径(coding.mjs 顶部对相对路径硬校验,传 `.` 会立即 halt)。
@@ -117,6 +127,7 @@ Workflow({ @@ -117,6 +127,7 @@ Workflow({
117 - `docs/08-模块任务管理.md § 一`(A0~A5 Plan 进度,步骤 2 读取) 127 - `docs/08-模块任务管理.md § 一`(A0~A5 Plan 进度,步骤 2 读取)
118 - `docs/08-模块任务管理.md § 二`(后端模块元数据 + 里程碑字段,步骤 3 读取) 128 - `docs/08-模块任务管理.md § 二`(后端模块元数据 + 里程碑字段,步骤 3 读取)
119 - `docs/08-模块任务管理.md § 三`(前端阶段整体里程碑,步骤 3 读取) 129 - `docs/08-模块任务管理.md § 三`(前端阶段整体里程碑,步骤 3 读取)
  130 +- `docs/superpowers/RESUME.md`(续跑 handoff:上次 halt 原因 / 自主假设 / 待跑模块,步骤 3.5 复盘;由 coding.mjs 维护)
120 - `workflows/coding.mjs`(B 阶段编排脚本,步骤 4 启动) 131 - `workflows/coding.mjs`(B 阶段编排脚本,步骤 4 启动)
121 - `plan-start`(姊妹入口,A 阶段) 132 - `plan-start`(姊妹入口,A 阶段)
122 - `CLAUDE.md`(项目指令) 133 - `CLAUDE.md`(项目指令)
workflows/coding.mjs
@@ -1001,6 +1001,41 @@ async function runAction(makePrompt, { site, grp, label, allowContinue = false } @@ -1001,6 +1001,41 @@ async function runAction(makePrompt, { site, grp, label, allowContinue = false }
1001 throw new Error(`HALT ${site}-adjudication-exhausted: ${ADJUDICATE_MAX} 轮仲裁仍未解决`) 1001 throw new Error(`HALT ${site}-adjudication-exhausted: ${ADJUDICATE_MAX} 轮仲裁仍未解决`)
1002 } 1002 }
1003 1003
  1004 +// ── 续跑 handoff(RESUME.md):halt / 全完成时追加一条跨运行记录 ─────────────
  1005 +// 进度真值仍是 git tag(milestone/req-done)+ 已 commit 的 module-reports / specs 工件——
  1006 +// 这些已让硬中断后 Router 正确续跑、per-feature 决策也落在工件里。RESUME.md 补的是
  1007 +// 「上次为何 halt + 本次做过哪些自主默认假设 + 还剩哪些模块」这类**跨会话会丢失**的定向信息
  1008 +// (GSD continue-here.md 类比),供下次 coding-start 重跑时向人工复盘。
  1009 +// best-effort:它自身写失败**绝不**阻断 / 掩盖主流程(尤其在 halt 善后路径上)。
  1010 +const RESUME_PATH = 'docs/superpowers/RESUME.md'
  1011 +function resumeJournalPromptM(sectionMd) {
  1012 + return [
  1013 + '# 追加续跑日志(RESUME handoff)— 非交互静默',
  1014 + microStepContract(),
  1015 + '',
  1016 + `## 任务:把给定条目**追加到 \`${RESUME_PATH}\` 末尾**后 commit(不改其它任何文件)`,
  1017 + `1. 确保目录 \`${ROOT}/docs/superpowers/\` 存在(不存在则创建)。`,
  1018 + `2. 取时间戳:\`git -C ${ROOT} log -1 --format=%cd --date=format:'%Y-%m-%d %H:%M'\`;取不到(无 commit)则用空串。`,
  1019 + `3. 若 \`${RESUME_PATH}\` 不存在 → 先写文件头:一行 \`# 续跑日志(RESUME handoff)\`,空行,再一行引用块说明「Coding 每次 halt/完成时追加,**最新条目在末尾**;中断后重跑 /erp-workflow:coding-start 前先读末尾条目」。`,
  1020 + '4. 把下面这段条目**追加到文件末尾**(前置一空行;把其中的 `<ts>` 替换为步骤 2 的时间戳):',
  1021 + '```markdown',
  1022 + sectionMd,
  1023 + '```',
  1024 + `5. commit:\`git -C ${ROOT} add ${RESUME_PATH}\` → \`git -C ${ROOT} commit -m "chore(resume): 续跑 handoff 追加"\`。`,
  1025 + '',
  1026 + '## 输出(ACTION_RESULT_SCHEMA)',
  1027 + '- 成功 → `{ "success": true }`;任何步骤失败 → `{ "success": false, "error": "<原因>", "detail": "<stderr 摘要>" }`(**不要**抛错,照实返回即可)。',
  1028 + ].join('\n')
  1029 +}
  1030 +async function recordResume(sectionMd) {
  1031 + // best-effort:续跑日志写失败绝不阻断主流程(它为 resume 而存在,不该反过来制造 halt)。
  1032 + try {
  1033 + const r = await agent(resumeJournalPromptM(sectionMd), { label:'resume-journal', phase:'Milestone', schema: ACTION_RESULT_SCHEMA })
  1034 + if (r && r.success) log('resume-journal 已追加 RESUME.md')
  1035 + else log(`resume-journal 写入失败(不阻断):${(r && r.error) || ''}`)
  1036 + } catch (e) { log(`resume-journal 异常(不阻断):${String(e?.message || e)}`) }
  1037 +}
  1038 +
1004 // recoverDirtyWorktreePromptM:branchSetup / milestone 前置的"工作树干净"被打破时的自主恢复(class D 部分)。 1039 // recoverDirtyWorktreePromptM:branchSetup / milestone 前置的"工作树干净"被打破时的自主恢复(class D 部分)。
1005 // 子代理检查脏文件——全是本阶段合法产物 → 自动 commit 后继续;含越界/不明改动 → 不提交、返回失败让上层 halt。 1040 // 子代理检查脏文件——全是本阶段合法产物 → 自动 commit 后继续;含越界/不明改动 → 不提交、返回失败让上层 halt。
1006 // **分支护栏(branch)**:自动 commit 只允许发生在目标功能分支上。若当前 HEAD 不在 branch(如里程碑后 HEAD 1041 // **分支护栏(branch)**:自动 commit 只允许发生在目标功能分支上。若当前 HEAD 不在 branch(如里程碑后 HEAD
@@ -2075,6 +2110,35 @@ const pending = haltedAtIdx &gt;= 0 @@ -2075,6 +2110,35 @@ const pending = haltedAtIdx &gt;= 0
2075 // (decisions:stage 缺值时未停而自主取的默认/解读,供 coding-start / 人工事后审阅,可能含错误假设)。 2110 // (decisions:stage 缺值时未停而自主取的默认/解读,供 coding-start / 人工事后审阅,可能含错误假设)。
2076 // 注:decisions 仅覆盖**本次运行实际新跑**的 stage;resume 时被 req-done/milestone tag 跳过的已完成功能, 2111 // 注:decisions 仅覆盖**本次运行实际新跑**的 stage;resume 时被 req-done/milestone tag 跳过的已完成功能,
2077 // 其决策不会重新登记于此——需到对应 docs/superpowers/specs|plans/<date>-<id>.md 产物显著位置查阅。 2112 // 其决策不会重新登记于此——需到对应 docs/superpowers/specs|plans/<date>-<id>.md 产物显著位置查阅。
  2113 +// 续跑 handoff(best-effort,静默):把本次运行结果落进 docs/superpowers/RESUME.md,
  2114 +// 使下次 coding-start 重跑能复盘「上次为何 halt / 做过哪些自主假设 / 还剩哪些模块」。
  2115 +// 硬中断(进程被杀,到不了这里)时不写——那种情况无 halt 原因可记,且 tag+工件已够 Router 续跑。
  2116 +const halted = results.find(r => r.status === 'halted')
  2117 +const decDigest = autonomousDecisions.length
  2118 + ? autonomousDecisions.map(d => ` - [\`${d.site}\`] ${d.question || '?'} → ${d.choice || '?'}(${d.confidence || '?'})`).join('\n')
  2119 + : ' - (本次运行无自主默认记录)'
  2120 +if (halted) {
  2121 + const pend = pending.length ? pending.map(p => `\`${p.module}\``).join('、') : '无'
  2122 + await recordResume([
  2123 + '## ⛔ HALT — 模块 `' + halted.module + '`(<ts>)',
  2124 + '',
  2125 + `- **halt 原因**:${halted.reason || '(空)'}`,
  2126 + '- **本次自主默认决策**(缺值时自动取的解读,可能含错误假设,重跑前请复核):',
  2127 + decDigest,
  2128 + `- **halt 后未跑的待办模块**:${pend}`,
  2129 + '- **下一步**:人工修复阻塞点后重跑 `/erp-workflow:coding-start`;Router 按 git tag 续跑,已完成模块自动跳过。',
  2130 + ].join('\n'))
  2131 +} else if (results.length) {
  2132 + const doneList = results.filter(r => r.status === 'done').map(r => `\`${r.module}\``).join('、') || '无'
  2133 + await recordResume([
  2134 + '## ✅ 全部完成(<ts>)',
  2135 + '',
  2136 + `- **本次完成模块**:${doneList}`,
  2137 + '- **本次自主默认决策**:',
  2138 + decDigest,
  2139 + ].join('\n'))
  2140 +}
  2141 +
2078 // 注:顶层 `return` 不是普通 Node ESM 语法;本文件由 Claude Workflow 运行时执行, 2142 // 注:顶层 `return` 不是普通 Node ESM 语法;本文件由 Claude Workflow 运行时执行,
2079 // 运行时会把脚本体包进 async function,顶层 `return` 是 Workflow 的结果通道。 2143 // 运行时会把脚本体包进 async function,顶层 `return` 是 Workflow 的结果通道。
2080 // 不要把本文件作为 `node workflows/coding.mjs` 直接运行,也不要改成 `export default {...}`, 2144 // 不要把本文件作为 `node workflows/coding.mjs` 直接运行,也不要改成 `export default {...}`,