Commit 94816863cd531c74bf443fee14384dbb5bcd3d78
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 才爆、白跑几十分钟编码。
Showing
11 changed files
with
72 additions
and
39 deletions
hooks/hooks.json
| ... | ... | @@ -14,6 +14,14 @@ |
| 14 | 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 | 1 | #!/usr/bin/env bash |
| 2 | 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 | 11 | set -u |
| 16 | 12 | |
| 17 | 13 | INPUT="$(cat)" |
| ... | ... | @@ -19,46 +15,29 @@ INPUT="$(cat)" |
| 19 | 15 | SENTINEL='[ERP-HALT]' |
| 20 | 16 | MAX_STALL=8 |
| 21 | 17 | |
| 22 | -# —— 从 stdin JSON 取字段(不依赖 jq)—— | |
| 23 | 18 | get_str() { |
| 24 | 19 | printf '%s' "$INPUT" \ |
| 25 | 20 | | sed -n "s/.*\"$1\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" \ |
| 26 | 21 | | head -n1 |
| 27 | 22 | } |
| 28 | 23 | TRANSCRIPT="$(get_str transcript_path)" |
| 29 | -# 反转义 Windows 路径里的 \\ 与 \/ | |
| 30 | 24 | TRANSCRIPT="${TRANSCRIPT//\\\\//}" |
| 31 | 25 | TRANSCRIPT="${TRANSCRIPT//\\//}" |
| 32 | 26 | |
| 33 | -# 计数器文件(按会话 transcript 路径派生 key,避免多会话互相干扰) | |
| 34 | 27 | KEY="$(printf '%s' "$TRANSCRIPT" | tr -cd 'A-Za-z0-9' | tail -c 40)" |
| 35 | 28 | [ -z "$KEY" ] && KEY="default" |
| 36 | 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 | 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 | 36 | allow_stop |
| 45 | 37 | fi |
| 46 | 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 | 41 | CNT=0 |
| 63 | 42 | [ -f "$CNT_FILE" ] && CNT="$(cat "$CNT_FILE" 2>/dev/null || echo 0)" |
| 64 | 43 | case "$CNT" in (''|*[!0-9]*) CNT=0 ;; esac |
| ... | ... | @@ -67,6 +46,6 @@ if [ "$CNT" -ge "$MAX_STALL" ]; then |
| 67 | 46 | fi |
| 68 | 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 | 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 | 37 | ### 步骤 1:确认 docs/08 存在 |
| 38 | 38 | |
| 39 | 39 | 用 `Glob` 检查 `docs/08-模块任务管理.md`。 |
| 40 | -- 不存在 → 输出「⚠️ 项目尚未初始化,请先运行 `/erp-workflow:plan-start`」并**停下**,不启动 Workflow。 | |
| 40 | +- 不存在 → 输出「⚠️ 项目尚未初始化,请先运行 `/erp-workflow:plan-start`」并**停下**,不启动 Workflow。输出末尾附 `[ERP-HALT]`。 | |
| 41 | 41 | |
| 42 | 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 | 65 | 例:[Plan 进度] A5 下游文档生成 未勾 → 先运行 /erp-workflow:plan-start |
| 66 | 66 | |
| 67 | 67 | 处理后重新运行 /erp-workflow:coding-start。 |
| 68 | + [ERP-HALT] 前置条件不满足,已停下。 | |
| 68 | 69 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| 69 | 70 | ``` |
| 70 | 71 | |
| ... | ... | @@ -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 | 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 | 22 | ``` |
| 23 | 23 | [add-req] ⛔ 初始 Plan(A0~A5)尚未完结,增量入口暂不可用。 |
| 24 | 24 | 请先运行 /erp-workflow:plan-start 完成初始规划,再用 /add-req 追加需求。 |
| 25 | +[ERP-HALT] 初始 Plan 未完结,已停下。 | |
| 25 | 26 | ``` |
| 26 | 27 | |
| 27 | 28 | ## 步骤 0:台账基线(首次启用 / 老项目迁移) |
| ... | ... | @@ -43,6 +44,7 @@ node ${CLAUDE_PLUGIN_ROOT}/lib/req-ledger.mjs scan <root> |
| 43 | 44 | [add-req] 已为现有 <N> 个需求单元建立台账基线(.req-ledger.json)。 |
| 44 | 45 | 首次基线无法区分存量与新增,故本次不做增量生成。 |
| 45 | 46 | 请在 docs/01 追加 / 修改需求后,再次运行 /erp-workflow:add-req。 |
| 47 | + [ERP-HALT] 基线已建立,等待人工追加需求。 | |
| 46 | 48 | ``` |
| 47 | 49 | (理由:首跑若把存量全当新增,会重复生成已有的 docs/03/05 工件并误重跑整仓。基线建立后,后续改动才能被准确 diff。) |
| 48 | 50 | - `true` → 进入步骤 1。 |
| ... | ... | @@ -55,7 +57,7 @@ node ${CLAUDE_PLUGIN_ROOT}/lib/req-ledger.mjs scan <root> |
| 55 | 57 | |
| 56 | 58 | 解析 `new[]` / `changed[]` / `removed[]`(每项 `{id, kind}`,kind = `req` 后端卡片 / `fe` 前端功能行)。 |
| 57 | 59 | |
| 58 | -- `new` 与 `changed` 均为空 → 打印 `[add-req] 无新增 / 变更需求,无需处理。` **停下**(不提交台账)。 | |
| 60 | +- `new` 与 `changed` 均为空 → 打印 `[add-req] 无新增 / 变更需求,无需处理。[ERP-HALT]` **停下**(不提交台账)。 | |
| 59 | 61 | - `removed` 非空 → **仅提示、不自动删**:打印 `检测到台账登记但 docs 已移除的单元:<列出>。下线需求请人工同步 docs/02/03/05 并删除对应 tag,本 skill 不自动执行删除。` 然后继续处理 new/changed(removed 不写回台账,留待人工,下次仍会提示)。 |
| 60 | 62 | |
| 61 | 63 | ## 步骤 2:校验新增 / 变更 REQ 卡真实数据(仅 kind=req) |
| ... | ... | @@ -141,6 +143,7 @@ node ${CLAUDE_PLUGIN_ROOT}/lib/req-ledger.mjs scan <root> |
| 141 | 143 | |
| 142 | 144 | 台账已更新并随全部增量产物 git 提交(工作树干净)。 |
| 143 | 145 | 运行 /erp-workflow:coding-start 增量编码(Router 只跑缺 tag 的模块/功能)。 |
| 146 | + [ERP-HALT] 增量处理完成,已停下(不自动进编码)。 | |
| 144 | 147 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| 145 | 148 | ``` |
| 146 | 149 | ... | ... |
skills/plan/db-design-gen/SKILL.md
skills/plan/downstream-gen/SKILL.md
| ... | ... | @@ -77,6 +77,7 @@ allowed-tools: Read Write Edit Glob Grep AskUserQuestion |
| 77 | 77 | [downstream-gen] ✅ A5 下游文档生成完成(Plan A0~A5 全部完成) |
| 78 | 78 | 产出:docs/02 / docs/05 / docs/08 § 二 模块清单 / docs/08 § 三 FE 清单;docs/08 § 一 A0~A5 已全勾。 |
| 79 | 79 | ⏭️ 下一步:运行 /erp-workflow:plan-start 进入终结闸 |
| 80 | + [ERP-HALT] Plan 阶段完成,已停下(不自动进编码)。 | |
| 80 | 81 | ``` |
| 81 | 82 | |
| 82 | 83 | ## 参考 | ... | ... |
skills/plan/plan-start/SKILL.md
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 | 23 | |
| 24 | 24 | ### B. 依赖检查 |
| 25 | 25 | |
| 26 | -检测 git/mysql/node 是否在 PATH; 缺失则自动安装;如果安装失败再停下并提示用户. | |
| 26 | +检测 git/mysql/node 是否在 PATH; 缺失则自动安装;如果安装失败打印 `[ERP-HALT] 依赖安装失败,已停下等人工处理。` 并停下,不继续后续步骤. | |
| 27 | 27 | |
| 28 | 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 | 40 | - 产出: docs/01-需求清单/<module>/{_module.md, <req_id>.md} |
| 41 | 41 | - 6 个占位已填真实值;输入/输出字段表为模板示例内容(如需可自行调整)。 |
| 42 | 42 | - 审阅后选「继续」进 A1 校验. |
| 43 | + [ERP-HALT] 需求卡骨架已生成,等待人工填写。 | |
| 43 | 44 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| 44 | 45 | ``` |
| 45 | 46 | ... | ... |
workflows/coding.mjs
| ... | ... | @@ -1097,6 +1097,21 @@ async function ensureLedgerBaseline() { |
| 1097 | 1097 | else log(`req-ledger 基线失败(不阻断):${(r && r.error) || ''}`) |
| 1098 | 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 | 1116 | // recoverDirtyWorktreePromptM:branchSetup / milestone 前置的"工作树干净"被打破时的自主恢复(class D 部分)。 |
| 1102 | 1117 | // 子代理检查脏文件——全是本阶段合法产物 → 自动 commit 后继续;含越界/不明改动 → 不提交、返回失败让上层 halt。 |
| ... | ... | @@ -2115,6 +2130,15 @@ log(`coding: ${todo.length}/${routed.modules.length} modules to run`) |
| 2115 | 2130 | // P1#3:首跑建需求台账基线(幂等、best-effort),使日后 /add-req 能正确识别增量、免空跑。 |
| 2116 | 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 | 2142 | const results = [] |
| 2119 | 2143 | let haltedAtIdx = -1 |
| 2120 | 2144 | for (const [idx, module] of todo.entries()) { | ... | ... |