Commit c684f5c0584ee6ff173f47a52cd028767ddeb08e
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>
Showing
3 changed files
with
80 additions
and
1 deletions
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 >= 0 | @@ -2075,6 +2110,35 @@ const pending = haltedAtIdx >= 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 {...}`, |