From 5f95dbf0ae61c8885bb8ffeb3fa8cfc18003bd11 Mon Sep 17 00:00:00 2001 From: zichun <26684461+reporkey@users.noreply.github.com> Date: Fri, 12 Jun 2026 22:04:40 +0800 Subject: [PATCH] docs: 并行编排实施计划 + 附录(设计真值,coding.mjs 注释引用'附录补 N'指向此处;附录优先于计划正文) --- docs/superpowers/plans/2026-06-12-parallel-orchestration-addendum.md | 223 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/superpowers/plans/2026-06-12-parallel-orchestration.md | 336 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 559 insertions(+), 0 deletions(-) create mode 100644 docs/superpowers/plans/2026-06-12-parallel-orchestration-addendum.md create mode 100644 docs/superpowers/plans/2026-06-12-parallel-orchestration.md diff --git a/docs/superpowers/plans/2026-06-12-parallel-orchestration-addendum.md b/docs/superpowers/plans/2026-06-12-parallel-orchestration-addendum.md new file mode 100644 index 0000000..860f291 --- /dev/null +++ b/docs/superpowers/plans/2026-06-12-parallel-orchestration-addendum.md @@ -0,0 +1,223 @@ +# Coding 并行化实施附录(设计综合裁决) + +> **本附录优先级高于计划正文《2026-06-12-parallel-orchestration.md》,冲突以附录为准。** +> +> 来源:预置修正点 D1–D5 + 三个镜头评审的全部 findings,逐条裁决后综合(裁决标准:正确性 > 简单性 > 性能;坚持 fail-open to serial——任何调度/隔离环节失败一律降级串行,**绝不**因并行机制新增 halt 点)。关键事实均已对照 workflows/coding.mjs 与 skills/plan/skeleton-gen/templates/ 实测核验(含 `node --check` 基线恒红的复现)。 +> +> 裁决总览:接受 28 条 findings(含跨镜头重复条目合并)+ D1–D5 全部确认(其中 D2/D3/D4/D5 带修正);拒绝 1 条(lane 测试库 moduleId 命名,见 § 十)。 + +--- + +## 一、总开关与入口(修正 D4) + +### 补 1:coding-start 只开模块级并行,features 留闸 + +- **改什么**:`ARGS.parallel` 缺省 → 全串行(D4 前半,确认不变)。coding-start 步骤 4 只传 `parallel: { modules: true, maxWidth: 3 }`,**不传** `features` / `featureMaxWidth`(即维持计划行 12 的 `features: false` 缺省)。`features: true` 的开启动作写成 Task 14 的验收 checkbox——评估通过后才改 coding-start 的 SKILL.md(一行改动)。 +- **为什么**:D4 原文让入口从第一天就传 `features: true`,Task 12-13 一旦合入该 flag 立即生效,绕过计划自设的两道闸(Phase F 题注「A–E 稳定 ≥1 轮」、Task 14「收益 < 20% 冻结」);届时排查问题的人不会想到入口早已埋了开关。 +- **落点**:Task 10(coding-start args 与横幅)、Task 14(新增"开启 features 开关"验收 checkbox)。 + +## 二、Phase F 范围(确认 D1) + +### 补 2:feature 级并行仅限后端 REQ,移除 ERP_PORT_OFFSET + +- **改什么**:D1 照案通过——Phase F 收窄为后端 REQ;Task 13 中 ERP_PORT_OFFSET 相关内容(含模板支持)整体移除;FE 的 feature 级并行收益由 Phase E spec 预生成承担。 +- **为什么**:评审核验成立——后端 REQ 链的 tdd/verify 不起栈、不占固定端口(Spring 测试随机端口);FE feature 的 e2e 要打穿 vite/playwright/后端三层端口配置,复杂度/收益比不成立。 +- **落点**:Phase F 题注、Task 13。 + +## 三、波次前置串行段(修正 D2:补 lane 占用感知) + +### 补 3:主根复位 + prune 不够,必须加 lane 占用感知 + +- **改什么**:D2 的「主根 HEAD 回默认分支 + 树净 + `git worktree prune`」确认保留,但补三点修正: + - (a) **lane 占用感知微步骤**(prune 之外另加):`git -C worktree list --porcelain` 枚举 `-lanes/` 下持有 `module-*` 分支的残留 lane。本次要跑的模块若其分支被某残留 lane 占用:lane 树脏 → 先对**该 lane** 跑 recoverDirtyWorktreePromptM(branch=`module-`)(in-scope 残留 commit 进模块分支,工作不丢);恢复成功或本就净 → `git worktree remove` 该 lane;恢复失败(越界残留)→ 该模块按现行 `branchSetup-dirty-worktree` 同类语义结构化 halt(**等价前移**,非新增 halt 类——串行路径走到 runBranchSetup 也会在同一种脏树上 halt),波次其余模块照常调度。 + - (b) 占用感知前置跑在**每个波次**(含宽 1)开跑前——宽 1 回退路径正是被残留 lane 卡死的高发场景:halt → lane 保留取证 → resume 后剩余模块独占波次(宽 1)→ 主根 legacy 路径 `git checkout module-` 被残留 lane 拒绝(git 实测 fatal: already used by worktree)→ 仲裁三轮全败 → 硬 halt。即"降级回串行"这条兜底路径本身会被残留 lane 毒化。 + - (c) 主根脏树恢复的目标分支传**当前 HEAD 分支**而非默认分支:HEAD 停在 `module-` 且脏 → recoverDirtyWorktree(branch=`module-`) 就地 commit-in-scope 后再 checkout 默认分支(传默认分支会被 coding.mjs:1144 的分支护栏按 dirty-on-wrong-branch 拒绝,造成本可避免的降级/halt);HEAD 已在**默认分支**且脏 → 不在前置段处理(绝不为并行机制新增"对默认分支自动 commit"),本波次降级 maxWidth=1 交给 legacy 路径现有恢复/halt 语义。 + - `git worktree prune` 仅保留用于清理"目录已删"的管理残留——它对目录完好的 lane(恰是 Task 6 Step 4 刻意保留的取证态)毫无作用,不要依赖它。 +- **为什么**:git 实测双向阻塞——分支被主根 checkout 时 `worktree add` 报 fatal(D2 动机,成立);反向残留 lane 持分支时主根 checkout 同样 fatal(D2 未覆盖)。 +- **落点**:Task 9 Step 3(前置串行段微步骤,扩展为含宽 1 波次)、Task 6 Step 4(halt 保留 lane 的注释补一句"resume 时由波次前置占用感知清理")。 + +## 四、migration 版本段(Task 7) + +### 补 4:maxV 改全局并集,laneIdx 保留 + +- **改什么**:maxMigrationVersionPromptM 的输入从"主根 `ls sql/migrations/V*.sql`"改为**全局并集**:主根文件 ∪ `git for-each-ref refs/heads/module-*` 的每个分支跑 `git ls-tree -r --name-only -- sql/migrations`,取全体最大版本号(纯 git 只读、确定性、零时间/随机源)。`vBase(maxV, laneIdx)` 的 laneIdx 保留不改,并在注释写明:并集口径下"halt-resume 后 laneIdx 漂移"**无害**——每个新波次的段基址恒高于一切已知版本(含未合并分支已占用的段),同段绝不二次发放,只产生无害的段空洞。 +- **为什么**:halt 波次收敛 + 保留 lane 意味着 halted 模块的 V 文件已 commit 在未合并的 `module-` 分支上,对主根 ls 不可见;下一波或下次 resume 会把同段重复发放给别的模块,两分支各写 V(文件名不同,git 合并静默通过),Flyway 在"同版本号多文件"上直接报错——正是计划行 23 点名的事故,且发生在两次运行之间,波次内防线救不了。评审另案的"docs02Idx 替代 laneIdx""段账本文件"两个子方案不采纳:并集口径已消除全部撞号路径,再加稳定键/账本是多余复杂度(正确性等同,简单性更差)。 +- **落点**:Task 7 Step 1(prompt 改并集)、Step 2(注释写明漂移无害的依据)。 + +### 补 5:Flyway out-of-order 约定 + +- **改什么**:docs-04 骨架模板锁定段加一条后端约定「Flyway 配置 `out-of-order: true`」;Task 7 Step 3 注释与 README 阶段 B 同步写明:并行版本段下版本号**只保唯一、不保连续、不保合并序**;独立模块 migration 互不引用正是同波次准入条件,乱序 apply 安全。 +- **为什么**:计划行 202「段间空洞无影响」只对**每次重建**的库成立。同波次两模块按完成序 merge,高版本段可能先落地默认分支;任何 inter-merge 窗口被 migrate 过、之后不重建的持久库(docs-04 § 3.5 鼓励的人工验收/演示库正落在该窗口),下一次 migrate 会因低版本号 pending 在 validate 上硬失败,报错形似模块 bug。 +- **落点**:Task 7 Step 3、Task 8(docs-04 模板同批改动)、Task 10(README)。 + +## 五、测试 DB lane 化(修正 D5:三件套 + fail-closed 兜底,Task 8) + +### 补 6:env 必须打穿到 Spring 进程,探测两点联查 + +- **改什么**(D5 修正后确认,三件套): + - (a) **新骨架约定**:docs-04 skeleton 模板锁定段 + tddPrompt 写明——后端 `application*.yml`(含测试 profile)JDBC URL 的 schema 段必须写成 Spring 占位 `${ERP_TEST_DB_SCHEMA:}`(env 缺省回退原 schema,**串行行为一字不变**;gradle test fork 的 JVM 与 bootRun 默认继承 env,env 即天然贯通,无需 SPRING_DATASOURCE_URL 拼装)。 + - (b) **探测两点联查**(替换 Task 8 Step 3 的单 grep):grep 目标项目 `scripts/` 含 `ERP_TEST_DB_SCHEMA` **且** grep `backend/src/**/application*.{yml,properties}` 含 `${ERP_TEST_DB_SCHEMA` 占位;任一不满足 → 本波次降级 maxWidth=1(log 写明原因 + 建议重跑 skeleton 升级,**不 halt**)。 + - (c) lane 注入维持"成品字符串"纪律(Task 8 Step 2 原案):`ERP_TEST_DB_SCHEMA=` 前缀由波次执行器读 config-vars.yaml 拼好渲染进 contract,绝不让子代理自己拼;明示**含派发给嵌套子会话的命令**。 +- **为什么**:实测模板现状——schema 引用只在 setup-test-db / seed-demo-data 两个 Node 脚本;gradle 测试与 bootRun 的 datasource 由 coding 期生成的 application*.yml 写死(全仓无任何 application 模板或 spring.datasource 占位约定),env 传进去无人消费。按计划只改三个模板,Task 8 Step 3 的探测**必然假阳性放行**:setup-test-db 建了 lane 库、两条 lane 的 gradle 测试仍同连共享库互相清库——fail-open 的降级判据必须可信,否则降级形同虚设。评审中"test.mjs run() 注入 SPRING_DATASOURCE_URL + 探测 grep 注入代码特征"的子方案不采纳:tdd 内循环由嵌套子会话直跑 gradle、不经 test.mjs,该注入覆盖不到主要路径;占位约定让 env 在所有 fork 路径天然贯通,更简单且覆盖完整。存量项目(无占位)按 (b) 老老实实降级串行,不另设兜底注入。 +- **落点**:Task 8 Step 1/2/3、tddPrompt(与 Task 7 Step 3 同文件顺带)、docs-04 模板。 + +### 补 7:lane schema 的 fail-closed 兜底 marker + +- **改什么**:波次执行器建好 lane worktree 后,在 lane 根写确定性标记文件 `.tmp/erp-lane-db`(一行 lane schema 名;`.tmp/` 已在 gitignore 模板忽略,绝不会被 recoverDirtyWorktree 判 in-scope 自动 commit)。三个脚本模板取值顺序改为:`process.env.ERP_TEST_DB_SCHEMA || || db.schema`——脚本按自身所在仓库根定位 marker,lane 副本天然 lane 作用域;主根无此文件,串行行为一字不变。lib 模板测试补 marker 优先级 case。明令**禁止**用"改 lane 副本 config-vars.yaml 的 database.schema"兜底(该文件随 git 提交,lane 树一脏会被自动 commit 并随 milestone 合回默认分支,把真 schema 名搞坏)。 +- **为什么**:lane 注入是纯 prompt-only,tdd 主会话还会把命令转述给二级子会话(提示词链两跳),任何一跳丢前缀,setup-test-db 就对共享 schema DROP+CREATE——症状是兄弟 lane 随机红测,烧 testGate attempt 与仲裁预算甚至假 halt。最具毁灭性的操作(DROP)需要确定性兜底,不能只靠提示词。 +- **落点**:Task 8 Step 1(三模板 + lib 测试)、Task 9 Step 3(建 lane 后写 marker 的微步骤)。 + +### 补 8:lane 库命名统一序号制(修正 D3) + +- **改什么**:模块级 lane 库维持 `_lane`(Task 8 Step 2 原案不变);Phase F 的 feature 级测试库由 D3 的 `__f` 改为 **`_lane_f`**(serial 主根跑 feature 并行时 N 取 0)。D3 的"每条 feature 链独立测试库"结论本身确认。 +- **为什么**:moduleId 进 schema 名引入两个不必要问题——64 字符上限需要专门截断规则;截断后长前缀相同的 moduleId 撞名会重新引入互相清库,且只在特定 id 组合下出现、极难排查。另 module id 允许 `-`(/^[A-Za-z0-9_-]+$/),落进 MySQL 标识符/JDBC URL 还要引号/转义处理。序号制后缀固定 ≤ 10 字符、唯一性由「波次内 lane 序号唯一 × 模块内批序号唯一」确定性保证,与模块级口径统一。 +- **落点**:D3 修正、Task 12-13(Phase F)。 + +## 六、起栈类 stage 全局互斥(新增;Task 8/9) + +### 补 9:stackLock——Seed 与 testGate 整段互斥 + +- **改什么**:新增与 withMainRootLock 同构的全局 **stackLock**(promise 链实现,确定性、无时间/随机源)。runModule 内把 **Seed stage 整段**与 **testGate 整段**(test.mjs 第 5 步 e2e 的 Playwright globalSetup 同样按固定端口冷起后端,e2e 段不可单拆则整个 testGate 包锁)各自 `await withStackLock(...)` 包进锁内——同一时刻全仓只有一条链在起栈/占端口。seedGenPrompt 与 gatePrompt 的 lane 注入同步写明:「端口探测/残留 pid 回收**只允许**在持有起栈互斥的本 stage 内执行」。Task 9 Step 3 注释写明取舍:「冷起栈资源(固定端口 + 进程树)全局唯一,相关 stage 串行化而非 lane 化」。 +- **为什么**:Seed 冷起 gradle bootRun 用 config-vars.yaml 固定端口,且 seedGenPrompt:645 明确指示"起栈前按既知端口回收上一次残留 pid"——≥2 宽波次两条链并发进 Seed 时,后到者会把兄弟 lane 正在验证的后端当残留 **kill 掉**(主动互杀,每个 ≥2 宽波次必然踩中的确定性故障,不是小概率 race)。不打穿端口配置(与 D1 的复杂度论证一致),改互斥:Seed/testGate 占模块总时长比例小,串行化损失远低于打穿三层端口配置;并行收益主体(tdd/review/fix,不起栈)完整保留。tdd 内循环(Spring 测试随机端口、不起栈)不进锁。 +- **落点**:Task 9 Step 1(锁定义并列处)、Task 9 Step 3(注释)、Task 8 Step 2(seedGenPrompt/gatePrompt 注入文案)。 + +### 补 10:seedStageContract 同步 lane 注入(Seed 全链用 lane schema) + +- **改什么**:Task 8 Step 2 的 lane 注入范围从"仅 featureStageContract"改为「**所有会触达 DB 的 contract**」:featureStageContract 与 **seedStageContract** 追加同一条成品硬约束(`c.dbSchema` 渲染)——Seed 全链(setup-test-db / gradle bootRun / seed-demo-data / mysql COUNT 对账)在 lane 模式一律带 `ERP_TEST_DB_SCHEMA=` 前缀(bootRun 经补 6 的占位约定吃到 lane schema;COUNT 对账连 lane schema 查询)。Task 8 验证清单加一条 grep 审计:凡 prompt 文本含 `setup-test-db.mjs` / `seed-demo-data.mjs` 字样的 builder,必须含 lane 注入分支、或确属主根专属(行为门只在 frontend-phase 终波宽 1 主根跑,天然豁免)。 +- **为什么**:Seed 是计划自己定义的"第三类 stage"(coding.mjs:580-590 注释明示不套 featureStageContract),按 Task 8 Step 2 字面注入覆盖不到它,而它恰是 DROP+CREATE/起栈/灌种子的大户——并行 lane 中按计划字面会直接 DROP 共享库。统一 lane schema 也与补 7 的 marker 兜底自洽(marker 把 lane 内一切脚本确定性指向 lane 库;若 Seed 按"锁内默认库"跑会与 marker 互相打架)——评审中"锁内段保持默认 schema、不注入 lane env"的替代子方案因此不采纳。 +- **落点**:Task 8 Step 2(含验证清单新增 grep 审计)。 + +## 七、执行器与穿线细节(Task 1/2/9) + +### 补 11:microStepContract 显式 root 入参,仲裁在 lane 上取证 + +- **改什么**:microStepContract 改为 `microStepContract(root)` 显式入参;主根专属 prompts(detectDefaultBranchPromptM、milestone 微步骤等)传 ROOT;runStage/runAction 的 opts 增加 `root`(缺省 ROOT),透传 adjudicate → adjudicatePromptM,使 lane stage 失败触发的仲裁子代理在 `c.root` 上取证。Task 1 Step 2 的两份名单相应改写,消除"microStepContract 列入穿线名单(追加 c、调用方必传),而其主根专属调用方又列入保持名单(无 c 可传)"的自相冲突。 +- **为什么**:lane 内 runStage/runAction 失败的仲裁按原计划钉死主根(`git -C ROOT`)——主根工作树在默认分支(或被兄弟 milestone 切换中),看不到 lane 分支上刚 commit 的 spec/plan/源码,retry/halt 裁决与 guidance 会基于错误树面作出;且名单按字面执行直接报错。 +- **落点**:Task 1 Step 2/3。 + +### 补 12:行为门 recordDecisions 直调收口 + 顶层 decisions 口径 + +- **改什么**:Task 1 Step 3 给 runBehaviorGate/runBehaviorGateOnce 穿线 c 的同时,coding.mjs:1927/:1935/:1953/:1973/:1988 共 5 处 recordDecisions 直调改为 `recordDecisions(site, list, c.decisions)`。顶层 return 的 `decisions` 改为「results 各 r.decisions 拼接 ∪ 全局 sink 残量」;loop-end 的汇总口径写明 =「flush 失败模块的 r.decisions ∪ 全局残量」(同一条决策绝不既丢失又重复)。 +- **为什么**:Task 2 删除 flushedCount/decStart 水位线后,这 5 处直调仍落全局数组——frontend-phase 模块的 per-module RESUME flush 改读 r.decisions 后会漏掉全部行为门决策;顶层 return 也只剩残量,记账悬空。 +- **落点**:Task 1 Step 3、Task 2 Step 1-2。 + +### 补 13:锁非重入纪律(withMainRootLock 与 stackLock 同适用) + +- **改什么**:两把锁的定义注释钉死:「**promise 链非重入**:只允许 runModule 顶层逐段获取(`await withMainRootLock(() => runMilestone(m))`、`await withMainRootLock(() => recordResume(...))`),被包函数及其调用链内**绝不**再调同一把锁;两把锁之间也**绝不**嵌套获取」。loop-end 的两处 recordResume(:2235/:2246,波次循环已结束、本无竞态)统一也走锁——保持单一纪律避免遗忘。 +- **为什么**:promise 链锁嵌套获取即互等死锁,且静默无诊断(Workflow 直接挂死);计划文字「runMilestone + recordResume 包进锁」没有说明获取层级,实现者若 runModule 包一层 + recordResume 函数体自包一层即触发。 +- **落点**:Task 9 Step 1。 + +### 补 14:Task 9 Step 2 伪代码修正 + +- **改什么**:remaining 过滤改为按波次成员集:`const waveIds = new Set(wave.map(m => m.id)); remaining = remaining.filter(m => !waveIds.has(m.id))`(**绝不**以 rs 为键反查——runtime-null 项没有 r.module,按 r.module 匹配会把故障模块留在 remaining,下轮 deps 不满足触发"图卡死降级全序"误重跑)。parallel() 调用处加注释:「结果数组与 thunk 数组按位次对齐(runtime 保证);null 仅表示 runtime 级故障(runModule 永不 throw)」。 +- **落点**:Task 9 Step 2。 + +### 补 15:lane 模式 milestone 前先收口 lane 树 + +- **改什么**:runModule 在调 runMilestone **之前**(仍在 lane 上下文)先对 `c.root` 跑 worktreeClean + recoverDirtyWorktree(branch=`module-`):in-scope 残留 commit 进模块分支、out-of-scope → halt(两个 prompt 已在 Task 1 穿线清单内,只补一个调用点)。runMilestone 自身(主根专属)的 step 1 wt-clean 保留——只管 merge 的机械安全。两个检查各管一棵树。 +- **为什么**:串行模式 milestone 的 wt-clean 同时守住「merge 机械安全」与「被测树 == 被合并树」两件事;lane 模式下它只看主根——lane 里 tdd/fix 留下的未提交文件参与了 testGate(test.mjs 跑在工作树上)却不进 merge,默认分支拿到的是"没测过的子集",正确性缺口无人兜底(残留只会让 removeLaneWorktree 的 bestEffort 删除失败留 log)。 +- **落点**:Task 2 Step 2(runModule 链内调用点)、Task 9 Step 1(与互斥段的相对位置:收口在 lane 上下文,先于取锁)。 + +## 八、调度与种子语义(Task 4/7) + +### 补 16:调度器新增"共享表"边规则 + +- **改什么**:schedulerPrompt 增加一条边规则:「两个模块的演示种子会触达**同一张表**(按 docs/03 的表归属 / docs/08 各模块 `路径:` 字段判定共同表)→ 必须加 fk 或 seed 边(宁串勿并),evidence 写共同表名」。 +- **为什么**:演示种子 1000–9999 主键区间是**按表**隔离的;两个无依赖边的并行模块若都向同一共享表插种子,各自 lane 互看不见对方文件,PK 都从 1000 起取——合并后 seed-demo-data 逐文件 apply 撞主键(data-error),拖到行为门/e2e 才爆、归因困难。共享表本身就是不可并行信号,符合"宁串勿并"偏置。 +- **落点**:Task 4 Step 2。 + +### 补 17:seed NN 约定显式化(允许并行同号) + +- **改什么**:seedGenPrompt 在 `c.vBase > 0`(并行 lane)时条件渲染一条附注:「并行 lane 间**允许同 NN**:NN 仅表观排序,(NN, module_id) 才是唯一键;同波次模块无 seed 依赖边、互不引用,注入按完整文件名字典序应用(seed-demo-data 现状已满足)」。不做 NN 偏移预分配(良性撞号,最小代价方案)。 +- **为什么**:两个并行模块从同一基点各算 NN=max+1 必产同号文件(文件名含 module id,git 无冲突、`_demo_seed_history` 按文件名记账仍确定性工作),但静默违反 prompt 自己声明的"NN=max+1 唯一"字面约定——豁免理由必须落进 prompt/注释,否则实现者各自发挥。 +- **落点**:Task 7(seedGenPrompt 同文件顺带改动)。 + +## 九、验证清单(全计划) + +### 补 18:语法门替换(基线 node --check 恒红) + +- **改什么**:实测基线 `node --check workflows/coding.mjs` 恒红(node v26:`export const meta` 判 ESM → 顶层 return 报 `Illegal return statement`,exit=1)。新增确定性包裹检查脚本(如 `lib/check-workflow-syntax.mjs`:读入文件 → 剥行首 `export ` → 包进 `async function __wf(args, agent, parallel, phase, log) { ... }` → 写 `.tmp/` 临时文件 → `node --check`,透传退出码)。全计划验证清单(计划 :69/:104/:330 等)的 `node --check workflows/coding.mjs` 全部替换为该脚本。 +- **为什么**:恒红的唯一确定性门会让实现者习惯性忽略红灯(掩盖本次并行化引入的真语法错),或误以为自己改坏了文件。 +- **落点**:全计划验证清单、各 Task 验证步。 + +## 十、裁决记录(拒绝与改判) + +**拒绝(1 条):** + +- 「lane 测试库按波次序号命名(`_lane`)与 halt 取证语义冲突,且与 D3 的 moduleId 命名法不一致」——**拒绝**。理由:(1) 波次严格串行执行,`_lane` 不存在并发撞名;halt 后整个运行即停(波次边界 break),lane 库被同号覆盖只发生在**用户主动重跑之后**,取证窗口由用户掌控,且主要取证物是保留的 lane worktree(种子/迁移/证据文件),库内数据本就"不保证存活、保证可复现"(seedGenPrompt 既有哲学);(2) moduleId 命名引入截断规则、截断撞名与 `-` 字符的 MySQL 标识符/JDBC 引号问题(见补 8),是更大的正确性风险面;(3) 命名口径已由补 8 统一为序号制,不存在"两套规则"。正确性等同下取更简单方案。 + +**改判说明(条目接受、其个别子建议被替代):** + +- lens「Seed 阶段完全不在并行隔离设计内」的"锁内段保持默认 schema/默认端口、不注入 lane env"子方案不采纳——与补 7 的 marker 兜底互相打架(marker 会把 lane 内 setup-test-db 确定性指向 lane 库);统一 lane schema(补 10)。stackLock 主张照案采纳(补 9)。 +- lens「migration 版本段按 laneIdx 分配不稳定」的"docs02Idx 替代 laneIdx"与"段账本文件"子建议不采纳——补 4 的全局并集 maxV 已使 laneIdx 漂移无害,再加稳定键/账本是多余复杂度。 +- lens「Task 8 Step 3 探测判据假阳性」的"test.mjs run() 拼 SPRING_DATASOURCE_URL 注入 + 探测 grep 注入代码特征"子方案不采纳——tdd 内循环不经 test.mjs,覆盖不全;改用占位约定 + 两点联查(补 6)。 +- 三条「附录文件不存在」meta findings:接受,以本附录正式落盘为解决;其中对 D1 成立、D5 方向成立的核验结论已并入补 2/补 6。 + +--- + +## 实施偏差记录 + +(后续实现代理在此逐条登记与计划/附录字面不符的取舍:条目格式建议「Task 编号 / 偏差内容 / 原因」。) + +- **Task 1 Step 2 / 穿线名单外的 4 个函数一并穿线 c**:`seedStageContract`、`behaviorReverifyPrompt`、`currentBranchPromptM`、`createFeSkeletonTagPromptM` 不在计划字面名单,但都含 `${ROOT}` 且属模块作用域(Seed 链 lane 化见补 10;后两者由穿线名单内的 runBranchSetup / runFrontendSkeleton 调用)。不穿线则 Step 4 的 grep 审计(残留必须全部落白名单)必然失败,故按审计要求穿线。 +- **Task 1 Step 2 / commitBlock 的 c 接在带默认值的 tail 之后**:签名为 `commitBlock(addPath, msg, tail = '…', c)`;两处用缺省 tail 的调用点显式传 `undefined` 占位(`undefined` 触发参数默认值,`null` 不会)。保持"c 为末位参数"的约定不破例。 +- **Task 3 / `if (!c.lane)` 实现为 `if (c.lane == null)`**:Task 9 伪代码的 lane 序号从 0 起(`lane: i`),`!c.lane` 会把 lane 0 误判为主根模式(竞态改全局 phase + 误走主根分支)。Phase A 串行下二者行为完全等价(lane 恒 null)。 +- **Task 2 Step 2 / 补 15 的 lane 收口调用点已加进 runModule,但以 `if (c.lane != null)` 门控**:补 15 落点指定 Task 2 Step 2,但其语义只对 lane 模式有意义;串行模式下 runMilestone step 1 的 wt-clean 已覆盖同一棵树(主根),无门控会平添一次重复子代理调用、违背"纯重构行为不变"。Phase D 启用 lane 后该段自动生效。 +- **Task 2 Step 1 / runAction 的 opts.dec 仅接受不消费**:ACTION_RESULT_SCHEMA 无 decisions 字段,runAction 内无 recordDecisions 可透传;按计划字面收下 dec(调用点签名与 runStage 统一),函数内注释说明现状。 +- **Task 1/2 验证步 / `node --check workflows/coding.mjs` 替换为 `node lib/check-workflow-syntax.mjs`**:按补 18 执行(基线恒红已复现:node v26 对 `export const meta`+顶层 return 报 Illegal return statement)。包裹检查脚本本次新建(lib/check-workflow-syntax.mjs),`.tmp/` 已加入仓库 .gitignore。 +- **Task 2 Step 3 / 真实项目 resume 对照未执行,checkbox 留空**:非交互子代理环境无真实 ERP 项目与 Workflow 运行时可跑。已用 stub agent/phase/log 对包裹后的脚本做行为冒烟替代验证:① 全 done 项目 → Router 全 skip → 返回 `{results:[],pending:[],decisions:[]}`、phase 仅 'Router';② 单 todo 模块首个微步骤 agent 返回 null → runModule 结构化返回 `{module,status:'halted',reason,decisions}`(永不 throw)、串行 phase 序列 Router,Router,Milestone、halt 后 RESUME 条目照写。真实 resume 对照留待 Phase A 收尾的人工跑批。 +- **Task 2 Step 2 / RESUME flush 成败记账新增模块级 `resumeFlushed` Set**:计划的 runModule 返回形状(`{module,status,reason?,decisions}`)没有 flush 成败位,而补 12 的 loop-end 口径("flush 失败模块的 r.decisions ∪ 全局残量")需要它;用顶层 Set 记账而不扩 runModule 返回形状。附带效果:旧水位线下"模块 A flush 失败、后继模块 B flush 成功会把 A 的决策一并视为已 flush"的漏记缺陷被修复——这正是补 12 口径的要求(绝不既丢失又重复)。 +- **Task 4 Step 2 / docs/02 全序的 order 边以"紧邻前驱链"形态默认输出**:计划只说"默认全保留为 order 边"未定边形态;对待跑序逐对全连接是 O(n²) 边爆炸且让松开几乎不可操作。prompt 写成:默认每个待跑模块对其 docs/02 紧邻前驱出一条 order 边(链式边传递地保留全序);松开某条链边必须对该模块与**全部前序待跑模块**逐对核查"查无 fk/api/seed 证据 ∧ docs/06 无顺序声明",任一对命中证据即对该对补对应 kind 边——语义与计划字面等价、不弱化"保留是默认"。 +- **Task 4 Step 3 / scheduleViolation 增加"重复模块项"检查**:计划字面只列未知 id / 自依赖 / 环;但 deps 数组同 id 出现两项时 annotateDeps 的 Map 会取后值静默丢边,与"宁串勿并"相悖。重复项按同一违例路径处置(adjudicate retry → 仍违例则降级全序),不新增 halt 点。 +- **Task 4 / runScheduler 在 todo 后端模块 ≤1 时跳过 scheduler 子代理**:此时模块间不存在任何可判边(dependsOn 只能指向 done 模块,恒满足),直接按空边集标注(`annotateDeps(todo, [], allBackendIds)`,frontend-phase 硬规则照常生效),与"空边集通过校验"完全等价,省一次取证子代理调用。 +- **Task 5 / `maxWidth` 以顶层 const 提前在 Phase B 定义**:计划把 `const maxWidth = Math.max(1, ARGS?.parallel?.maxWidth ?? 3)` 写在 Task 9 Step 2 伪代码里,但 `nextWave(remaining, doneSet)` 两参签名钉死(Task 9 调用形即两参),宽度上限只能闭包取得。Task 9 实现时直接复用本 const,勿重复定义。 +- **Task 4/5 验证 / 纯函数冒烟以 `.tmp/` 临时脚本替代**:coding.mjs 无法 node --test;除注释内联自证外,已从 coding.mjs 原文正则抽出 serialChainDeps / scheduleViolation / annotateDeps / nextWave 跑 22 个确定性 case(菱形/链式/全独立/环降级 + 全部违例分支 + frontend-phase 硬规则)全绿,脚本即跑即删不入库。 +- **Task 8 Step 1 / scripts-test-template 不做 `DB_SCHEMA = env || db.schema` 字面改动,改为 marker→env 回填**:该模板实测不读 config-vars.yaml、自身不连库(D5 核验:schema 只被 setup-test-db / seed-demo-data 两脚本与后端 application*.yml 占位消费);按补 6/补 7 语义落为「env 缺省时读 `.tmp/erp-lane-db` 回填 `process.env.ERP_TEST_DB_SCHEMA`」,让 gradle test fork 与 e2e globalSetup→bootRun 经 env 继承吃到 lane schema(占位 `${ERP_TEST_DB_SCHEMA:<缺省 schema>}` 消费)。另:三模板在 marker 生效时各打一行留痕 log(env 前缀在命令行上自明,marker 是不可见兜底路径,指错库排查需要痕迹)——仅覆盖路径新增输出,缺省路径行为一字不变。 +- **Task 8 Step 1 / docs-04 模板锁定段顺带落盘补 5 的 `out-of-order: true` 约定**:补 5 落点写明该 docs-04 改动「Task 8 同批」;与补 6 的 application*.yml 占位约定写进同一锁定块(§1.1 后),避免悬空给后续 Task 各自发挥。 +- **Task 8 Step 1 / 新建 `lib/test-template.test.mjs`**:计划 Files 只列 setup-test-db 测试「+ 兄弟测试」,test 模板此前无测试文件;为断言 env/marker 真正贯通到全部子进程(D5 要害,5 个闸门步骤逐一探针)新建。占位 `{{...}}` 以探针脚本相对路径填充(模拟 skeleton-gen Plan 期填充;占位嵌在模板 JS 单引号字符串内,填充值不能带引号)。 +- **Task 8 Step 1 验证 / `node --test lib/` 目录形式在 node v26 报 MODULE_NOT_FOUND,改跑等价 glob `node --test 'lib/*.test.mjs'`**:111 测试全绿(含本次新增 env 覆盖 / marker 优先级 / 贯通 case)。 +- **Task 6 Step 2 / createLaneWorktreePromptM 增加第三参 defaultBranch**:计划字面签名两参,但 prompt 文本的「不存在 → `worktree add -b <默认分支>`」需要默认分支真值;由调用方传 detectDefaultBranchPromptM 的结果(runBranchSetup lane 分叉已如此接线),不让子代理自行探测 main/master——保持单一真值来源与确定性。 +- **Task 6 Step 4 / halt 的 RESUME lane 路径以 halted reason 前缀实现**:runModule 返回形状(`{module,status,reason?,decisions}`)Phase A 已钉死不扩;catch 分支在 `c.lane != null` 时给 reason 前缀 `[lane 保留取证: ] `,RESUME halt 条目与终端 `⛔` 日志(都渲染 reason)即自然携带 lane 路径。lane 路径本身可由 `laneRoot(module.id)` 确定性重算,Task 10 重构图状 pending 记账时可改为结构化字段。 +- **Task 8 Step 2 / lane 注入条件取 `c.dbSchema` 非空而非计划字面的 `c.lane != null`**:注入内容必须渲染成品 schema 名,`c.dbSchema` 为空时渲染只会产出坏约束(`ERP_TEST_DB_SCHEMA=` 空值前缀);Phase D lane 模式两字段同时填充(lane 必有 dbSchema,建 lane 的前置是探测通过),语义等价且对"误建 lane 未填 schema"的异常态更安全。seedStageContract 同步注入(补 10);behaviorGate*/frontendSkeleton 含 setup-test-db/seed-demo-data 字样但属 frontend-phase 终波宽 1 主根专属,按补 10 豁免口径在注释中登记。 +- **Task 8 Step 2-3 / 波次执行器侧接线留给 Task 9,Phase C 先落原语**:`readDbSchemaPromptM()`(config-vars.yaml 读 schema 基底,FIELD_VALUE_SCHEMA)、`laneDbSchema(base, laneIdx)`(补 8 序号制 `_lane` 拼接)、`laneDbSupportPromptM()`(补 6b 两点联查,新增 LANE_DB_PROBE_SCHEMA:`{scriptsEnv, ymlPlaceholder, detail?}` 两布尔独立返回、合取判定与降级 maxWidth=1 留 JS 编排层)均已定义;Task 8 Step 3 checkbox 留空——其字面含波次执行器的降级动作,属 Task 9 Step 3 职责。 +- **Task 7/8(顺带)/ 补 9 的 seedGenPrompt、gatePrompt 起栈互斥注入文案已随 lane 注入落盘**:文案描述编排层语义(「本 stage 整段在全局起栈互斥内执行,端口探测/残留 pid 回收只允许在持有互斥的本 stage 内做」),stackLock 本体按落点留 Task 9 Step 1;串行(c.dbSchema 空)不渲染,行为不变。补 17 的 seed NN 同号豁免按落点以 `c.vBase > 0` 条件渲染进 seedGenPrompt。 +- **Phase C 验证 / 条件渲染冒烟以 `.tmp/` 临时脚本替代**(同 Task 4/5 先例):抽取 laneRoot/vBase/laneDbSchema 与 8 个受改 builder 跑确定性断言(vBase 5 组边界值、lane/串行双态渲染、`${ERP_TEST_DB_SCHEMA:...}` 占位字面量未被模板插值、worktree 三分支命令形)全绿,脚本即跑即删不入库;`node lib/check-workflow-syntax.mjs` 通过。 +- **Task 9 Step 1 / 两把锁以 `makeChainLock()` 工厂统一生成**:计划伪代码为 `let mainRootLock` + 独立函数;补 9 写明 stackLock 与 withMainRootLock 同构,故以工厂去重。语义逐点等价:链式 then 排队、fn 抛错 catch 复位尾指针、调用方拿到的 run 保留原始 rejection(halt 不被锁吞)。补 13 非重入纪律全文写在工厂定义处注释。 +- **Task 9 Step 2 / remaining 过滤以 `consumed` 集实现(补 14 的成员集口径 + 降级裁员细化)**:补 14 的 waveIds 按"nextWave 产出的整波"过滤,但 Step 3 的"本波次降级 maxWidth=1"会把波内未跑成员裁掉——它们必须留在 remaining 等下波,照搬整波 waveIds 会把未跑模块静默吞掉。consumed = 占用感知前移 halt 的成员 ∪ 实际执行的 wave 成员;键仍取自波次成员、绝不以 rs 反查(补 14 本意完整保留)。 +- **Task 9 Step 2 / runtime-null 槽位 reason 带 lane 取证前缀并抢救 decisions**:计划字面 `reason:'runtime-null'`;实现为 `[lane 保留取证: ] runtime-null`(与 runModule catch 同口径,Task 10 的 RESUME halt 条目渲染 reason 即自带 lane 路径)。lane ctx 预构造在 thunk 外(`ctxs[i]`),null 槽位仍能回收已登记的 c.decisions 进补 12 的 loop-end 口径。parallel() 只在 ≥2 宽 lane 波次使用,前缀恒有意义。 +- **Task 9 Step 2 / 串行闸(useParallel=false)不调 runScheduler**:计划行 12 要求缺省"走现行串行路径一行不差",而 runScheduler 会引入 Schedule phase 与一次取证子代理。串行直接 `serialChainDeps(todo)`(nextWave 在链式图上每轮恰出 1 个),执行语义与现行逐模块循环一致。唯一新增:占用感知微步骤按补 3b 字面("每个波次含宽 1")在串行波次也跑——宽 1 回退被残留 lane 毒化正是补 3b 的动机;无残留 lane 时为单次只读枚举、零状态变更。 +- **Task 9 Step 3 / 降级路径回收本次已建 lane**:计划只说"任一失败 → 降级宽 1";若建到一半失败即降级,已建 lane 持有的 module-* 分支会让降级后的宽 1 主根 checkout 直接 fatal(降级路径自我毒化,且本波占用感知已跑过救不回)。prepareParallelWave 失败时对本次已建 lane 逐个 bestEffort 移除(刚建的树必净,移除幂等安全)再返回 null。 +- **Task 9 Step 3 / 补 3a 的"残留 lane 移除失败"并入同一前移 halt 路径**:补 3a 只规定"恢复失败 → halt";恢复成功/本就净但 `git worktree remove` 失败时分支依旧被占,该模块走任何路径都会 fatal→仲裁→halt,故同按 branchSetup-dirty-worktree 口径前移结构化 halt(reason 带 lane 路径),不新增 halt 类。 +- **Task 9 Step 3 / 占用感知枚举自身失败 fail-open 跳过**:补 3 未规定枚举微步骤(listLaneWorktrees)失败的处置;按"绝不因并行机制新增 halt 点"原则 log 后跳过本波感知——残留 lane 若真挡路,由 runBranchSetup 现行 fatal→仲裁→halt 语义兜底(等价不前移)。 +- **Phase D 验证 / 行为冒烟以 `.tmp/` 临时脚本替代**(同前序先例):stub agent/parallel/phase/log 对包裹后脚本跑 27 个确定性断言全绿——①2 宽波次全 done(marker/lane 清理齐全)②同波次一 done 一 halted + 波次边界收敛 + pending blockedBy ③串行闸不调 scheduler/不建 lane/走主根 checkout ④runtime-null 槽位按位次记 halted ⑤lane DB 探测不支持 → 降级宽 1 后逐波推进全 done ⑥净残留 lane 收口复跑 / 脏残留恢复失败前移 halt 且兄弟照常 ⑦双链带 REQ 并行下 gate/seed 持起栈互斥零交错。脚本即跑即删不入库;`node lib/check-workflow-syntax.mjs` 通过。Task 10 的 coding-start SKILL.md 与 README 两个 checkbox 不在本次(仅 coding.mjs 部分)范围。 +- **Task 11 / spec 预生成启用闸 `useSpecPrefetch = ARGS?.parallel?.features === true || ARGS?.parallel?.modules === true`**:计划未给字面闸;按"spec 预生成无物理冲突风险(batch 只写盘不 commit、文件名含 id 不相撞),可与模块并行开关解耦"自主决策——两开关任一开启即启用,前后端 featureLoop 同享。缺省(`parallel` 缺省 / `modules:false` 逃生口且未开 features)全关,维持与 master 串行等价。它**不是** Phase F 的 feature 级并行(tdd/fix 链仍严格串行,那才归 features 闸全权管辖、受 Task 14 收益闸约束)。 +- **Task 11 / `deriveSpecPrompt(id, phase, c, { batch = false } = {})` 是"c 为末位参数"约定的唯一特例**:计划 Step 2 字面签名即 `deriveSpecPrompt(id, phase, c, { batch: true })`(计划字面优先于 Phase A 接口备忘的穿线约定);opts 带默认值,现有 3 参调用点无须改动。特例已在函数头注释登记,后续阶段勿仿。 +- **Task 11 / 统一 commit 微步骤以 bestEffortAction 跑,失败不 halt 而是清空 specById 全量回退主链串行重跑**:计划只说"一个微步骤统一 add+commit"未定失败处置;按附录总则"绝不因并行机制新增 halt 点"——预生成写盘内容留在树上,主链 spec 重跑按 prompt 既有"已存在同 id spec 复用日期前缀/就地 Edit"规则复用产物(resume 语义同源),commit 仍失败由现行 commitBlock 的 halt 语义兜底(halt 留在现行路径,不新增类别)。微步骤自身幂等:`git diff --cached --quiet` 无 staged 变更即视为成功,绝不空 commit 报错。 +- **Task 11 / 预生成加 `items.length >= 2` 与 `todo.length >= 2` 两道阈值**:计划未提阈值;单 feature 时预生成无并行收益、纯多一次统一 commit 微步骤(多烧一次子代理),直接走现行串行路径。2 项中 1 项已 done 时(todo=1)批量 tag check 结果仍被主链消费(doneById 缓存),仅 spec 不进 batch。 +- **Task 11 / 批量 tag check 直用 agent() 而非 agentR,null/失败槽位不预生成**:agentR 的 throw 在 parallel() 内会被折成 null 丢失诊断;批量查失败的项留给主链 agentR 逐项重查(其 halt 语义与现行串行路径逐字一致),且不为完成态未知的项白烧 spec 子代理。 +- **Phase E 验证 / 行为冒烟以 `.tmp/` 临时脚本替代**(同前序先例):抽出 featureLoop / deriveSpecPrompt / commitBlock 原文配 stub 跑 21 个确定性断言全绿——①预生成全成功(tag check 宽 3 + spec 宽 2、主链零 spec 重跑、统一 commit 含全部成功项、doneById 缓存零重查、plan/req-done 照常)②batch 单项失败落 null 回退主链串行(统一 commit 只含成功项)③统一 commit 失败 specById 清空全回退④闸关全串行(零 parallel() 调用、agentR 逐项)⑤单项不预生成⑥todo 单项不 batch 但缓存照常消费⑦deriveSpecPrompt 双态渲染(batch 无任何 commit 命令、非 batch commitBlock 原样)。脚本即跑即删不入库;`node lib/check-workflow-syntax.mjs` 通过、禁用 builtin grep 零命中。 +- **Task 12 / `planFactsPromptM(planPath, c)` 带末位 c**:计划字面签名单参;plan 文件 commit 在模块分支上(lane 模式主根树面可能看不到),按 Phase A 穿线约定走 `microStepContract(c.root)`,c 为末位参数。 +- **Task 12/13 / feature 测试库命名按补 8 落为 `_lane_f`,无截断规则**:D3 原案(计划/任务摘要中的"确定性截断的 `__f`")被补 8 明文改判;实现取 `c.dbSchema`(模块 lane 模式已是 `_lane`)直接缀 `_f`,串行主根 N 取 0(`laneDbSchema(base, 0)`),i = 批内槽位序号。 +- **Task 13 / reviewWithFixLoop 增加尾参 opts `{ flipDocs08 = true }`**:"c 为末位参数"约定的第二个特例(先例 deriveSpecPrompt 的 `{batch}`,同因:带默认值的 opts 不动既有调用点)。动机:docs/08 相邻 REQ 行在并行 feature 分支上各自勾选,批后合并大概率同 hunk 冲突——冲突即触发"丢弃分支 + 串行重跑全链",cosmetic 副作用不配这个代价;feature lane 内跳过勾选,批后收口在模块分支统一补落(仍在 approve 之后、tag 之前,语义不变)。 +- **Task 13 / feature 并行前置补 lane 测试库支持探测(计划 Task 12/13 未列)**:串行主根模式跑 feature 并行前按补 6b 两点联查(laneDbSupportPromptM)+ readDbSchemaPromptM 读基底,任一不满足 → 本模块全部批降级串行(log,不 halt)——并行 feature 链的 tdd 内循环测试若同连共享库会互相清库,降级判据与模块级同源。模块 lane 模式 `c.dbSchema` 非空视为波次前置已探测通过,免重查。 +- **Task 13 / mergeFeatureBranchPromptM 冲突时自动 `git merge --abort`**:与 milestone 合并"冲突留树给人工"刻意相反——此处计划钉死了自动恢复路径(丢弃分支 → 回模块分支串行重跑),abort 恢复干净模块分支是该路径的前置,树面没有人工要救的东西(注释已写明取舍)。合并经 agentR:null 走既有 NULL_AGENT halt(计划只豁免"冲突不 adjudicate",不豁免 runtime 故障;null 时合并是否落地不可知,绝不盲目丢弃分支)。 +- **Task 13 / 批内链失败的处置(计划未规定)按"批边界收敛"落地**:兄弟链全部跑完、成功者完成合并 + 打 tag 落地后,统一抛 `HALT feature-batch`(失败链的 feature lane 整树保留取证,reason 带 `[feature-lane 保留取证: ]` 前缀);同批合并冲突成员的串行重跑此时搁置(模块即将 halt 不再起新链,resume 由 req-done tag 缺失兜底自动重跑)。链失败是真实 stage halt(串行链同样会停),不属 fail-open 豁免之列。 +- **Task 13 / 准入分批为"保 docs/02 原序的贪心连续分批"**:计划未定分批算法。无 facts / 改 schema 的 feature 自成串行项并截断当前批,**绝不跨越它重排**(后续 feature 可能依赖它的 schema/代码产出);批内成员放弃相对顺序(两两不相交 + 无 schema 改动正是顺序无关的准入前提);lane 建失败的退串成员在批后按序补跑。 +- **Task 13 / 模块分支名取 `currentBranchPromptM(c)` 的 HEAD**:featureLoop 不接收 module 形参(Phase A 签名钉死 `featureLoop(items, phase, c)`),feature 分支的基分支与合并目标取 c.root 当前 HEAD(runBranchSetup 已确保在 `module-` 上),不另起约定;读取失败 → 全部批降级串行(fail-open)。 +- **Task 13 / feature lane 路径约定 `<模块工作树根>-feats/`、feature ctx 共享模块 decisions**:路径由 feature id 确定性导出(模块 lane 模式下落在 `-lanes/` 前缀内会被 listLaneWorktrees 枚举,但其分支为 feat/*,占用感知只按 module-* 匹配,互不干扰)。feature ctx 的 decisions 直接引用 `c.decisions`(单线程事件循环 push 原子),RESUME flush 与补 12 口径零改动;vBase 透传仅保字段语义一致(准入已排除 schema 改动,tdd 不取号)。 +- **Task 13 / 执行序改变仅在 features 闸开启时生效**:plan 全集先行(先串行跑完全部 spec+plan 再提取 facts 分批)只在 `useFeatureParallel ∧ phase==='backend' ∧ items.length>=2` 生效;闸关 / FE phase / 单 feature 维持现行逐 feature 交错链。spec/plan/tdd/verify/review/tag 调用点抽为 featureLoop 内共用闭包(isDone/ensureSpec/runPlanStage/runImplChain/tagDone),并行批与串行链 site/label/allowContinue/dec/root 口径同源一字不差。 +- **Phase F 验证 / 行为冒烟以 `.tmp/` 临时脚本替代**(同前序先例):stub agent/parallel/phase/log 对包裹后脚本跑 30 个确定性断言全绿——①并行批 happy path(plan 全集先行、改 schema 项不进批、feature lane 注入 `erp_lane0_f0/_f1`、合并→勾 docs/08→tag 收口有序、lane 内跳过勾选、清理 lane+删分支)②合并冲突丢弃重跑(tdd 二跑落模块树、重跑通过才打 tag)③lane DB 不支持全批降级串行(探测惰性缓存单次)④批内链失败批边界收敛 halt(兄弟先合并落地、失败 lane 保留取证、不起后续串行项)⑤闸关串行等价(零 facts/零 feature 微步骤、逐 feature 交错)。脚本即跑即删不入库;`node lib/check-workflow-syntax.mjs` 通过、禁用 builtin grep 零命中、`${ROOT}` 审计残留全部落主根专属白名单。 + +### 终审遗留修复(F1–F11) + +- **F1 修复**:scheduleViolation 增加 deps 完整性校验——todo 后端模块必须在 deps 恰好逐一覆盖,缺项即违例(处置同其它违例:adjudicate retry → 降级全序,不新增 halt 点);annotateDeps 的 `|| []` 保留为防御并注明完整性校验后理论不可达(仅 0/1 模块免调度的空边集路径会走到,语义恰为"无依赖")。 +- **F2 修复**:featureStageContract 的 lane 注入段(c.dbSchema 非空分支)追加「lane 内禁全量测试入口」硬约束——tdd/verify/fix 只允许目标化测试命令(gradle 按类/按模块过滤、vitest 过滤模式),禁止 `node scripts/test.mjs` / `pnpm e2e:ci` 等含 e2e/冷起全栈的全量入口(固定端口起栈击杀兄弟 lane),全量回归只发生在 test-gate(编排层起栈互斥保护)。 +- **F3 修复**:runScheduler 自 scheduler agent 调用起整体 try/catch,catch 路径 log 后降级 serialChainDeps——fail-open 不再只依赖 agent() 永不 reject 的契约,runtime 抛错不会让 Workflow 顶层崩溃。 +- **F4 修复**:phase('Schedule') 移到「todoBackendIds ≤ 1 早退」分支之后——todo 为空 / 仅 1 个后端模块时完全不调 scheduler 也不切 Schedule phase,resume 全 done 项目的 UI 终态不再从 Router 漂移。 +- **F5 修复**:schedulerPrompt 共享表边规则(规则 5)补方向约定——按 docs/02 § 二顺序**后者依赖前者**的单向边,明令禁止双向各输出一条(成环白白损失并行度)。 +- **F6 修复**:createLaneWorktreePromptM / createFeatureWorktreePromptM 第 1 步改精确匹配语义——按 `git worktree list --porcelain` 行解析 `worktree ` 字段**全等于** path 才算已注册,明令禁止子串包含判断(id 互为前缀的误匹配)。 +- **F7 修复**:microStepContract 的「所有 git 命令必须用 git -C 」补例外说明——除非步骤明示了其它 `-C` 目标(lane / feature worktree 路径,以步骤原文为准),消除与 lane prompt 内 `git -C ` 步骤的字面冲突。 +- **F8 修复**:新增顶层 `laneEnvCache`(null=未探测 / {fail} / {baseSchema})缓存 lane 库 schema 基底 + lane DB 支持度两点联查——首个 ≥2 宽波次探测后复用,成功失败都缓存;agentR 抛错(瞬时 runtime 故障非环境事实)不缓存留下波重探;maxV 不入缓存每波重读。featureBatchRun 侧已有 featPrep 惰性缓存,不在本项范围。 +- **F9 修复**:laneOccupancySweep 的枚举步改直调 agent() 判空——null 安静 log 后 fail-open 跳过本波感知,不再经 agentR(其 null 路径先打 ⛔ HALT 再被 catch 吞掉,稀释「⛔=真 halt」约定);模块级恢复/移除仍走 agentR(那是真结构化 halt)。 +- **F10 修复**:currentBranchPromptM 输出段 `BRANCH_NAME_SCHEMA` 笔误改为实际下发的 `DEFAULT_BRANCH_SCHEMA`。 +- **F11 修复**:spec 预生成 runStage 的 dec 改每项临时 sink(specDecsById),仅当该项 specById 成果被主链 ensureSpec **实际消费**时并入 c.decisions(delete 防二次并入,不重复打 log);thunk 失败落 null 与统一提交失败清空 specById 时连 sink 一起丢弃——主链重跑会重新登记,同一 stage 决策不再记两遍。 diff --git a/docs/superpowers/plans/2026-06-12-parallel-orchestration.md b/docs/superpowers/plans/2026-06-12-parallel-orchestration.md new file mode 100644 index 0000000..49bbd51 --- /dev/null +++ b/docs/superpowers/plans/2026-06-12-parallel-orchestration.md @@ -0,0 +1,336 @@ +# Coding 编排并行化(module 级波次 + feature 级受限并行)实施计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** coding.mjs 的模块主循环与 featureLoop 从全串行改为**受控并行**:LLM 只负责输出"依赖边"(局部、有证据可查的判断),JS 做确定性拓扑波次调度与硬验证;物理隔离(lane worktree + lane 测试 DB + migration 版本段)保证并行链互不污染;任何调度/隔离环节失败一律**降级回现行串行路径**(fail-open to serial),绝不因"想并行"而 halt。 + +**Architecture:** 六个阶段,严格按序落地。A:上下文穿线 + runModule 抽取(纯重构,行为不变,可独立提交验证);B:调度器(LLM 依赖边 + JS 拓扑波次,宽度=1 时与现行为完全一致);C:物理隔离基础设施(lane worktree 生命周期、migration 版本段分配、测试 DB lane 化);D:并行执行器(波次 parallel + milestone 互斥 + halt 波次收敛 + 图状 pending 记账);E:模块内 spec 预生成(便宜收益);F:feature 级受限并行(准入闸:不改 schema + plan 文件集不相交;**A–E 在真实项目稳定跑过 ≥1 轮后再做**)。 + +**Tech Stack:** Node ESM(coding.mjs 是 Claude Workflow 脚本,顶层 return 是结果通道,**不能** `node` 直接跑,语法检查用 `node --check`);Workflow runtime 的 `parallel()` / `agent()`(注意其"失败→null、永不 reject"语义);git worktree(每 worktree 独立 index,对象库/tag 全仓共享);骨架模板改造用 `node --test` 跑 lib 测试。 + +**总开关与降级原则(贯穿全计划):** +- 新增 `args.parallel`(coding-start 透传):`{ modules: true, maxWidth: 3, features: false }`;`parallel` 缺省或 `modules:false` → 走现行串行路径,**一行不差**。 +- 波次宽度为 1 的波次(含 frontend-phase 终波)**不建 lane**,直接在主根走现行路径——并行机器只在"同波次 ≥2 个模块"时启用。 +- 调度失败(schema 违例 / 环 / 仲裁耗尽 / scheduler agent null)→ log 后降级 docs/02 全序串行,**不 halt**。 +- 隔离不可用(存量项目脚本不支持 lane DB env)→ 该模块降级串行(或全程持 DB 互斥锁),**不 halt**。 + +**背景事实(执行者必读):** +- `featureLoop` 的顺序 for-await 是**有意设计**(coding.mjs:1719-1736):共享工作树的 `.git/index.lock` 争用、migration `V=max+1` 撞号、`pipeline()` 吞 HALT throw 三个原因。并行化不是删掉这段注释,而是把这三个前提逐一替换掉(lane worktree / 版本段 / runModule 内部 catch)。 +- `parallel()` 的 thunk 抛错会**静默落为 null、调用本身永不 reject**——所以 runModule 必须在内部 catch 并返回 `{status:'halted', reason}` 结构化结果,绝不依赖 throw 穿越 parallel() 边界(与现主循环 try/catch:2209-2215 的语义对齐)。 +- `ROOT` 是顶层 const(:197-205),被全部 ~50 个 prompt builder 以模板串捕获。并行模块各有自己的 lane 根,**穿线是本计划最大的机械改动面**;不能改成可变全局(async 交错下不安全)。 +- git worktree:每个 worktree 有**独立 index**,跨 worktree 并行 commit 不争 `.git/index.lock`;对象库与 tag 全仓共享(lane 里打的 `req-done/*` 主根立即可见)。worktree 路径必须由 module id 确定性导出(Workflow runtime 禁 `Date.now`/`Math.random`)。 +- 测试 DB 是单一 schema 且 `DROP DATABASE` 重建(skills/plan/skeleton-gen/templates/scripts-setup-test-db-template.mjs:100-101)——两个并行模块的任何测试执行(tdd 内循环 / verify / gate)都会互相清库。lane DB 是模块级并行的**硬前提**,不是优化。 +- migration `V = sql/migrations/ 现有最大版本号 + 1`(tddPrompt:414)。两个并行模块从同一基点分支,各自算出相同的 max+1 → 合并后 Flyway 版本撞号(文件名不同不产生 git 冲突,但 apply 失败)。必须预分配版本段。 +- milestone(merge 到默认分支 + docs/08 字段 + tag + RESUME.md 追加)全部作用于**主根 + 默认分支**(runMilestone:1583-1660、recordResume:1080-1084)——并行下必须全局互斥串行。merge 冲突保持**硬 halt**(设计原则不变,:1604)。 +- 自主决策记账用全局水位线 `decStart = autonomousDecisions.length`(:2155)做逐模块增量 flush——并行交错下水位线语义直接失效,必须改 per-module 收集器。 +- 主循环里的全局 `phase('Backend')` 等调用(:2157-2189)在并行下会互相竞态;agent 级 `opts.phase`(grp)已全覆盖,UI 分组不受影响。 +- Workflow runtime 并发上限 min(16, cores-2);每条模块链同一时刻只占 1 个 agent slot(链内串行),maxWidth=3 远在限额内。 +- ERP 模块依赖链通常很重(基础数据 → 业务模块),实际波次宽度可能只有 2-3——这正是"LLM 出依赖边、宁串勿并"的预期形态,不要为了并行而放松边。 + +--- + +## Phase A — 上下文穿线 + runModule 抽取(纯重构,行为不变) + +> 本阶段完成后跑一次真实项目(或 dry-run 已完成项目的 resume 路径)确认行为与 master 完全一致,再进 Phase B。 + +### Task 1: 引入模块上下文 `C` 并穿线 prompt builders + +**Files:** +- Modify: `workflows/coding.mjs`(全文机械改动) + +- [x] **Step 1: 定义上下文工厂** + +在 ROOT 校验(:205)之后插入: + +```js +// 模块执行上下文:串行/主根模式 root=ROOT、lane=null;并行 lane 模式由 Phase C/D 填充。 +// 所有"模块作用域"的 prompt builder 与 runner 一律经 C 取根,不再直接闭包 ROOT。 +// ROOT 仍保留:仅供"主根专属"操作(milestone merge / docs/08 / RESUME / ledger / preflight)使用。 +function makeCtx(overrides = {}) { + return { root: ROOT, lane: null, dbSchema: '', vBase: 0, decisions: [], ...overrides } +} +``` + +- [x] **Step 2: prompt builder 穿线** + +下列函数追加**末位参数 `c`**(无默认值,调用方必传,漏传会在模板串里渲染出 `undefined`——配合 Step 4 的 grep 审计兜底),函数体内 `${ROOT}` → `${c.root}`: +`featureStageContract` / `commitBlock` / `deriveSpecPrompt` / `planPrompt` / `tddPrompt` / `verifyPrompt` / `reviewPrompt` / `fixPrompt` / `gatePrompt` / `seedGenPrompt` / `behaviorGate*` / `frontendSkeleton*` / `microStepContract` / 全部微步骤 prompt(`worktreeCleanPromptM`、`checkBranchExistsPromptM`、`checkout*`、`createBranchFrom*`、`checkReqDoneTagPromptM`、`createReqDoneTagPromptM`、`readDocs08Checkbox*`、`writeDocs08Checkbox*`、cross-module 三件套、`reportPrompt`、`recoverDirtyWorktreePromptM`)。 + +**保持直用 ROOT 不穿线**(主根专属):`routerPrompt`、`adjudicatePromptM`、`resumeJournalPromptM`、`ledgerBaselinePromptM`、`preflightPromptM`、`detectDefaultBranchPromptM`、milestone 专用微步骤(`checkAlreadyMergedPromptM`、`executeMergePromptM`、`readDocs08FieldPromptM`、`writeDocs08FieldPromptM`、`checkTagExistsPromptM`、`createTagPromptM`、`findReportPromptM`、`updateReportPromptM`)。 + +> 注意 docs/08 **checkbox**(approve 后逐功能勾选)发生在模块分支上 → 穿线;docs/08 **里程碑字段**发生在 merge 后的默认分支上 → 主根专属。两组不要搞混。 + +- [x] **Step 3: runner 穿线** + +`featureLoop(items, phase, c)`、`reviewWithFixLoop(..., c)`、`testGate(module, phase, c)`、`runBranchSetup(module, c)`、`runCrossModule(module, c)`、`runFrontendSkeleton(feItems, c)`、`flipDocs08Checkbox(..., c)`、`runBehaviorGate*(..., c)` 全部接收并向 prompt builder 透传 `c`。`runStage`/`runAction`/`adjudicate` 本身不动(它们不拼 ROOT)。 + +- [x] **Step 4: 审计 + 语法** + +- `node lib/check-workflow-syntax.mjs`(补 18 替换:`node --check` 基线恒红) +- grep 审计:`grep -n '\${ROOT}' workflows/coding.mjs` 的剩余命中必须全部落在 Step 2 列出的"主根专属"函数内,逐一人工核对。 +- grep 审计:`grep -n '(c)\|, c)' workflows/coding.mjs` 抽查 10 个调用点确认没有漏传。 + +### Task 2: 抽取 `runModule` + decisions 记账去全局水位线 + +**Files:** +- Modify: `workflows/coding.mjs`(:2151-2216 主循环、:925-937 recordDecisions) + +- [x] **Step 1: recordDecisions 带 sink** + +`recordDecisions(site, decisions, sink = autonomousDecisions)`;`runStage`/`runAction` opts 增加可选 `dec` 透传给 recordDecisions。模块作用域内的全部 runStage/runAction 调用点传 `dec: c.decisions`。 + +- [x] **Step 2: 抽取 runModule** + +现主循环 try 块体(runBranchSetup → 后端段 → 前端段 → report → runMilestone → RESUME flush)整体搬进: + +```js +// 永不 throw:内部 catch 全部 HALT 并结构化返回——这是它能放进 parallel() 的前提。 +async function runModule(module, c) { + try { + ...(原 try 块体,phase(...) 调用按 Task 3 处理,全部 helper 传 c) + return { module: module.id, status: 'done', decisions: c.decisions } + } catch (e) { + const reason = String(e.message || e) + log(`⛔ HALT — 模块 ${module.id}:${reason}`) + return { module: module.id, status: 'halted', reason, decisions: c.decisions } + } +} +``` + +主循环退化为:`for (const m of todo) { const r = await runModule(m, makeCtx()); results.push(r); if (r.status==='halted') { haltedAtIdx = idx; break } }`。逐模块 RESUME flush 改为读 `r.decisions`(替代 decStart 水位线;`flushedCount` 机制随之删除,loop-end 的"增量 decisions"直接改为"全部未 flush 模块的 decisions 汇总")。 + +- [ ] **Step 3: 行为一致性验证** + +`node lib/check-workflow-syntax.mjs`(补 18 替换);对照 master 跑一次 resume 场景(全 done 项目 → Router 全 skip → 直接 ✅),输出与 RESUME.md 条目逐字段对比。 + +### Task 3: `phase()` 全局调用条件化 + +**Files:** +- Modify: `workflows/coding.mjs`(runModule 内全部 `phase(...)` 调用) + +- [x] runModule 内的 `phase('Backend')` 等改为 `if (!c.lane) phase('Backend')`——串行/主根模式保留现 UI 分组行为;lane 并行下跳过(agent 级 `opts.phase` 已保证分组),避免兄弟模块竞态改全局 phase 状态。 + +--- + +## Phase B — 调度器(LLM 出依赖边 + JS 拓扑波次) + +### Task 4: SCHEDULE_SCHEMA + schedulerPrompt + +**Files:** +- Modify: `workflows/coding.mjs`(schema 区 + Router 之后) + +- [x] **Step 1: schema** + +```js +// SCHEDULE_SCHEMA:调度子代理只输出"依赖边 + 证据",绝不直接输出"并行组"—— +// 并行性是全局属性由 JS 拓扑推导;LLM 只做局部、可引证的判断。kind=uncertain 也算边(宁串勿并)。 +const SCHEDULE_SCHEMA = { type:'object', additionalProperties:false, + required:['deps'], properties:{ deps:{ type:'array', items:{ + type:'object', additionalProperties:false, + required:['id','dependsOn'], + properties:{ + id:{type:'string'}, + dependsOn:{ type:'array', items:{ type:'object', additionalProperties:false, + required:['id','kind','evidence'], + properties:{ + id:{type:'string'}, + kind:{type:'string', enum:['fk','api','seed','order','doc','uncertain']}, + evidence:{type:'string'} } } } } } } } } +``` + +- [x] **Step 2: schedulerPrompt(只读)** + +要点(写进 prompt): +- 输入:`docs/02 § 二`(人定全序——**默认全保留为 order 边,只有当两个模块之间查不到任何 fk/api/seed 证据、且 docs/06 实现策略未声明顺序约束时才可松开**);`docs/03`(跨模块表的 FK 引用 → fk 边);`docs/05`(模块端点被其它模块消费 → api 边);`docs/01` REQ 卡的"依赖接口"字段;演示种子跨模块主键引用 → seed 边。 +- **保守偏置逐字写明**:"不确定就加边(kind=uncertain)。错误串行只损失时间,错误并行损失正确性。" +- 每条边 `evidence` 必须可定位(文件 + 小节/表名/端点)。 +- 只对 **todo 后端模块**输出 deps;`frontend-phase` 不让 LLM 判——JS 硬规则强制依赖全部后端模块。 + +- [x] **Step 3: JS 校验 + 仲裁 + 降级** + +仿 routerViolation 形态(:2108-2128):未知 id / 自依赖 / 环(Kahn 检测)→ adjudicate retry(带 violation guidance 重跑 scheduler)≤ ADJUDICATE_MAX;仍违例 → **log 后降级 docs/02 全序**(todo 顺序链式依赖),不 halt。scheduler agent 返回 null → 同降级。 + +### Task 5: 波次计算 + +**Files:** +- Modify: `workflows/coding.mjs` + +- [x] Kahn 拓扑 → `nextWave(remaining, doneSet)`:`remaining` 中 deps ⊆ doneSet 的模块(done 集初始含 routed 里 `done:true` 的模块),按 docs/02 原序取前 `maxWidth` 个。`ready 为空且 remaining 非空` → 图卡死(理论上 Step 3 已排除)→ log 降级全序。 +- [x] 单测思路(纯函数抽到顶部、不依赖 agent):菱形依赖、链式、全独立、环降级四个 case 用注释内联自证(coding.mjs 无法 node --test,逻辑保持 ≤30 行简单可读)。 + +--- + +## Phase C — 物理隔离(lane worktree + 版本段 + lane DB) + +### Task 6: lane worktree 生命周期微步骤 + +**Files:** +- Modify: `workflows/coding.mjs`(微步骤区 + runBranchSetup) + +- [x] **Step 1: 路径约定** + +`laneRoot(moduleId) = ROOT + '-lanes/' + moduleId`(主根同级、确定性、不含时间戳;在 ROOT 之外,不脏主树)。 + +- [x] **Step 2: 微步骤 prompts(ACTION_RESULT_SCHEMA,全部幂等)** + +- `createLaneWorktreePromptM(branch, path)`:分支已存在 → `git -C ${ROOT} worktree add ${path} ${branch}`;不存在 → `git -C ${ROOT} worktree add -b ${branch} ${path} <默认分支>`;path 已在 `git worktree list` → 校验其 HEAD 分支正确即返回成功(resume)。 +- `removeLaneWorktreePromptM(path)`:`git worktree remove`(脏树时 `--force` **禁用**——脏 lane 留给人工,照实返回失败)。 +- [x] **Step 3: runBranchSetup lane 分叉** + +`c.lane != null` 时:跳过主根 checkout 逻辑(:1559-1576),改走 createLaneWorktree(分支语义不变:`module-`);脏树恢复(recoverDirtyWorktreePromptM)作用于 `c.root`。主根模式走原路径不动。 + +- [x] **Step 4: 清理策略** + +模块 milestone 成功后 `removeLaneWorktree`(bestEffortAction,失败只 log);halt → **保留 lane 供取证**,RESUME 条目写明 lane 路径。 + +### Task 7: migration 版本段分配 + +**Files:** +- Modify: `workflows/coding.mjs`(tddPrompt + 波次执行器) + +- [x] **Step 1: 读现有最大版本(微步骤,FIELD_VALUE_SCHEMA 复用)** + +`maxMigrationVersionPromptM()`:`ls ${ROOT}/sql/migrations/V*.sql` 解析最大 ``,无文件返回 0。每个**并行波次开跑前**读一次(主根,串行点)。 + +- [x] **Step 2: JS 段分配** + +`vBase(maxV, laneIdx) = (Math.floor(maxV / 100) + 1 + laneIdx) * 100`——lane 0 得下一个百段、lane 1 再下一段,确定性、零随机。写入 `c.vBase`。 + +- [x] **Step 3: tddPrompt 段规则** + +Schema 改动前置段(:414)改为条件渲染:`c.vBase > 0` 时 → "`V` 取**你的专属版本段 [${c.vBase}, ${c.vBase + 99}]** 内现有最大 +1(段内无文件则取 ${c.vBase});**绝不**使用段外版本号";否则维持现行 max+1 文案。 +- 注释写明依据:独立模块(无依赖边才会同波次)的 migration 互不引用,版本相对顺序任意;lane DB 每次从零重建 + 合并后全量按版本序 apply,段间空洞无影响。 + +### Task 8: 测试 DB lane 化 + +**Files:** +- Modify: `skills/plan/skeleton-gen/templates/scripts-setup-test-db-template.mjs` +- Modify: `skills/plan/skeleton-gen/templates/scripts-test-template.mjs` +- Modify: `skills/plan/skeleton-gen/templates/scripts-seed-demo-data-template.mjs` +- Modify: `lib/setup-test-db-template.test.mjs`(+ 兄弟测试) +- Modify: `workflows/coding.mjs`(featureStageContract + 支持探测) + +- [x] **Step 1: 模板支持 env 覆盖** + +三个模板统一:`const DB_SCHEMA = process.env.ERP_TEST_DB_SCHEMA || db.schema`(校验逻辑沿用;缺省行为一字不变)。e2e globalSetup / 后端测试配置若模板内有 schema 引用同步处理。`node --test` 补 env 覆盖 case。 + +- [x] **Step 2: featureStageContract lane 注入** + +`c.lane != null` 时 contract 追加一条硬约束:「本模块运行在并行 lane:**所有**测试/建库/种子命令必须前置 `ERP_TEST_DB_SCHEMA=_lane`(含派发给子会话的命令)」。schema 名由波次执行器从 config-vars.yaml 读一次(微步骤)后拼好放 `c.dbSchema`,prompt 直接渲染成品字符串,不让子代理自己拼。 + +- [x] **Step 3: 存量项目探测降级** + +波次执行器在建 lane 前派只读微步骤 grep 目标项目 `scripts/` 是否含 `ERP_TEST_DB_SCHEMA`:不支持 → 本波次降级 maxWidth=1(log 写明原因 + 建议重跑 skeleton 升级脚本),**不 halt**。 +> 没有 lane DB 的存量项目,模块级并行收益有限(tdd 内循环测试是大头)——这是降级而非锁等待的原因:与其并行却在 DB 锁上排队,不如明示串行。 + +--- + +## Phase D — 并行执行器 + +### Task 9: runWaves + 全局互斥 + halt 波次收敛 + +**Files:** +- Modify: `workflows/coding.mjs`(主循环替换) + +- [x] **Step 1: milestone / RESUME 互斥锁** + +```js +// 主根串行段互斥:milestone(merge/docs08/tag)与 RESUME 追加都作用于主根+默认分支。 +// promise 链实现,无原生锁可用;fn 抛错不破坏链(catch 复位)。 +let mainRootLock = Promise.resolve() +function withMainRootLock(fn) { + const run = mainRootLock.then(fn) + mainRootLock = run.catch(() => {}) + return run +} +``` + +runModule 内 `runMilestone` + 里程碑后的 `recordResume` 包进 `withMainRootLock`;`runCrossModule` 的 diff 基准读默认分支(只读)不需要锁。 + +- [x] **Step 2: 主循环替换为波次执行** + +```js +const useParallel = ARGS?.parallel?.modules === true +const maxWidth = Math.max(1, ARGS?.parallel?.maxWidth ?? 3) +... +while (remaining.length) { + const wave = nextWave(remaining, doneSet) // Task 5 + const rs = wave.length === 1 || !useParallel + ? [await runModule(wave[0], makeCtx())] // 单宽/关闭 → 主根 legacy 路径 + : await parallel(wave.map((m, i) => () => runModule(m, makeCtx({ + lane: i, root: laneRoot(m.id), dbSchema: laneDb(i), vBase: vb(i) })))) + for (const r of rs.filter(Boolean)) { results.push(r); r.status==='done' && doneSet.add(r.module) } + // parallel() 把 thunk 异常折成 null(runModule 永不 throw,null 只剩 runtime 级故障)→ 记 halted + rs.forEach((r, i) => { if (!r) results.push({ module: wave[i].id, status:'halted', reason:'runtime-null' }) }) + remaining = remaining.filter(m => !rs.some(...m 已跑)) + if (results.some(r => r.status === 'halted')) break // 波次边界收敛:跑完本波,停 +} +``` + +halt 语义:**波次内兄弟跑完**(无取消原语,接受算力浪费),波次边界停下——比现行"单模块即断"粗一档,RESUME 记账(Task 10)必须把同波次全部 halt 原因逐条列出。 + +- [x] **Step 3: 波次前置串行段** + +每个 ≥2 宽波次开跑前(主根,串行):读 maxMigrationVersion(Task 7)→ 读 config-vars.yaml schema 名(Task 8)→ lane DB 支持探测(Task 8 Step 3)→ 建 lane worktrees(Task 6)。任何一步失败 → 本波次降级 maxWidth=1。 + +### Task 10: 图状 pending 记账 + 入口/文档 + +**Files:** +- Modify: `workflows/coding.mjs`(收尾段 :2218-2256) +- Modify: `skills/coding/coding-start/SKILL.md`(步骤 4 args + 横幅) +- Modify: `README.md`(阶段 B 描述"功能链顺序 for-await,单工作树串行 commit"处) + +- [x] pending 从"线性 slice 余量"改为图余量:`remaining` 中每项标注 `blockedBy`(未满足的依赖);RESUME halt 条目逐条列出"同波次 halt 模块 + 各自原因 + 保留的 lane 路径 + 待跑模块及其 blockedBy"。 +- [x] coding-start 步骤 4 的 Workflow args 增加 `parallel: { modules: true, maxWidth: 3 }`,并注明逃生口(`parallel: { modules:false }` 完全回退现行为);横幅文案"按模块顺序"改"按依赖波次"。 +- [x] README 阶段 B 一节同步:调度(LLM 依赖边 + JS 拓扑)/ lane 隔离 / 降级原则三句话。 + +--- + +## Phase E — 模块内 spec 预生成 + +### Task 11: featureLoop 头部并行 spec + +**Files:** +- Modify: `workflows/coding.mjs`(featureLoop + deriveSpecPrompt) + +- [x] **Step 1: 准入与边界** + +spec 只读 docs/prototype/tokens(不读兄弟 feature 代码)→ 模块内全部未完成 feature 的 spec 可并行**预生成**。plan 不预生成(它要 Grep 现有代码定位文件,依赖前序 feature 产出)。 + +- [x] **Step 2: 批量模式** + +`deriveSpecPrompt(id, phase, c, { batch: true })`:batch 下 commitBlock 替换为「**只写盘不 commit**」(避免同 worktree 并行 commit 争 index)。featureLoop 入口:tag check 并行 → 未完成项 `parallel()` 跑 spec(runStage 包裹放进 thunk,**thunk 内 catch**,失败项落 null)→ 一个微步骤统一 `git add docs/superpowers/specs && commit`。 +- 成功项结果存 `specById`,主链 for-await 直接消费 artifactPath、跳过 spec stage;null 项回退主链内重跑 spec(现行路径,含 commit)。 +- resume 语义不变:spec prompt 已有"存在同 id spec 复用日期前缀"规则。 + +--- + +## Phase F — feature 级受限并行(A–E 稳定后另行启动) + +> 准入闸是本阶段的灵魂:**LLM 提议、JS 验证**。plan 本来就锁文件边界——文件集相交与否是确定性可算的,不信 LLM 单方判断。 + +### Task 12: PLAN_FACTS 提取 + 准入判定 + +- [x] 微步骤 `planFactsPromptM(planPath)` → `{ schemaChange: boolean, files: string[] }`(impl_file + test_file 全集)。 +- [x] JS 准入:批内两两 `files` 不相交 ∧ 全部 `schemaChange === false` ∧ 批宽 ≤ `parallel.featureMaxWidth`(默认 2)。任何不满足 → 该 feature 留在串行链。改 schema 的 feature **永远串行**。 + +### Task 13: feature lane 链 + +- [x] 执行序:模块内先串行跑完全部 plan(轻)→ 提取 facts → 可并行批:每个 feature 从模块分支 HEAD 建 `feat/` 分支 + feature worktree → tdd→verify→review→fix 链在 feature lane 内跑(FE feature 额外需要 `ERP_PORT_OFFSET` env,模板同 Task 8 方式支持)→ 批完成后**串行**合并回模块分支 → 打 `req-done/`。 +- [x] 合并冲突(文件集不相交下理论不可能,命中即说明 facts 提取失真)→ 不 adjudicate:丢弃该 feature 分支,回模块分支**串行重跑**该 feature 全链(log 写明根因)。 + +### Task 14: 收益评估闸 + +- [ ] 在真实项目对照计时(串行 vs 模块并行 vs +feature 并行),feature 级收益 < 20% 则冻结 Task 12-13(review/fix 循环占比高的项目里 feature 并行的边际收益可能撑不起复杂度)。 +- [ ] 评估通过后开启 features 开关:coding-start 步骤 4 args 补传 `features: true`(+ `featureMaxWidth`,一行改动;附录补 1——在此之前入口**绝不**传 features)。 + +--- + +## 全计划验证清单 + +- [x] `node lib/check-workflow-syntax.mjs` 每个 Task 后必跑(补 18 替换:`node --check` 基线恒红)。 +- [x] `node --test 'lib/*.test.mjs'` 全绿(Task 8 模板测试;node v26 目录形式报 MODULE_NOT_FOUND,等价 glob,111 测试)。 +- [x] grep 审计无残留:模块作用域 prompt 内不得有裸 `${ROOT}`(36 处命中全部落主根专属白名单);`Date.now`/`Math.random`/`new Date()` 零引入。 +- [ ] 串行等价性:`parallel: { modules:false }` 下全流程行为与 master 一致(resume 场景对照)。 +- [ ] 降级路径逐一触发验证:scheduler 违例→全序、lane DB 不支持→单宽、环→全序、worktree 建失败→单宽。 +- [ ] 并行冒烟:两个无依赖小模块的真实项目跑通——lane 各自 commit 无争用、V 段不撞、lane DB 互不清库、milestone 串行合并、RESUME 条目完整。 -- libgit2 0.22.2