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争用、migrationV<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(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 后的默认分支上 → 主根专属。两组不要搞混。
- 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)。
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/01REQ 卡的"依赖接口"字段;演示种子跨模块主键引用 → 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.mjsKahn 拓扑 →
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 包进 withMainRootLock;runCrossModule 的 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_OFFSETenv,模板同 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 条目完整。