diff --git a/hooks/hooks.json b/hooks/hooks.json index ef4c4a2..ee414d9 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -14,6 +14,14 @@ { "type": "command", "command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/scripts/auto-continue.sh" } ] } + ], + "StopFailure": [ + { + "matcher": "server_error|overloaded|unknown", + "hooks": [ + { "type": "command", "command": "\"${CLAUDE_PLUGIN_ROOT}\"/hooks/scripts/log-stop-failure.sh" } + ] + } ] } } diff --git a/hooks/scripts/auto-continue.sh b/hooks/scripts/auto-continue.sh index 9f173bf..3d7538a 100755 --- a/hooks/scripts/auto-continue.sh +++ b/hooks/scripts/auto-continue.sh @@ -1,17 +1,13 @@ #!/usr/bin/env bash # Stop 钩子:让 ERP workflow 在回合中途结束时自动续跑,除非命中「终止标记」或「防循环上限」。 -# -# 背景:编码阶段(逐 REQ:spec→plan→tdd→verify→review→docs/08,或 coding.mjs Workflow 之外的 -# 手动推进)做完一段会退回 Claude 默认行为——报告、停下、等用户敲 continue。本钩子在 Stop 事件 -# 拦下这次停止并自动喂「继续推进」,使各子任务一口气跑完,用户无需手敲 continue。 -# -# 设计: -# 1. 终止标记 [ERP-HALT] —— 终止/硬护栏点(全部完成、test-gate 持续红、路径护栏、 -# Workflow 已启动转后台、缺值阻塞等)打印它。出现即放行停下。 -# 2. 防循环 —— 连续「无工具调用」的空转停止累加计数;达到上限放行停下。 -# 正常推进时由 PostToolUse 的 reset-stall-counter.sh 清零(或本钩子自检 transcript 增长清零), -# 故真流程不受限。 -# 3. 否则 —— block 并喂一句「继续推进」指令。 +# 终止标记 [ERP-HALT] 只认 transcript 中 assistant 行内的出现——钩子自己的 reason、 +# 窗口取最近 200 行:兜住「横幅在前、诊断在后」的回合(横幅后每个诊断动作约 2-3 行)。 +# Skill 加载文本都不在 assistant 行,避免自匹配误放行(曾导致钩子实际只能续跑一次)。 +# 防循环:连续「无工具调用」的空转停止计数达上限放行;有工具调用即由 PostToolUse 的 +# reset-stall-counter.sh 清零(唯一可靠的进展信号;不再用 transcript 字节增长启发式—— +# 纯文字空转一轮也会增长数 KB,按字节判进展会让上限永远到不了)。 +# 注意:不按 stop_hook_active 提前退出——本钩子就是要多次续跑,防循环靠空转计数 +# (Claude Code 自身另有连续 block 的硬上限兜底)。 set -u INPUT="$(cat)" @@ -19,46 +15,29 @@ INPUT="$(cat)" SENTINEL='[ERP-HALT]' MAX_STALL=8 -# —— 从 stdin JSON 取字段(不依赖 jq)—— get_str() { printf '%s' "$INPUT" \ | sed -n "s/.*\"$1\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" \ | head -n1 } TRANSCRIPT="$(get_str transcript_path)" -# 反转义 Windows 路径里的 \\ 与 \/ TRANSCRIPT="${TRANSCRIPT//\\\\//}" TRANSCRIPT="${TRANSCRIPT//\\//}" -# 计数器文件(按会话 transcript 路径派生 key,避免多会话互相干扰) KEY="$(printf '%s' "$TRANSCRIPT" | tr -cd 'A-Za-z0-9' | tail -c 40)" [ -z "$KEY" ] && KEY="default" CNT_FILE="${TMPDIR:-/tmp}/erp-autocont-$KEY" -SIZE_FILE="${TMPDIR:-/tmp}/erp-autosize-$KEY" -allow_stop() { rm -f "$CNT_FILE" "$SIZE_FILE" 2>/dev/null; exit 0; } +allow_stop() { rm -f "$CNT_FILE" 2>/dev/null; exit 0; } -# 1) 终止标记出现在最近的 transcript 尾部 → 放行停下 +# 1) 终止标记出现在最近的 assistant 行 → 放行停下 if [ -n "$TRANSCRIPT" ] && [ -f "$TRANSCRIPT" ]; then - if tail -c 16000 "$TRANSCRIPT" 2>/dev/null | grep -qF "$SENTINEL"; then + if tail -n 200 "$TRANSCRIPT" 2>/dev/null | grep '"type":"assistant"' | grep -qF "$SENTINEL"; then allow_stop fi fi -# 2) 进展自复位(自包含,不依赖 PostToolUse):transcript 自上次停止以来显著增长 -# → 说明这一轮做了实质工作(工具调用 + 结果),清零空转计数。 -CUR_SIZE=0 -[ -n "$TRANSCRIPT" ] && [ -f "$TRANSCRIPT" ] && CUR_SIZE="$(wc -c < "$TRANSCRIPT" 2>/dev/null | tr -d ' ')" -case "$CUR_SIZE" in (''|*[!0-9]*) CUR_SIZE=0 ;; esac -LAST_SIZE=0 -[ -f "$SIZE_FILE" ] && LAST_SIZE="$(cat "$SIZE_FILE" 2>/dev/null || echo 0)" -case "$LAST_SIZE" in (''|*[!0-9]*) LAST_SIZE=0 ;; esac -echo "$CUR_SIZE" > "$SIZE_FILE" -if [ $((CUR_SIZE - LAST_SIZE)) -gt 4000 ]; then - rm -f "$CNT_FILE" 2>/dev/null -fi - -# 3) 防循环:连续空转停止达到上限 → 放行停下 +# 2) 防循环:连续空转停止达到上限 → 放行停下(计数由 PostToolUse 在有工具调用时清零) CNT=0 [ -f "$CNT_FILE" ] && CNT="$(cat "$CNT_FILE" 2>/dev/null || echo 0)" case "$CNT" in (''|*[!0-9]*) CNT=0 ;; esac @@ -67,6 +46,6 @@ if [ "$CNT" -ge "$MAX_STALL" ]; then fi echo $((CNT + 1)) > "$CNT_FILE" -# 拦下停止 → 续跑 -printf '%s' '{"decision":"block","reason":"继续推进 ERP 编码流程:直接执行下一步——若上一个 REQ/任务已完成且 docs/08 已勾,立即开始下一个未完成 REQ 的完整生命周期(spec→plan→TDD→verify→review→docs/08);若当前 skill 刚结束,用 Skill 工具调用链中的下一个 skill 或对应派发器。不要输出收尾叙述、不要停下问用户是否继续。只有打印了 [ERP-HALT] 的终止/硬护栏点(全部完成、测试闸持续红、缺值阻塞、Workflow 已转后台)才应停下。"}' +# 3) 拦下停止 → 续跑 +printf '%s' '{"decision":"block","reason":"继续推进 ERP 编码流程,不要输出收尾叙述、不要停下问用户是否继续。若 coding Workflow 正在后台运行或刚返回通知,按其结果继续诊断/续跑;只有打印了终止标记的硬护栏点(全部完成、测试闸持续红、缺值阻塞、Workflow 已转后台等待)才应停下。"}' exit 0 diff --git a/hooks/scripts/log-stop-failure.sh b/hooks/scripts/log-stop-failure.sh new file mode 100755 index 0000000..04e5778 --- /dev/null +++ b/hooks/scripts/log-stop-failure.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# StopFailure 钩子:回合因 API 错误终止时记一条日志(官方语义:该事件输出与退出码被忽略、 +# 无 decision 控制,"Used for side effects like logging or cleanup"——无法在此自动续跑; +# 如官方未来改变该事件语义再考虑续跑。核实于 2026-06-11 code.claude.com/docs/en/hooks.md: +# "StopFailure | No | Output and exit code are ignored" +# 用途:无人值守长跑被网络错误打断时(曾停摆 14 小时无人接管),事后可从日志确认中断时间 +# 与错误类型,人工或外部 watcher 据此续跑。 +set -u +INPUT="$(cat)" +LOG="${TMPDIR:-/tmp}/erp-stopfailure.log" +ts="$(date '+%Y-%m-%d %H:%M:%S')" +etype="$(printf '%s' "$INPUT" | sed -n 's/.*"error_type"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)" +printf '%s StopFailure error_type=%s\n' "$ts" "${etype:-unknown}" >> "$LOG" 2>/dev/null || true +exit 0 diff --git a/skills/coding/coding-start/SKILL.md b/skills/coding/coding-start/SKILL.md index 2dd7e82..0a123d1 100644 --- a/skills/coding/coding-start/SKILL.md +++ b/skills/coding/coding-start/SKILL.md @@ -37,7 +37,7 @@ allowed-tools: Read Glob Workflow Bash(git rev-parse *) Bash(git tag *) ### 步骤 1:确认 docs/08 存在 用 `Glob` 检查 `docs/08-模块任务管理.md`。 -- 不存在 → 输出「⚠️ 项目尚未初始化,请先运行 `/erp-workflow:plan-start`」并**停下**,不启动 Workflow。 +- 不存在 → 输出「⚠️ 项目尚未初始化,请先运行 `/erp-workflow:plan-start`」并**停下**,不启动 Workflow。输出末尾附 `[ERP-HALT]`。 ### 步骤 2:Plan 完成态校验 + 取 projectRoot(HARD GATE) @@ -65,6 +65,7 @@ allowed-tools: Read Glob Workflow Bash(git rev-parse *) Bash(git tag *) 例:[Plan 进度] A5 下游文档生成 未勾 → 先运行 /erp-workflow:plan-start 处理后重新运行 /erp-workflow:coding-start。 + [ERP-HALT] 前置条件不满足,已停下。 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ``` @@ -127,7 +128,7 @@ Workflow({ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ``` -> `[ERP-HALT]` 标记供 Stop 钩子(`hooks/scripts/auto-continue.sh`)识别:Workflow 一旦转后台,主循环应停下等通知,**不**被自动续跑机制误推去重启 coding-start。逐 REQ 手动推进的会话则无此标记,由 Stop 钩子自动续跑,直到打印 `[ERP-HALT]`(全部完成 / 硬护栏)才停。 +> 横幅末行的终止标记供 Stop 钩子(`hooks/scripts/auto-continue.sh`)识别:Workflow 一旦转后台,主循环应停下等通知,**不**被自动续跑机制误推去重启 coding-start。逐 REQ 手动推进的会话则无此标记,由 Stop 钩子自动续跑,直到打印终止标记(全部完成 / 硬护栏)才停。 ### 步骤 6:Workflow 完成/halt 通知返回后——先输出最终状态横幅 diff --git a/skills/plan/add-req/SKILL.md b/skills/plan/add-req/SKILL.md index 31771e9..e7ab833 100644 --- a/skills/plan/add-req/SKILL.md +++ b/skills/plan/add-req/SKILL.md @@ -22,6 +22,7 @@ allowed-tools: Read Write Edit Grep Glob AskUserQuestion Bash(node *) Bash(git * ``` [add-req] ⛔ 初始 Plan(A0~A5)尚未完结,增量入口暂不可用。 请先运行 /erp-workflow:plan-start 完成初始规划,再用 /add-req 追加需求。 +[ERP-HALT] 初始 Plan 未完结,已停下。 ``` ## 步骤 0:台账基线(首次启用 / 老项目迁移) @@ -43,6 +44,7 @@ node ${CLAUDE_PLUGIN_ROOT}/lib/req-ledger.mjs scan [add-req] 已为现有 个需求单元建立台账基线(.req-ledger.json)。 首次基线无法区分存量与新增,故本次不做增量生成。 请在 docs/01 追加 / 修改需求后,再次运行 /erp-workflow:add-req。 + [ERP-HALT] 基线已建立,等待人工追加需求。 ``` (理由:首跑若把存量全当新增,会重复生成已有的 docs/03/05 工件并误重跑整仓。基线建立后,后续改动才能被准确 diff。) - `true` → 进入步骤 1。 @@ -55,7 +57,7 @@ node ${CLAUDE_PLUGIN_ROOT}/lib/req-ledger.mjs scan 解析 `new[]` / `changed[]` / `removed[]`(每项 `{id, kind}`,kind = `req` 后端卡片 / `fe` 前端功能行)。 -- `new` 与 `changed` 均为空 → 打印 `[add-req] 无新增 / 变更需求,无需处理。` **停下**(不提交台账)。 +- `new` 与 `changed` 均为空 → 打印 `[add-req] 无新增 / 变更需求,无需处理。[ERP-HALT]` **停下**(不提交台账)。 - `removed` 非空 → **仅提示、不自动删**:打印 `检测到台账登记但 docs 已移除的单元:<列出>。下线需求请人工同步 docs/02/03/05 并删除对应 tag,本 skill 不自动执行删除。` 然后继续处理 new/changed(removed 不写回台账,留待人工,下次仍会提示)。 ## 步骤 2:校验新增 / 变更 REQ 卡真实数据(仅 kind=req) @@ -141,6 +143,7 @@ node ${CLAUDE_PLUGIN_ROOT}/lib/req-ledger.mjs scan 台账已更新并随全部增量产物 git 提交(工作树干净)。 运行 /erp-workflow:coding-start 增量编码(Router 只跑缺 tag 的模块/功能)。 + [ERP-HALT] 增量处理完成,已停下(不自动进编码)。 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ``` diff --git a/skills/plan/db-design-gen/SKILL.md b/skills/plan/db-design-gen/SKILL.md index 2216929..2d50ab2 100644 --- a/skills/plan/db-design-gen/SKILL.md +++ b/skills/plan/db-design-gen/SKILL.md @@ -68,6 +68,7 @@ allowed-tools: Read Write Edit Grep Glob 产出:docs/03-数据库设计文档.md + REQ 卡片依赖表/模块头涉及表已回填 ⏸ 请审阅 docs/03(业务实体覆盖、字段类型/默认值、索引、语义引用关系、`【人工填写:需用户审阅】` 标注)。 审阅完成后运行:/erp-workflow:plan-start + [ERP-HALT] docs/03 已生成,等待人工审阅。 ``` ## 参考 diff --git a/skills/plan/downstream-gen/SKILL.md b/skills/plan/downstream-gen/SKILL.md index 5add7d9..eb54e7a 100644 --- a/skills/plan/downstream-gen/SKILL.md +++ b/skills/plan/downstream-gen/SKILL.md @@ -77,6 +77,7 @@ allowed-tools: Read Write Edit Glob Grep AskUserQuestion [downstream-gen] ✅ A5 下游文档生成完成(Plan A0~A5 全部完成) 产出:docs/02 / docs/05 / docs/08 § 二 模块清单 / docs/08 § 三 FE 清单;docs/08 § 一 A0~A5 已全勾。 ⏭️ 下一步:运行 /erp-workflow:plan-start 进入终结闸 + [ERP-HALT] Plan 阶段完成,已停下(不自动进编码)。 ``` ## 参考 diff --git a/skills/plan/plan-start/SKILL.md b/skills/plan/plan-start/SKILL.md index 8549503..8aa83fa 100644 --- a/skills/plan/plan-start/SKILL.md +++ b/skills/plan/plan-start/SKILL.md @@ -100,6 +100,7 @@ A 阶段 checkbox 全部 `[x]` 后先跑下面 4 项前移闸门; 全过才放 [docs/04 §零] node 栈缺 e2e 命令 → docs/04-技术规范.md §零 补齐后再次运行 /erp-workflow:plan-start 重新校验。 + [ERP-HALT] 终结闸未过,已停下等人工回填。 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ``` diff --git a/skills/plan/project-init/SKILL.md b/skills/plan/project-init/SKILL.md index 20b407f..e18cb25 100644 --- a/skills/plan/project-init/SKILL.md +++ b/skills/plan/project-init/SKILL.md @@ -23,7 +23,7 @@ allowed-tools: Read Write Glob Edit Skill Bash(node *) Bash(git init) Bash(git r ### B. 依赖检查 -检测 git/mysql/node 是否在 PATH; 缺失则自动安装;如果安装失败再停下并提示用户. +检测 git/mysql/node 是否在 PATH; 缺失则自动安装;如果安装失败打印 `[ERP-HALT] 依赖安装失败,已停下等人工处理。` 并停下,不继续后续步骤. 完成后,在 `docs/08-模块任务管理.md` 中勾选: - ` - [ ] 依赖检查通过` diff --git a/skills/plan/scope-lock/SKILL.md b/skills/plan/scope-lock/SKILL.md index 3523c00..da906fc 100644 --- a/skills/plan/scope-lock/SKILL.md +++ b/skills/plan/scope-lock/SKILL.md @@ -40,6 +40,7 @@ allowed-tools: Read Write Edit Grep Glob Skill AskUserQuestion Bash(mkdir *) - 产出: docs/01-需求清单//{_module.md, .md} - 6 个占位已填真实值;输入/输出字段表为模板示例内容(如需可自行调整)。 - 审阅后选「继续」进 A1 校验. + [ERP-HALT] 需求卡骨架已生成,等待人工填写。 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ``` diff --git a/workflows/coding.mjs b/workflows/coding.mjs index 8e605a9..029f7b5 100644 --- a/workflows/coding.mjs +++ b/workflows/coding.mjs @@ -1097,6 +1097,21 @@ async function ensureLedgerBaseline() { else log(`req-ledger 基线失败(不阻断):${(r && r.error) || ''}`) } catch (e) { log(`req-ledger 基线异常(不阻断):${String(e?.message || e)}`) } } +function preflightPromptM() { + return [ + '# Preflight:编码阶段环境探测(只读探测 + 少量无副作用命令,不改任何文件)', + microStepContract(), + '', + `读 \`${ROOT}/docs/04-技术规范.md § 零\`(锁定技术栈与命令清单),逐项探测本机环境是否满足:`, + '1. **node**:`node --version` 主版本满足 docs/04 要求。', + '2. **JDK**(若后端为 Java):`java -version` 主版本与 docs/04 锁定版本一致;不一致时探测 `/usr/libexec/java_home -v <版本>`(macOS)或 `JAVA_HOME` 是否可解析到正确版本,并在 detail 给出导出 JAVA_HOME 的具体命令。', + '3. **数据库客户端**:docs/04 指明的 DB(如 mysql)客户端命令存在;若 `.env.local` 存在则尝试一次只读连接探测(失败不算硬错,记入 detail 提示)。', + '4. **测试入口**:docs/04 § 零命令清单里 test-gate 用到的脚本文件(如 `scripts/test.mjs`)存在。', + '', + '全部满足 → `{ "success": true }`;任一硬缺失 → `{ "success": false, "error": "<一句话>", "detail": "<逐项列出缺什么、装/配的具体命令>" }`。', + '## 输出(ACTION_RESULT_SCHEMA)', + ].join('\n') +} // recoverDirtyWorktreePromptM:branchSetup / milestone 前置的"工作树干净"被打破时的自主恢复(class D 部分)。 // 子代理检查脏文件——全是本阶段合法产物 → 自动 commit 后继续;含越界/不明改动 → 不提交、返回失败让上层 halt。 @@ -2115,6 +2130,15 @@ log(`coding: ${todo.length}/${routed.modules.length} modules to run`) // P1#3:首跑建需求台账基线(幂等、best-effort),使日后 /add-req 能正确识别增量、免空跑。 await ensureLedgerBaseline() +// Preflight:一次性环境探测。新机器上工具链缺失若拖到 test-gate / 行为门才爆, +// 要先白跑几十分钟编码;这里在起跑线带诊断 halt。 +if (todo.length) { + phase('Router') + const pre = await agentR(preflightPromptM(), { label: 'preflight', phase: 'Router', schema: ACTION_RESULT_SCHEMA }) + if (!pre.success) throw haltError(`HALT preflight-env: ${pre.error || ''}${pre.detail ? '\n' + pre.detail : ''}`) + log('preflight: 环境探测通过') +} + const results = [] let haltedAtIdx = -1 for (const [idx, module] of todo.entries()) {