From 094033f2537e729b10304bfe057e8133678af3e4 Mon Sep 17 00:00:00 2001 From: yanghl Date: Mon, 15 Jun 2026 07:25:42 +0800 Subject: [PATCH] coding.mjs: 前端 jsdom-only 重叠实现(开关 frontendOverlap,缺省关) --- README.md | 7 +++++++ skills/coding/coding-start/SKILL.md | 2 ++ workflows/coding.mjs | 184 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------ 3 files changed, 181 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 766f610..c413356 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,13 @@ Claude Code 插件:ERP / 后端管理系统全流程开发框架。 → testGate(frontend,全量回归 vitest+playwright,兜底行为 fix 引入的回归) → runMilestone(milestone/frontend-phase) + · 前端 jsdom-only 重叠(可选,parallel.frontendOverlap,缺省关):前端**不依赖后端**的部分 + (骨架 + spec/plan + tdd 的 jsdom 任务 + vitest verify)在独立前端 worktree 与后端波次**并发**, + 打中间 tag fe-code-done/;后端全部 done 后 reconcile(合 default→frontend-phase)+ phase2 + (e2e 任务 + review + req-done + 行为门 + 测试闸 + 里程碑,主根全栈)。重叠期前端无栈/无固定 + 端口/无 DB,端口与 lane 不变量全保住;缺省关 / phase1 失败 / 后端未全 done 回退现行终波路径。 + 设计:docs/superpowers/plans/2026-06-15-frontend-overlap-jsdom.md + 子代理无法弹窗 → 缺值即写阻塞点并 halt(终止态,非对话框);fail-fast 后等人工修复重跑 coding-start 续跑 handoff:主循环结束(halt 或全完成)时 best-effort 追加 docs/superpowers/RESUME.md diff --git a/skills/coding/coding-start/SKILL.md b/skills/coding/coding-start/SKILL.md index 4f2ae7c..4a6e259 100644 --- a/skills/coding/coding-start/SKILL.md +++ b/skills/coding/coding-start/SKILL.md @@ -106,6 +106,8 @@ Workflow({ > `pluginRoot` 供 coding.mjs 调用插件 `lib/req-ledger.mjs` 在首跑时自动建立需求台账基线(P1#3,使日后 `/add-req` 能识别增量、免空跑);缺省则该增强静默跳过。 > > `parallel` 是模块级并行总开关:`modules: true` 时 coding.mjs 先跑调度器(LLM 只输出模块间依赖边,JS 做确定性拓扑波次),同波次 ≥2 个后端模块各自在 lane worktree + lane 测试库 + 专属 migration 版本段内并行推进;任何调度/隔离环节失败一律 fail-open 降级串行,绝不因并行机制 halt。**逃生口**:改传 `parallel: { modules: false }`(或整个不传 `parallel`)即完全回退现行串行路径。`features`(feature 级并行)**刻意不传**——维持缺省关闭,待并行化计划 Task 14 收益评估通过后才在此处开启(附录补 1)。 +> +> `frontendOverlap`(前端 jsdom-only 重叠)**同样刻意不传**——维持缺省关闭。开启(`parallel: { modules: true, maxWidth: 3, frontendOverlap: true }`)时,前端**不依赖后端**的部分(骨架 + spec/plan + tdd 的 jsdom 任务 + vitest verify)在独立前端 worktree 与后端波次**并发**跑,打中间 tag `fe-code-done/`;后端全部 done 后才 reconcile(合 default→frontend-phase)并跑 phase2(e2e 任务 + review + req-done + 行为门 + 测试闸 + 里程碑,主根全栈)。重叠期前端无栈/无固定端口/无 DB,补 8/9/10 不变量与端口/lane 安全全部保住。**风险**:前端对 docs/05 契约写代码,后端编码期若偏离契约,错误拖到 phase2 e2e/行为门才暴露、需返工——故列为显式评估开关,收益/正确性评估通过后才在此处开启(设计:`docs/superpowers/plans/2026-06-15-frontend-overlap-jsdom.md`)。缺省关 / phase1 失败 / 后端未全 done 一律回退现行终波路径,绝不新增 halt 点。 ### 步骤 5:告知用户 Workflow 已启动 diff --git a/workflows/coding.mjs b/workflows/coding.mjs index 75219f6..80d2ff4 100644 --- a/workflows/coding.mjs +++ b/workflows/coding.mjs @@ -512,7 +512,7 @@ function planPrompt(id, phase, specPath, c) { // ---- stage 3:按 plan 逐任务 TDD(原 feature-tdd / fe-feature-tdd)---- // planPath:上游 plan artifactPath;ledger 是 prompt 层的显式自约束(无 harness 强制)。 -function tddPrompt(id, phase, planPath, c) { +function tddPrompt(id, phase, planPath, c, feStage = 'all') { const fe = isFrontend(phase) return [ `# ${fe ? 'fe-feature-tdd' : 'feature-tdd'} — 逐任务 TDD ${id}`, @@ -543,6 +543,14 @@ function tddPrompt(id, phase, planPath, c) { // schema 时 ERP_TEST_DB_SCHEMA 传进去无人消费(脚本支持 ≠ 测试进程支持)。占位 env 缺省回退字面量, // 串行行为一字不变;gradle test fork 与 bootRun 默认继承 env,无需 SPRING_DATASOURCE_URL 拼装。 fe ? '' : '- **数据源 schema 占位约定**:生成 / 修改后端 `application*.yml`(含测试 profile)时,JDBC URL 的 schema 段必须写成 Spring 占位 `${ERP_TEST_DB_SCHEMA:}`(env 缺省回退原 schema,行为不变);**绝不**把 schema 名写死成纯字面量。', + // feStage 任务子集(前端 jsdom-only 重叠;设计 docs/superpowers/plans/2026-06-15-frontend-overlap-jsdom.md)。 + // 'all'(缺省,含全部后端)与现行一字不差;'jsdom'/'e2e' 仅前端 phase1/phase2 传入。 + fe && feStage === 'jsdom' + ? '- **本阶段 = 仅 jsdom 任务(重叠 phase1,不依赖后端)**:只处理 plan 中 `测试先行类型 = jsdom` 的代码任务;`= e2e` 的任务**全部跳过**,并在 `summary` 里以 `deferredE2e: []` 列出(留待 phase2)。测试命令**只用** vitest(`frontend.test_command` / `frontend.unit_test_runner`)。**硬护栏**:本阶段**绝不**运行 Playwright / `pnpm e2e:ci` / `frontend.e2e_command` / 任何冷起全栈或固定端口的 e2e 命令——命中即 `{status:"halt", reason:"jsdom 模式禁跑 e2e(重叠 phase1 后端尚未就绪)"}`。FeStub→真组件替换(见下)**在本阶段完成**。' + : '', + fe && feStage === 'e2e' + ? '- **本阶段 = 仅 e2e 任务(phase2,后端已就绪;jsdom 已在 phase1 绿)**:只处理 plan 中 `测试先行类型 = e2e` 的任务;jsdom 任务**跳过**(phase1 已完成实现与单测)。e2e 基线约束见下;FeStub→真组件替换 phase1 已做,本阶段确认 router 仍指向真组件即可。' + : '', '- 按顺序处理每个代码类任务:(a) 在 `test_file::test_name` 写**失败**测试;(b) **派发 Agent 子会话**跑测试确认失败,子会话只返回 `{command, exit_code, failing_assertion}` JSON;(c) 写**最小**实现使测试通过;(d) 再派子会话确认通过;(e) commit(含 `REQ_ID` / REQ 标签)。', fe ? '- **测试落位(硬约定,对齐 docs/04 § 2.1)**:jsdom 单测用 vitest/jest 一律写到 `frontend/tests/`,目录**镜像** `frontend/src/` 相对路径(如 `src/pages/home/HomePage.tsx` → `tests/pages/home/HomePage.test.tsx`);e2e 类型在 `frontend/e2e/` 写 Playwright(headless)。**绝不**把 `*.test.*` / `*.spec.*` / `__tests__/` / `__mocks__/` / `__smoke__/` 落在 `frontend/src/` 内(交付源码与测试物理分离,同后端 src/main↔src/test)。实现时:色值用 `var(--color-*)`(不硬编码 hex),业务校验按 spec 在 form-level 复刻。' @@ -575,7 +583,7 @@ function tddPrompt(id, phase, planPath, c) { // ---- stage 4:把功能测试派子会话跑,渲染证据(原 feature-verify / fe-feature-verify)---- // specPath:用于复用日期前缀;round:0 = TDD 后初次 verify,1..5 = fix 后 reverify(每轮独立证据文件, // 避免 reverify 覆盖前轮证据)。 -function verifyPrompt(id, phase, implSummary, specPath, round = 0, c) { +function verifyPrompt(id, phase, implSummary, specPath, round = 0, c, feStage = 'all') { const fe = isFrontend(phase) const suffix = round === 0 ? 'verify' : `verify-r${round}` return [ @@ -589,7 +597,14 @@ function verifyPrompt(id, phase, implSummary, specPath, round = 0, c) { implSummary ? `- 上游 TDD 摘要:${implSummary}` : '', '', '## 流程', - fe + fe && feStage === 'jsdom' + ? [ + // 重叠 phase1:只验 jsdom 单测(不依赖后端),e2e 留 phase2。 + `- 测试目标:从 plan 取 \`测试先行类型 = jsdom\` 的 test_file → 拼 vitest/jest 过滤模式。命令从 \`${c.root}/docs/04-技术规范.md § 零 frontend.test_command\` / \`frontend.unit_test_runner\` 取(缺失默认 \`pnpm test:ci\`)。`, + '- 派子会话**只跑 unit(vitest)**,返回 `{ unit:{command,exit_code,passed,failed,failed_list,stdout_excerpt} }`(`stdout_excerpt` ≤ 30 行)。**硬护栏**:本阶段绝不跑 e2e / Playwright / `pnpm e2e:ci`(后端尚未就绪)。', + '- **unit `exit_code != 0` 或 `failed > 0`** → 渲染证据后 halt。', + ].join('\n') + : fe ? [ `- 测试目标:从 plan 取 \`测试先行类型 = jsdom\` 的 test_file → 拼 vitest/jest 过滤模式;\`= e2e\` 的 → 拼 Playwright spec 过滤模式。命令从 \`${c.root}/docs/04-技术规范.md § 零 frontend.test_command\` / \`frontend.e2e_command\` 取(缺失默认 \`pnpm test:ci\` / \`pnpm e2e:ci\`)。`, '- 派子会话依次跑 unit + e2e,子会话只返回结构化 JSON:`{ unit:{command,exit_code,passed,failed,failed_list,stdout_excerpt}, e2e:{...同结构} }`(`stdout_excerpt` ≤ 30 行)。', @@ -1580,6 +1595,34 @@ function createReqDoneTagPromptM(id, phase, c) { ].join('\n') } +// ── 微步骤:前端 jsdom-only 重叠的中间完成态 tag `fe-code-done/`(设计见 +// docs/superpowers/plans/2026-06-15-frontend-overlap-jsdom.md)── +// 语义:phase1 完成(组件代码 + jsdom 绿,e2e/review/req-done 全部延后)。与 req-done 正交、双 tag 续跑链: +// fe-code-done 存在 → phase1 跳过该 FE;req-done 存在 → phase2 跳过;两者皆缺 → phase1 重跑该 FE。 +// 仅在 frontendOverlap 开启时产生;关闭时全程只用 req-done,与现行一字不差。 +function checkFeCodeDoneTagPromptM(id, c) { + return [ + `# tag \`fe-code-done/${id}\` 是否存在(前端 phase1 完成态 dedup 真值)`, + microStepContract(c.root), + '', + `跑 \`git -C ${c.root} tag -l fe-code-done/${id}\`。`, + '## 输出(EXISTS_SCHEMA)', + '- stdout 含完整匹配 → `{ "exists": true }`;为空 → `{ "exists": false }`', + ].join('\n') +} + +function createFeCodeDoneTagPromptM(id, c) { + return [ + `# 打 annotated tag \`fe-code-done/${id}\`(前端 phase1:组件代码 + jsdom 绿,e2e/review 延后)`, + microStepContract(c.root), + '', + `跑 \`git -C ${c.root} tag -a fe-code-done/${id} -m "fe-code(${id}): jsdom green, e2e/review deferred (overlap phase1)"\`。`, + `先用 \`git -C ${c.root} tag -l fe-code-done/${id}\` 检查;已存在则视为成功(幂等)直接返回 success。`, + '## 输出(ACTION_RESULT_SCHEMA)', + '- 成功 / 已存在:`{ "success": true }`;其它失败:`{ "success": false, "error": "" }`', + ].join('\n') +} + // ── 微步骤:批量预生成 spec 的统一提交(Phase E Task 11)── // batch 模式 spec 只写盘不 commit(避免同一工作树并行 commit 争 .git/index.lock);全部预生成 // 落盘后由本微步骤一次性 add+commit 收口。幂等:resume 场景 spec 可能是"复用已提交文件、就地 @@ -1693,6 +1736,24 @@ function executeMergePromptM(defaultBranch, branch, phaseId) { ].join('\n') } +// reconcile(前端 jsdom-only 重叠 phase1→phase2 边界,设计 docs/superpowers/plans/2026-06-15-frontend-overlap-jsdom.md): +// phase1 的 frontend-phase 分支自**重叠开始时**的 default 分叉,不含后端代码;phase2 的 e2e 要打真后端, +// 故先把已完成的 default(含全部后端 milestone)合并进 frontend-phase。路径互斥(frontend/ vs backend/) +// → 冲突概率极低;冲突按 milestone merge 同口径「留树 halt 给人工」,绝不 abort。幂等:default 已是 +// frontend-phase 祖先(无重叠 / resume 已 merge)→ git "Already up to date" → success。主根专属。 +function mergeDefaultIntoFrontendPromptM(branch, defaultBranch) { + return [ + `# reconcile:把 \`${defaultBranch}\`(已完成后端)合并进 \`${branch}\``, + microStepContract(ROOT), + '', + `1. 跑 \`git -C ${ROOT} checkout ${branch}\` 确保 HEAD 在 ${branch}(phase1 worktree 已移除,分支空闲;若仍被占用则整步失败留人工)。`, + `2. 跑 \`git -C ${ROOT} merge --no-ff ${defaultBranch} -m "merge(reconcile): integrate backend into ${branch} for e2e"\`。`, + '## 输出(ACTION_RESULT_SCHEMA)', + '- 成功 / 已是最新(Already up to date)→ `{ "success": true }`', + '- 合并冲突 / 其它失败 → `{ "success": false, "error": "", "detail": "<冲突文件换行分隔 或 stderr 前 30 行>" }`;**不要**自动 abort / stash / 改文件。', + ].join('\n') +} + function readDocs08FieldPromptM(fe, id) { const section = fe ? '§ 三' : '§ 二' const title = fe @@ -2190,8 +2251,13 @@ const useSpecPrefetch = ARGS?.parallel?.features === true || ARGS?.parallel?.mod // 冲突风险、两开关任一开启即用;feature 级 tdd/fix 链并行只看本闸。 const useFeatureParallel = ARGS?.parallel?.features === true const featureMaxWidth = Math.max(1, ARGS?.parallel?.featureMaxWidth ?? 2) -async function featureLoop(items, phase, c) { +// feStage(前端 jsdom-only 重叠;设计 docs/superpowers/plans/2026-06-15-frontend-overlap-jsdom.md): +// 'all'(缺省)= 现行完整链(spec→plan→tdd→verify→review→req-done),后端恒走此;前端 overlap 关时亦走此。 +// 'jsdom'(overlap phase1)= spec→plan→tdd(仅 jsdom 任务)→verify(仅 vitest)→tag fe-code-done;**不** review、**不** req-done。 +// 'e2e'(phase2,后端已就绪)= dedup 读 fe-code-done → spec/plan(复用)→tdd(仅 e2e 任务)→verify(unit+e2e)→review→req-done。 +async function featureLoop(items, phase, c, feStage = 'all') { const grp = phase === 'backend' ? 'Backend' : 'Frontend' + const isPhase1 = feStage === 'jsdom' // 只推进到 fe-code-done(中间态) // spec 的 artifactPath 校验(主链与批量预生成共用同一份,确保两条路径产物口径一致)。 const specValidate = r => { if (!r.artifactPath) return 'spec 返回 ok 但缺 artifactPath(流程靠它定位 spec 并派生下游日期前缀)' @@ -2285,17 +2351,20 @@ async function featureLoop(items, phase, c) { } // fc:执行上下文(串行 = 模块 ctx c;Phase F 并行批 = feature lane ctx)。rwOpts 透传 // reviewWithFixLoop(feature lane 内跳过 docs/08 勾选,理由见该函数头注释)。 - const runImplChain = async (id, planPath, specPath, fc, rwOpts) => { + // stage(末位,缺省 'all'=现行):透传 tddPrompt/verifyPrompt 的 feStage;'jsdom' 额外跳过 review(phase1 + // 只到 fe-code-done,review→req-done 延到 phase2)。'e2e'/'all' 含 review,一字不差。 + const runImplChain = async (id, planPath, specPath, fc, rwOpts, stage = 'all') => { // tdd allowContinue:false:tddPrompt 的 halt = 路径作用域越界护栏 / 同测试卡死 10 次——硬边界, // 仲裁不得 continue 放行(越界把前端实现混进后端分支 / 卡死等于测试没真过)。 - const impl = await runStage(g => tddPrompt(id, phase, planPath, fc) + g, { + const impl = await runStage(g => tddPrompt(id, phase, planPath, fc, stage) + g, { site:`tdd:${phase}:${id}`, grp, label:`tdd:${phase}:${id}`, allowContinue: false, dec: fc.decisions, root: fc.root, }) // verify allowContinue:false:verifyPrompt 的 halt = 功能测试红色(exit!=0 / failed>0)——与 test-gate 红同级硬边界, // 绝不 continue 放行红色实现进 review→approve→打 req-done tag(否则红色功能被永久标记完成、resume 跳过)。 - const v0 = await runStage(g => verifyPrompt(id, phase, impl.summary || '', specPath, 0, fc) + g, { + const v0 = await runStage(g => verifyPrompt(id, phase, impl.summary || '', specPath, 0, fc, stage) + g, { site:`verify:${phase}:${id}`, grp, label:`verify:${phase}:${id}`, allowContinue: false, dec: fc.decisions, root: fc.root, }) + if (stage === 'jsdom') return // overlap phase1:jsdom 绿即止,不 review(review→req-done 留 phase2) const reviewResult = await reviewWithFixLoop(id, phase, v0, specPath, fc, rwOpts) log(`review approved ${phase}:${id} after ${reviewResult.rounds} round(s)`) } @@ -2304,6 +2373,15 @@ async function featureLoop(items, phase, c) { const tagDone = async (id) => runAction(g => createReqDoneTagPromptM(id, phase, c) + g, { site:`req-done-tag:${phase}:${id}`, grp, label:`reqdone:${phase}:${id}`, dec: c.decisions, root: c.root, }) + // overlap phase1 中间完成态 tag:fe-code-done/(组件代码 + jsdom 绿)。 + const tagFeCodeDone = async (id) => runAction(g => createFeCodeDoneTagPromptM(id, c) + g, { + site:`fe-code-done-tag:${id}`, grp, label:`fecodedone:${id}`, dec: c.decisions, root: c.root, + }) + // phase1 入口 dedup:fe-code-done/ 已存在 → 本 FE phase1 已完成,跳过(phase2 用 req-done dedup,走 isDone)。 + const isPhase1Done = async (id) => { + const r = await agentR(checkFeCodeDoneTagPromptM(id, c), {label:`fecodecheck:${phase}:${id}`, phase: grp, schema: EXISTS_SCHEMA}) + return r.exists + } // ── Phase F(Task 12/13):后端 REQ 的 feature 级受限并行(设计见 featureBatchRun 头注释)。 // frontend phase 不进准入(附录补 2);闸关 / 单 feature 时走下方现行串行链,一字不差。 @@ -2313,11 +2391,15 @@ async function featureLoop(items, phase, c) { } for (const id of items) { - if (await isDone(id)) { log(`featureLoop skip ${phase}:${id} — tag req-done/${id} 已存在`); continue } + // dedup:phase1 用 fe-code-done;'all'/'e2e'(phase2)用 req-done。phase2 的 fe-code-done 必已存在 + // (phase1 落过),spec/plan 经 ensureSpec/runPlanStage 复用既有产物,tdd(e2e) 只处理延后的 e2e 任务。 + if (isPhase1 ? await isPhase1Done(id) : await isDone(id)) { + log(`featureLoop skip ${phase}:${id} — tag ${isPhase1 ? 'fe-code-done' : 'req-done'}/${id} 已存在`); continue + } const spec = await ensureSpec(id) const plan = await runPlanStage(id, spec) - await runImplChain(id, plan.artifactPath, spec.artifactPath, c) - await tagDone(id) + await runImplChain(id, plan.artifactPath, spec.artifactPath, c, undefined, feStage) + await (isPhase1 ? tagFeCodeDone(id) : tagDone(id)) } } @@ -3054,6 +3136,28 @@ const withStackLock = makeChainLock() // phase(...) 全局 UI 分组只在主根模式调用(c.lane == null):lane 并行下兄弟模块会竞态改全局 phase 状态, // 而 agent 级 opts.phase(grp)已全覆盖分组——注意不能写 `!c.lane`(lane 序号 0 是合法 lane,falsy 会误判主根)。 const resumeFlushed = new Set() // per-module RESUME flush 成功记账(替代 flushedCount 水位线;loop-end 据此补记失败模块) + +// ---- runFrontendPhase1:前端 jsdom-only 重叠的 phase1(设计 docs/superpowers/plans/2026-06-15-frontend-overlap-jsdom.md)---- +// 与后端波次循环**并发**跑,**只做不依赖后端**的事:branchSetup(建前端 worktree)→ skeleton → featureLoop(jsdom)。 +// 跑在**独立前端 worktree**(c.lane 非 null、root=前端 lane 路径、dbSchema=''——jsdom 无 DB,featureStageContract +// 的 lane-DB 注入按 c.dbSchema 非空判定,此处不触发),与后端主根/lane 的 git index 互不干扰;**不**调全局 +// phase()(并发下会与后端波次竞态改全局 UI 状态——agent 级 grp='Frontend' 已覆盖分组),**不** merge/milestone/ +// RESUME(全部留 phase2)。**永不 throw**:catch 结构化返回,phase1 halt 不停后端(reason 带前端 worktree 取证前缀)。 +// 幂等(resume):skeleton 凭 fe-skeleton-done、各 FE 凭 fe-code-done 去重,已完成项整段 skip。 +async function runFrontendPhase1(feModule, c) { + try { + await runBranchSetup(feModule, c) + await runFrontendSkeleton(feModule.feItems, c) + await featureLoop(feModule.feItems, 'frontend', c, 'jsdom') + log(`fe-overlap phase1 完成:${feModule.feItems.length} 个 FE 的组件代码 + jsdom 已绿(e2e/review 留 phase2)`) + return { module: 'frontend-phase', status: 'done', decisions: c.decisions } + } catch (e) { + const reason = `[fe-overlap phase1 保留取证: ${c.root}] ${String(e?.message || e)}` + log(`⛔ HALT — fe-overlap phase1:${reason}`) + return { module: 'frontend-phase', status: 'halted', reason, decisions: c.decisions } + } +} + async function runModule(module, c) { try { if (c.lane == null) phase('Milestone') @@ -3082,7 +3186,9 @@ async function runModule(module, c) { // 使逐 FE verify(e2e) 与阶段末尾行为门的可构建前提成立、tddPrompt 的 FeStub→真组件占位替换有真值起点。幂等(fe-skeleton-done tag)。 await runFrontendSkeleton(module.feItems, c) // featureLoop 的 review 循环只做静态验收(reviewer approve 即打 req-done)——行为验收不在内循环。 - await featureLoop(module.feItems, 'frontend', c) + // module.feStage(前端 jsdom-only 重叠):缺省 'all'=现行完整链;overlap phase2 传 'e2e'(phase1 已在 + // 前端 worktree 跑完 jsdom,本处只补 e2e 任务 + review + req-done)。skeleton 上方已幂等(fe-skeleton-done)。 + await featureLoop(module.feItems, 'frontend', c, module.feStage || 'all') // 阶段级行为门(v3):整个前端阶段只跑一次行为验收——起全栈 + 演示/sentinel 种子,按全部 FE spec 聚合 // 作用域并集验「按钮真生效/文字对」;硬问题转 must-fix→fix→单测复验→重跑门(≤BEHAVIOR_STAGE_MAX 轮)。 // 放在 testGate 之前:行为 fix 改动 frontend/ 源码,绿后由 testGate 全量回归兜底,不让回归证据过期。 @@ -3288,6 +3394,24 @@ const results = [] const doneSet = new Set(routed.modules.filter(m => m.done).map(m => m.id)) let remaining = useParallel ? await runScheduler(todo, routed.modules) : serialChainDeps(todo) +// ── 前端 jsdom-only 重叠(设计 docs/superpowers/plans/2026-06-15-frontend-overlap-jsdom.md)── +// frontendOverlap 开 + 存在待跑 frontend-phase(有 feItems)时:把 frontend-phase 从波次图剔除,起 phase1 +// (前端 worktree,只跑 skeleton + jsdom,不依赖后端)与后端波次循环**并发**;屏障(后端全部 done)后再 +// reconcile(合 default→frontend-phase)+ 标准 runModule(feStage='e2e')(主根全栈,保住补 8/9/10)。缺省关 / +// phase1 失败 / 后端未全 done → 完全回退现行终波路径,phase1 产物(fe-code-done)落 tag、resume 续跑,绝不新增 halt 点。 +const useFrontendOverlap = ARGS?.parallel?.frontendOverlap === true +const feModule = routed.modules.find(m => m.id === 'frontend-phase' && !m.done && (m.feItems || []).length) +const overlapOn = useFrontendOverlap && !!feModule +const feDecisions = [] // phase1/phase2 共享决策收集器(RESUME flush 在 phase2 milestone 覆盖两段) +const fePending = [] // 后端未全 done 时,frontend-phase 的 pending 记账(并入下方 pending) +let fe1Promise = null +if (overlapOn) { + remaining = remaining.filter(m => m.id !== 'frontend-phase') // 从波次图剔除,后端波次独立推进 + const feCtx = makeCtx({ lane: 'fe', root: laneRoot('frontend-phase'), dbSchema: '', vBase: 0, decisions: feDecisions }) + fe1Promise = runFrontendPhase1(feModule, feCtx) // 不 await:与下方后端波次循环并发 + log(`fe-overlap: 前端 phase1 已与后端波次并发启动(worktree ${laneRoot('frontend-phase')})`) +} + while (remaining.length) { let wave = nextWave(remaining, doneSet) if (!useParallel) wave = wave.slice(0, 1) // 防御:串行闸下恒单宽(链式图本就每轮恰出 1 个) @@ -3346,13 +3470,49 @@ while (remaining.length) { if (results.some(r => r.status === 'halted')) break } +// ── 前端 jsdom-only 重叠:屏障 + phase2 ──(overlap 关时整段跳过,现行终波路径不变) +if (overlapOn) { + const fe1 = await fe1Promise // 屏障:等前端 phase1(与后端波次并发)收束 + // 后端是否全部 done:无 halted(含 sweep/runtime-null)且 remaining 已空(无 pending 后端)。 + const backendAllDone = !results.some(r => r.status === 'halted') && remaining.length === 0 + if (fe1.status === 'halted') { + results.push(fe1) // phase1 halt:phase2 不跑;已完成 FE 的 fe-code-done 已保留,resume 续跑 + } else if (!backendAllDone) { + // 后端未全 done(halt / 还有 pending):phase1 产物(fe-code-done)已落 tag,phase2 留待 resume。 + fePending.push({ module: 'frontend-phase', status: 'pending', + blockedBy: ['<全部后端 done>'], decisions: fe1.decisions }) + log('fe-overlap: 后端未全部完成 → 前端 phase2 延后;phase1 产物已保留,resume 续跑') + } else { + // 后端全部 done + phase1 done → phase2:reconcile(合 default→frontend-phase + 移除 phase1 worktree)→ + // 标准 runModule(feStage='e2e')(主根全栈,skeleton 幂等跳过,behaviorGate/testGate/milestone 原样)。 + phase('Frontend') + const def = await agentR(detectDefaultBranchPromptM(), { label:'fe-overlap:default', phase:'Milestone', schema: DEFAULT_BRANCH_SCHEMA }) + await bestEffortAction(removeLaneWorktreePromptM(laneRoot('frontend-phase')), + { label:'fe-overlap:rm-worktree', phase:'Milestone', okMsg:'fe-overlap: 已移除 phase1 worktree', failTag:'fe-overlap phase1 worktree 移除' }) + const rec = await agentR(mergeDefaultIntoFrontendPromptM('frontend-phase', def.branch), + { label:'fe-overlap:reconcile', phase:'Milestone', schema: ACTION_RESULT_SCHEMA }) + if (!rec.success) { + // reconcile merge 冲突/失败:与 milestone merge 同口径,结构化 halt 留人工(phase1 产物已在 frontend-phase 分支)。 + const reason = `fe-overlap reconcile(merge ${def.branch}→frontend-phase)失败:${rec.error || ''}${rec.detail ? '\n' + rec.detail : ''}` + log(`⛔ HALT — frontend-phase:${reason}`) + results.push({ module: 'frontend-phase', status: 'halted', reason, decisions: fe1.decisions }) + } else { + feModule.feStage = 'e2e' // 让 runModule 前端段走 e2e(jsdom 已在 phase1 绿) + const fe2 = await runModule(feModule, makeCtx({ decisions: feDecisions })) + results.push(fe2) + if (fe2.status === 'done') doneSet.add(fe2.module) + } + } +} + // pending:图余量(Task 10)——halt 收敛后 remaining 中每项标注 blockedBy(尚未满足的依赖), // caller / coding-start 据此告知"修好后还有哪些待跑、各自被谁挡着",而非线性 slice 误导 // (波次图下 halt 模块之后的 docs/02 顺位可能与 halt 毫无依赖关系)。 +// fePending:前端 jsdom-only 重叠下「phase1 done 但后端未全 done」的 frontend-phase pending(并入图余量)。 const pending = remaining.map(m => ({ module: m.id, status: 'pending', blockedBy: (m.deps || []).filter(d => !doneSet.has(d)), -})) +})).concat(fePending) // Workflow 结果:跑完 / halt 的逐模块摘要 + halt 后未跑的 pending 模块列表 + 全流程自主决策日志 // (decisions:stage 缺值时未停而自主取的默认/解读,供 coding-start / 人工事后审阅,可能含错误假设)。 -- libgit2 0.22.2