2026-06-12-parallel-orchestration.md 23.9 KB

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<n>=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<n> = 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(全文机械改动)

  • Step 1: 定义上下文工厂

在 ROOT 校验(:205)之后插入:

// 模块执行上下文:串行/主根模式 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 }
}
  • 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(worktreeCleanPromptMcheckBranchExistsPromptMcheckout*createBranchFrom*checkReqDoneTagPromptMcreateReqDoneTagPromptMreadDocs08Checkbox*writeDocs08Checkbox*、cross-module 三件套、reportPromptrecoverDirtyWorktreePromptM)。

保持直用 ROOT 不穿线(主根专属):routerPromptadjudicatePromptMresumeJournalPromptMledgerBaselinePromptMpreflightPromptMdetectDefaultBranchPromptM、milestone 专用微步骤(checkAlreadyMergedPromptMexecuteMergePromptMreadDocs08FieldPromptMwriteDocs08FieldPromptMcheckTagExistsPromptMcreateTagPromptMfindReportPromptMupdateReportPromptM)。

注意 docs/08 checkbox(approve 后逐功能勾选)发生在模块分支上 → 穿线;docs/08 里程碑字段发生在 merge 后的默认分支上 → 主根专属。两组不要搞混。

  • 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 透传 crunStage/runAction/adjudicate 本身不动(它们不拼 ROOT)。

  • 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)

  • Step 1: recordDecisions 带 sink

recordDecisions(site, decisions, sink = autonomousDecisions)runStage/runAction opts 增加可选 dec 透传给 recordDecisions。模块作用域内的全部 runStage/runAction 调用点传 dec: c.decisions

  • Step 2: 抽取 runModule

现主循环 try 块体(runBranchSetup → 后端段 → 前端段 → report → runMilestone → RESUME flush)整体搬进:

// 永不 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(...) 调用)

  • 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 之后)

  • Step 1: schema

// 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'} } } } } } } } }
  • 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 硬规则强制依赖全部后端模块。

  • 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

  • Kahn 拓扑 → nextWave(remaining, doneSet)remaining 中 deps ⊆ doneSet 的模块(done 集初始含 routed 里 done:true 的模块),按 docs/02 原序取前 maxWidth 个。ready 为空且 remaining 非空 → 图卡死(理论上 Step 3 已排除)→ log 降级全序。

  • 单测思路(纯函数抽到顶部、不依赖 agent):菱形依赖、链式、全独立、环降级四个 case 用注释内联自证(coding.mjs 无法 node --test,逻辑保持 ≤30 行简单可读)。


Phase C — 物理隔离(lane worktree + 版本段 + lane DB)

Task 6: lane worktree 生命周期微步骤

Files:

  • Modify: workflows/coding.mjs(微步骤区 + runBranchSetup)

  • Step 1: 路径约定

laneRoot(moduleId) = ROOT + '-lanes/' + moduleId(主根同级、确定性、不含时间戳;在 ROOT 之外,不脏主树)。

  • 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 留给人工,照实返回失败)。

  • Step 3: runBranchSetup lane 分叉

c.lane != null 时:跳过主根 checkout 逻辑(:1559-1576),改走 createLaneWorktree(分支语义不变:module-<id>);脏树恢复(recoverDirtyWorktreePromptM)作用于 c.root。主根模式走原路径不动。

  • Step 4: 清理策略

模块 milestone 成功后 removeLaneWorktree(bestEffortAction,失败只 log);halt → 保留 lane 供取证,RESUME 条目写明 lane 路径。

Task 7: migration 版本段分配

Files:

  • Modify: workflows/coding.mjs(tddPrompt + 波次执行器)

  • Step 1: 读现有最大版本(微步骤,FIELD_VALUE_SCHEMA 复用)

maxMigrationVersionPromptM()ls ${ROOT}/sql/migrations/V*.sql 解析最大 <n>,无文件返回 0。每个并行波次开跑前读一次(主根,串行点)。

  • Step 2: JS 段分配

