# 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 条目完整。