diff --git a/README.md b/README.md index b345485..b2301d4 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,10 @@ Claude Code 插件:ERP / 后端管理系统全流程开发框架。 → runMilestone(milestone/frontend-phase) 子代理无法弹窗 → 缺值即写阻塞点并 halt(终止态,非对话框);fail-fast 后等人工修复重跑 coding-start + + 续跑 handoff:主循环结束(halt 或全完成)时 best-effort 追加 docs/superpowers/RESUME.md + (上次 halt 原因 + 本次自主默认假设 + 待跑模块),供下次 coding-start 步骤 3.5 复盘; + 进度真值仍是 git tag,RESUME.md 仅定向,写失败绝不阻断主流程 ``` ## 首次使用 @@ -128,7 +132,7 @@ erp-workflow-plugin/ | Skill | 作用 | 谁调用 | |---|---|---| | `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` | -| `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` | +| `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` | | `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` | ### Plan 阶段 A skill(A0~A5,共 6 个) diff --git a/skills/coding/coding-start/SKILL.md b/skills/coding/coding-start/SKILL.md index e38c1e3..9001503 100644 --- a/skills/coding/coding-start/SKILL.md +++ b/skills/coding/coding-start/SKILL.md @@ -77,6 +77,16 @@ allowed-tools: Read Glob Workflow Bash(git rev-parse *) Bash(git tag *) 向用户简述「已完成 N 个模块 / 待跑 M 个模块;前端阶段:已完成 / 待跑」。 +### 步骤 3.5:复盘上次运行(续跑 handoff,若有) + +用 `Glob` 检查 `docs/superpowers/RESUME.md`(由 coding.mjs 在每次 halt / 全部完成时追加,最新条目在末尾): + +- 不存在 → 跳过本步(首次运行 / 尚无记录)。 +- 存在 → `Read` 其**末尾最新条目**,向用户复述上次运行状态,帮助决定是否直接重跑: + - 末尾是 `## ⛔ HALT` → 提示「上次在模块 `` halt,原因:``;待跑模块:``;上次做过的自主默认假设见该条目(重跑前可复核)」。 + - 末尾是 `## ✅ 全部完成` → 提示「上次已全部完成;本次重跑通常无新增模块(除非用 /add-req 加了增量需求)」。 +- RESUME.md 仅作**定向参考与人工复盘**,进度真值仍以 git tag(步骤 3)为准;不因它阻断启动。 + ### 步骤 4:启动 Coding Workflow 用 `Workflow` 工具调用编码编排脚本。`projectRoot` 必须使用步骤 2 里 `git rev-parse --show-toplevel` 得到的绝对路径(coding.mjs 顶部对相对路径硬校验,传 `.` 会立即 halt)。 @@ -117,6 +127,7 @@ Workflow({ - `docs/08-模块任务管理.md § 一`(A0~A5 Plan 进度,步骤 2 读取) - `docs/08-模块任务管理.md § 二`(后端模块元数据 + 里程碑字段,步骤 3 读取) - `docs/08-模块任务管理.md § 三`(前端阶段整体里程碑,步骤 3 读取) +- `docs/superpowers/RESUME.md`(续跑 handoff:上次 halt 原因 / 自主假设 / 待跑模块,步骤 3.5 复盘;由 coding.mjs 维护) - `workflows/coding.mjs`(B 阶段编排脚本,步骤 4 启动) - `plan-start`(姊妹入口,A 阶段) - `CLAUDE.md`(项目指令) diff --git a/workflows/coding.mjs b/workflows/coding.mjs index d7a2beb..531f2fc 100644 --- a/workflows/coding.mjs +++ b/workflows/coding.mjs @@ -1001,6 +1001,41 @@ async function runAction(makePrompt, { site, grp, label, allowContinue = false } throw new Error(`HALT ${site}-adjudication-exhausted: ${ADJUDICATE_MAX} 轮仲裁仍未解决`) } +// ── 续跑 handoff(RESUME.md):halt / 全完成时追加一条跨运行记录 ───────────── +// 进度真值仍是 git tag(milestone/req-done)+ 已 commit 的 module-reports / specs 工件—— +// 这些已让硬中断后 Router 正确续跑、per-feature 决策也落在工件里。RESUME.md 补的是 +// 「上次为何 halt + 本次做过哪些自主默认假设 + 还剩哪些模块」这类**跨会话会丢失**的定向信息 +// (GSD continue-here.md 类比),供下次 coding-start 重跑时向人工复盘。 +// best-effort:它自身写失败**绝不**阻断 / 掩盖主流程(尤其在 halt 善后路径上)。 +const RESUME_PATH = 'docs/superpowers/RESUME.md' +function resumeJournalPromptM(sectionMd) { + return [ + '# 追加续跑日志(RESUME handoff)— 非交互静默', + microStepContract(), + '', + `## 任务:把给定条目**追加到 \`${RESUME_PATH}\` 末尾**后 commit(不改其它任何文件)`, + `1. 确保目录 \`${ROOT}/docs/superpowers/\` 存在(不存在则创建)。`, + `2. 取时间戳:\`git -C ${ROOT} log -1 --format=%cd --date=format:'%Y-%m-%d %H:%M'\`;取不到(无 commit)则用空串。`, + `3. 若 \`${RESUME_PATH}\` 不存在 → 先写文件头:一行 \`# 续跑日志(RESUME handoff)\`,空行,再一行引用块说明「Coding 每次 halt/完成时追加,**最新条目在末尾**;中断后重跑 /erp-workflow:coding-start 前先读末尾条目」。`, + '4. 把下面这段条目**追加到文件末尾**(前置一空行;把其中的 `` 替换为步骤 2 的时间戳):', + '```markdown', + sectionMd, + '```', + `5. commit:\`git -C ${ROOT} add ${RESUME_PATH}\` → \`git -C ${ROOT} commit -m "chore(resume): 续跑 handoff 追加"\`。`, + '', + '## 输出(ACTION_RESULT_SCHEMA)', + '- 成功 → `{ "success": true }`;任何步骤失败 → `{ "success": false, "error": "<原因>", "detail": "" }`(**不要**抛错,照实返回即可)。', + ].join('\n') +} +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)}`) } +} + // recoverDirtyWorktreePromptM:branchSetup / milestone 前置的"工作树干净"被打破时的自主恢复(class D 部分)。 // 子代理检查脏文件——全是本阶段合法产物 → 自动 commit 后继续;含越界/不明改动 → 不提交、返回失败让上层 halt。 // **分支护栏(branch)**:自动 commit 只允许发生在目标功能分支上。若当前 HEAD 不在 branch(如里程碑后 HEAD @@ -2075,6 +2110,35 @@ const pending = haltedAtIdx >= 0 // (decisions:stage 缺值时未停而自主取的默认/解读,供 coding-start / 人工事后审阅,可能含错误假设)。 // 注:decisions 仅覆盖**本次运行实际新跑**的 stage;resume 时被 req-done/milestone tag 跳过的已完成功能, // 其决策不会重新登记于此——需到对应 docs/superpowers/specs|plans/-.md 产物显著位置查阅。 +// 续跑 handoff(best-effort,静默):把本次运行结果落进 docs/superpowers/RESUME.md, +// 使下次 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') + : ' - (本次运行无自主默认记录)' +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 续跑,已完成模块自动跳过。', + ].join('\n')) +} else if (results.length) { + const doneList = results.filter(r => r.status === 'done').map(r => `\`${r.module}\``).join('、') || '无' + await recordResume([ + '## ✅ 全部完成()', + '', + `- **本次完成模块**:${doneList}`, + '- **本次自主默认决策**:', + decDigest, + ].join('\n')) +} + // 注:顶层 `return` 不是普通 Node ESM 语法;本文件由 Claude Workflow 运行时执行, // 运行时会把脚本体包进 async function,顶层 `return` 是 Workflow 的结果通道。 // 不要把本文件作为 `node workflows/coding.mjs` 直接运行,也不要改成 `export default {...}`,