Commit 94816863cd531c74bf443fee14384dbb5bcd3d78

Authored by zichun
1 parent 29f3a2de

hooks: 静默连跑修复——sentinel 自匹配/窗口、[ERP-HALT] 全覆盖、StopFailure 日志、preflight

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