vBase(maxV, laneIdx) = (Math.floor(maxV / 100) + 1 + laneIdx) * 100——lane 0 得下一个百段、lane 1 再下一段,确定性、零随机。写入 c.vBase

  • Step 3: tddPrompt 段规则

Schema 改动前置段(:414)改为条件渲染:c.vBase > 0 时 → "V<n>你的专属版本段 [${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 + 支持探测)

  • Step 1: 模板支持 env 覆盖

三个模板统一:const DB_SCHEMA = process.env.ERP_TEST_DB_SCHEMA || db.schema(校验逻辑沿用;缺省行为一字不变)。e2e globalSetup / 后端测试配置若模板内有 schema 引用同步处理。node --test 补 env 覆盖 case。

  • Step 2: featureStageContract lane 注入

c.lane != null 时 contract 追加一条硬约束:「本模块运行在并行 lane:所有测试/建库/种子命令必须前置 ERP_TEST_DB_SCHEMA=<schema>_lane<N>(含派发给子会话的命令)」。schema 名由波次执行器从 config-vars.yaml 读一次(微步骤)后拼好放 c.dbSchema,prompt 直接渲染成品字符串,不让子代理自己拼。

  • 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(主循环替换)

  • Step 1: milestone / RESUME 互斥锁

// 主根串行段互斥: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 包进 withMainRootLockrunCrossModule 的 diff 基准读默认分支(只读)不需要锁。

  • Step 2: 主循环替换为波次执行
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 原因逐条列出。

  • 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"处)

  • pending 从"线性 slice 余量"改为图余量:remaining 中每项标注 blockedBy(未满足的依赖);RESUME halt 条目逐条列出"同波次 halt 模块 + 各自原因 + 保留的 lane 路径 + 待跑模块及其 blockedBy"。

  • coding-start 步骤 4 的 Workflow args 增加 parallel: { modules: true, maxWidth: 3 },并注明逃生口(parallel: { modules:false } 完全回退现行为);横幅文案"按模块顺序"改"按依赖波次"。

  • README 阶段 B 一节同步:调度(LLM 依赖边 + JS 拓扑)/ lane 隔离 / 降级原则三句话。


Phase E — 模块内 spec 预生成

Task 11: featureLoop 头部并行 spec

Files:

  • Modify: workflows/coding.mjs(featureLoop + deriveSpecPrompt)

  • Step 1: 准入与边界

spec 只读 docs/prototype/tokens(不读兄弟 feature 代码)→ 模块内全部未完成 feature 的 spec 可并行预生成。plan 不预生成(它要 Grep 现有代码定位文件,依赖前序 feature 产出)。

  • 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 提取 + 准入判定

  • 微步骤 planFactsPromptM(planPath){ schemaChange: boolean, files: string[] }(impl_file + test_file 全集)。
  • JS 准入:批内两两 files 不相交 ∧ 全部 schemaChange === false ∧ 批宽 ≤ parallel.featureMaxWidth(默认 2)。任何不满足 → 该 feature 留在串行链。改 schema 的 feature 永远串行

Task 13: feature lane 链

  • 执行序:模块内先串行跑完全部 plan(轻)→ 提取 facts → 可并行批:每个 feature 从模块分支 HEAD 建 feat/<id> 分支 + feature worktree → tdd→verify→review→fix 链在 feature lane 内跑(FE feature 额外需要 ERP_PORT_OFFSET env,模板同 Task 8 方式支持)→ 批完成后串行合并回模块分支 → 打 req-done/<id>
  • 合并冲突(文件集不相交下理论不可能,命中即说明 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)。

全计划验证清单

  • node lib/check-workflow-syntax.mjs 每个 Task 后必跑(补 18 替换:node --check 基线恒红)。
  • node --test 'lib/*.test.mjs' 全绿(Task 8 模板测试;node v26 目录形式报 MODULE_NOT_FOUND,等价 glob,111 测试)。
  • 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 条目完整。