Commit 094033f2537e729b10304bfe057e8133678af3e4

Authored by yanghl
1 parent 7a0069c5

coding.mjs: 前端 jsdom-only 重叠实现(开关 frontendOverlap,缺省关)

让前端不依赖后端的工作与后端波次并发,所有真后端测试延后。5 类改动:
- tddPrompt/verifyPrompt 加 feStage:jsdom 模式只处理 jsdom 任务、只跑 vitest、
  硬禁 e2e 命令;e2e 模式只补延后的 e2e 任务。新增 fe-code-done/<FE> 中间 tag。
- featureLoop 加 feStage 分流:jsdom 跳 review 打 fe-code-done;e2e/all 走
  review→req-done。新增 runFrontendPhase1(前端 worktree,不调 phase()、不
  merge/milestone、永不 throw)。
- 主循环:frontendOverlap 开时把 frontend-phase 从波次图剔除、起 phase1 与后端
  波次并发;屏障后 reconcile(merge default→frontend-phase)+ 标准
  runModule(feStage=e2e) 主根全栈跑 phase2。后端未全 done / phase1 halt → phase2
  延后、产物保留 resume,绝不新增 halt 点。
- coding-start SKILL.md + README:开关说明,缺省不传(同 features 闸纪律)。

重叠期前端无栈/无固定端口/无 DB,补 8/9/10 不变量与端口/lane 安全全保住。
缺省关时 feStage 恒 all、新分支不激活、新编排整段跳过,现行行为零变化。
语法门通过。设计真值见 docs/superpowers/plans/2026-06-15-frontend-overlap-jsdom.md。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
README.md
@@ -67,6 +67,13 @@ Claude Code 插件:ERP / 后端管理系统全流程开发框架。 @@ -67,6 +67,13 @@ Claude Code 插件:ERP / 后端管理系统全流程开发框架。
67 → testGate(frontend,全量回归 vitest+playwright,兜底行为 fix 引入的回归) 67 → testGate(frontend,全量回归 vitest+playwright,兜底行为 fix 引入的回归)
68 → runMilestone(milestone/frontend-phase) 68 → runMilestone(milestone/frontend-phase)
69 69
  70 + · 前端 jsdom-only 重叠(可选,parallel.frontendOverlap,缺省关):前端**不依赖后端**的部分
  71 + (骨架 + spec/plan + tdd 的 jsdom 任务 + vitest verify)在独立前端 worktree 与后端波次**并发**,
  72 + 打中间 tag fe-code-done/<FE>;后端全部 done 后 reconcile(合 default→frontend-phase)+ phase2
  73 + (e2e 任务 + review + req-done + 行为门 + 测试闸 + 里程碑,主根全栈)。重叠期前端无栈/无固定
  74 + 端口/无 DB,端口与 lane 不变量全保住;缺省关 / phase1 失败 / 后端未全 done 回退现行终波路径。
  75 + 设计:docs/superpowers/plans/2026-06-15-frontend-overlap-jsdom.md
  76 +
70 子代理无法弹窗 → 缺值即写阻塞点并 halt(终止态,非对话框);fail-fast 后等人工修复重跑 coding-start 77 子代理无法弹窗 → 缺值即写阻塞点并 halt(终止态,非对话框);fail-fast 后等人工修复重跑 coding-start
71 78
72 续跑 handoff:主循环结束(halt 或全完成)时 best-effort 追加 docs/superpowers/RESUME.md 79 续跑 handoff:主循环结束(halt 或全完成)时 best-effort 追加 docs/superpowers/RESUME.md
skills/coding/coding-start/SKILL.md
@@ -106,6 +106,8 @@ Workflow({ @@ -106,6 +106,8 @@ Workflow({
106 > `pluginRoot` 供 coding.mjs 调用插件 `lib/req-ledger.mjs` 在首跑时自动建立需求台账基线(P1#3,使日后 `/add-req` 能识别增量、免空跑);缺省则该增强静默跳过。 106 > `pluginRoot` 供 coding.mjs 调用插件 `lib/req-ledger.mjs` 在首跑时自动建立需求台账基线(P1#3,使日后 `/add-req` 能识别增量、免空跑);缺省则该增强静默跳过。
107 > 107 >
108 > `parallel` 是模块级并行总开关:`modules: true` 时 coding.mjs 先跑调度器(LLM 只输出模块间依赖边,JS 做确定性拓扑波次),同波次 ≥2 个后端模块各自在 lane worktree + lane 测试库 + 专属 migration 版本段内并行推进;任何调度/隔离环节失败一律 fail-open 降级串行,绝不因并行机制 halt。**逃生口**:改传 `parallel: { modules: false }`(或整个不传 `parallel`)即完全回退现行串行路径。`features`(feature 级并行)**刻意不传**——维持缺省关闭,待并行化计划 Task 14 收益评估通过后才在此处开启(附录补 1)。 108 > `parallel` 是模块级并行总开关:`modules: true` 时 coding.mjs 先跑调度器(LLM 只输出模块间依赖边,JS 做确定性拓扑波次),同波次 ≥2 个后端模块各自在 lane worktree + lane 测试库 + 专属 migration 版本段内并行推进;任何调度/隔离环节失败一律 fail-open 降级串行,绝不因并行机制 halt。**逃生口**:改传 `parallel: { modules: false }`(或整个不传 `parallel`)即完全回退现行串行路径。`features`(feature 级并行)**刻意不传**——维持缺省关闭,待并行化计划 Task 14 收益评估通过后才在此处开启(附录补 1)。
  109 +>
  110 +> `frontendOverlap`(前端 jsdom-only 重叠)**同样刻意不传**——维持缺省关闭。开启(`parallel: { modules: true, maxWidth: 3, frontendOverlap: true }`)时,前端**不依赖后端**的部分(骨架 + spec/plan + tdd 的 jsdom 任务 + vitest verify)在独立前端 worktree 与后端波次**并发**跑,打中间 tag `fe-code-done/<FE>`;后端全部 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 点。
109 111
110 ### 步骤 5:告知用户 Workflow 已启动 112 ### 步骤 5:告知用户 Workflow 已启动
111 113
workflows/coding.mjs
@@ -512,7 +512,7 @@ function planPrompt(id, phase, specPath, c) { @@ -512,7 +512,7 @@ function planPrompt(id, phase, specPath, c) {
512 512
513 // ---- stage 3:按 plan 逐任务 TDD(原 feature-tdd / fe-feature-tdd)---- 513 // ---- stage 3:按 plan 逐任务 TDD(原 feature-tdd / fe-feature-tdd)----
514 // planPath:上游 plan artifactPath;ledger 是 prompt 层的显式自约束(无 harness 强制)。 514 // planPath:上游 plan artifactPath;ledger 是 prompt 层的显式自约束(无 harness 强制)。
515 -function tddPrompt(id, phase, planPath, c) { 515 +function tddPrompt(id, phase, planPath, c, feStage = 'all') {
516 const fe = isFrontend(phase) 516 const fe = isFrontend(phase)
517 return [ 517 return [
518 `# ${fe ? 'fe-feature-tdd' : 'feature-tdd'} — 逐任务 TDD ${id}`, 518 `# ${fe ? 'fe-feature-tdd' : 'feature-tdd'} — 逐任务 TDD ${id}`,
@@ -543,6 +543,14 @@ function tddPrompt(id, phase, planPath, c) { @@ -543,6 +543,14 @@ function tddPrompt(id, phase, planPath, c) {
543 // schema 时 ERP_TEST_DB_SCHEMA 传进去无人消费(脚本支持 ≠ 测试进程支持)。占位 env 缺省回退字面量, 543 // schema 时 ERP_TEST_DB_SCHEMA 传进去无人消费(脚本支持 ≠ 测试进程支持)。占位 env 缺省回退字面量,
544 // 串行行为一字不变;gradle test fork 与 bootRun 默认继承 env,无需 SPRING_DATASOURCE_URL 拼装。 544 // 串行行为一字不变;gradle test fork 与 bootRun 默认继承 env,无需 SPRING_DATASOURCE_URL 拼装。
545 fe ? '' : '- **数据源 schema 占位约定**:生成 / 修改后端 `application*.yml`(含测试 profile)时,JDBC URL 的 schema 段必须写成 Spring 占位 `${ERP_TEST_DB_SCHEMA:<db.schema 字面量>}`(env 缺省回退原 schema,行为不变);**绝不**把 schema 名写死成纯字面量。', 545 fe ? '' : '- **数据源 schema 占位约定**:生成 / 修改后端 `application*.yml`(含测试 profile)时,JDBC URL 的 schema 段必须写成 Spring 占位 `${ERP_TEST_DB_SCHEMA:<db.schema 字面量>}`(env 缺省回退原 schema,行为不变);**绝不**把 schema 名写死成纯字面量。',
  546 + // feStage 任务子集(前端 jsdom-only 重叠;设计 docs/superpowers/plans/2026-06-15-frontend-overlap-jsdom.md)。
  547 + // 'all'(缺省,含全部后端)与现行一字不差;'jsdom'/'e2e' 仅前端 phase1/phase2 传入。
  548 + fe && feStage === 'jsdom'
  549 + ? '- **本阶段 = 仅 jsdom 任务(重叠 phase1,不依赖后端)**:只处理 plan 中 `测试先行类型 = jsdom` 的代码任务;`= e2e` 的任务**全部跳过**,并在 `summary` 里以 `deferredE2e: [<task...>]` 列出(留待 phase2)。测试命令**只用** vitest(`frontend.test_command` / `frontend.unit_test_runner`)。**硬护栏**:本阶段**绝不**运行 Playwright / `pnpm e2e:ci` / `frontend.e2e_command` / 任何冷起全栈或固定端口的 e2e 命令——命中即 `{status:"halt", reason:"jsdom 模式禁跑 e2e(重叠 phase1 后端尚未就绪)"}`。FeStub→真组件替换(见下)**在本阶段完成**。'
  550 + : '',
  551 + fe && feStage === 'e2e'
  552 + ? '- **本阶段 = 仅 e2e 任务(phase2,后端已就绪;jsdom 已在 phase1 绿)**:只处理 plan 中 `测试先行类型 = e2e` 的任务;jsdom 任务**跳过**(phase1 已完成实现与单测)。e2e 基线约束见下;FeStub→真组件替换 phase1 已做,本阶段确认 router 仍指向真组件即可。'
  553 + : '',
546 '- 按顺序处理每个代码类任务:(a) 在 `test_file::test_name` 写**失败**测试;(b) **派发 Agent 子会话**跑测试确认失败,子会话只返回 `{command, exit_code, failing_assertion}` JSON;(c) 写**最小**实现使测试通过;(d) 再派子会话确认通过;(e) commit(含 `REQ_ID` / REQ 标签)。', 554 '- 按顺序处理每个代码类任务:(a) 在 `test_file::test_name` 写**失败**测试;(b) **派发 Agent 子会话**跑测试确认失败,子会话只返回 `{command, exit_code, failing_assertion}` JSON;(c) 写**最小**实现使测试通过;(d) 再派子会话确认通过;(e) commit(含 `REQ_ID` / REQ 标签)。',
547 fe 555 fe
548 ? '- **测试落位(硬约定,对齐 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 复刻。' 556 ? '- **测试落位(硬约定,对齐 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) { @@ -575,7 +583,7 @@ function tddPrompt(id, phase, planPath, c) {
575 // ---- stage 4:把功能测试派子会话跑,渲染证据(原 feature-verify / fe-feature-verify)---- 583 // ---- stage 4:把功能测试派子会话跑,渲染证据(原 feature-verify / fe-feature-verify)----
576 // specPath:用于复用日期前缀;round:0 = TDD 后初次 verify,1..5 = fix 后 reverify(每轮独立证据文件, 584 // specPath:用于复用日期前缀;round:0 = TDD 后初次 verify,1..5 = fix 后 reverify(每轮独立证据文件,
577 // 避免 reverify 覆盖前轮证据)。 585 // 避免 reverify 覆盖前轮证据)。
578 -function verifyPrompt(id, phase, implSummary, specPath, round = 0, c) { 586 +function verifyPrompt(id, phase, implSummary, specPath, round = 0, c, feStage = 'all') {
579 const fe = isFrontend(phase) 587 const fe = isFrontend(phase)
580 const suffix = round === 0 ? 'verify' : `verify-r${round}` 588 const suffix = round === 0 ? 'verify' : `verify-r${round}`
581 return [ 589 return [
@@ -589,7 +597,14 @@ function verifyPrompt(id, phase, implSummary, specPath, round = 0, c) { @@ -589,7 +597,14 @@ function verifyPrompt(id, phase, implSummary, specPath, round = 0, c) {
589 implSummary ? `- 上游 TDD 摘要:${implSummary}` : '', 597 implSummary ? `- 上游 TDD 摘要:${implSummary}` : '',
590 '', 598 '',
591 '## 流程', 599 '## 流程',
592 - fe 600 + fe && feStage === 'jsdom'
  601 + ? [
  602 + // 重叠 phase1:只验 jsdom 单测(不依赖后端),e2e 留 phase2。
  603 + `- 测试目标:从 plan 取 \`测试先行类型 = jsdom\` 的 test_file → 拼 vitest/jest 过滤模式。命令从 \`${c.root}/docs/04-技术规范.md § 零 frontend.test_command\` / \`frontend.unit_test_runner\` 取(缺失默认 \`pnpm test:ci\`)。`,
  604 + '- 派子会话**只跑 unit(vitest)**,返回 `{ unit:{command,exit_code,passed,failed,failed_list,stdout_excerpt} }`(`stdout_excerpt` ≤ 30 行)。**硬护栏**:本阶段绝不跑 e2e / Playwright / `pnpm e2e:ci`(后端尚未就绪)。',
  605 + '- **unit `exit_code != 0` 或 `failed > 0`** → 渲染证据后 halt。',
  606 + ].join('\n')
  607 + : fe
593 ? [ 608 ? [
594 `- 测试目标:从 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\`)。`, 609 `- 测试目标:从 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\`)。`,
595 '- 派子会话依次跑 unit + e2e,子会话只返回结构化 JSON:`{ unit:{command,exit_code,passed,failed,failed_list,stdout_excerpt}, e2e:{...同结构} }`(`stdout_excerpt` ≤ 30 行)。', 610 '- 派子会话依次跑 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) { @@ -1580,6 +1595,34 @@ function createReqDoneTagPromptM(id, phase, c) {
1580 ].join('\n') 1595 ].join('\n')
1581 } 1596 }
1582 1597
  1598 +// ── 微步骤:前端 jsdom-only 重叠的中间完成态 tag `fe-code-done/<FE>`(设计见
  1599 +// docs/superpowers/plans/2026-06-15-frontend-overlap-jsdom.md)──
  1600 +// 语义:phase1 完成(组件代码 + jsdom 绿,e2e/review/req-done 全部延后)。与 req-done 正交、双 tag 续跑链:
  1601 +// fe-code-done 存在 → phase1 跳过该 FE;req-done 存在 → phase2 跳过;两者皆缺 → phase1 重跑该 FE。
  1602 +// 仅在 frontendOverlap 开启时产生;关闭时全程只用 req-done,与现行一字不差。
  1603 +function checkFeCodeDoneTagPromptM(id, c) {
  1604 + return [
  1605 + `# tag \`fe-code-done/${id}\` 是否存在(前端 phase1 完成态 dedup 真值)`,
  1606 + microStepContract(c.root),
  1607 + '',
  1608 + `跑 \`git -C ${c.root} tag -l fe-code-done/${id}\`。`,
  1609 + '## 输出(EXISTS_SCHEMA)',
  1610 + '- stdout 含完整匹配 → `{ "exists": true }`;为空 → `{ "exists": false }`',
  1611 + ].join('\n')
  1612 +}
  1613 +
  1614 +function createFeCodeDoneTagPromptM(id, c) {
  1615 + return [
  1616 + `# 打 annotated tag \`fe-code-done/${id}\`(前端 phase1:组件代码 + jsdom 绿,e2e/review 延后)`,
  1617 + microStepContract(c.root),
  1618 + '',
  1619 + `跑 \`git -C ${c.root} tag -a fe-code-done/${id} -m "fe-code(${id}): jsdom green, e2e/review deferred (overlap phase1)"\`。`,
  1620 + `先用 \`git -C ${c.root} tag -l fe-code-done/${id}\` 检查;已存在则视为成功(幂等)直接返回 success。`,
  1621 + '## 输出(ACTION_RESULT_SCHEMA)',
  1622 + '- 成功 / 已存在:`{ "success": true }`;其它失败:`{ "success": false, "error": "<stderr>" }`',
  1623 + ].join('\n')
  1624 +}
  1625 +
1583 // ── 微步骤:批量预生成 spec 的统一提交(Phase E Task 11)── 1626 // ── 微步骤:批量预生成 spec 的统一提交(Phase E Task 11)──
1584 // batch 模式 spec 只写盘不 commit(避免同一工作树并行 commit 争 .git/index.lock);全部预生成 1627 // batch 模式 spec 只写盘不 commit(避免同一工作树并行 commit 争 .git/index.lock);全部预生成
1585 // 落盘后由本微步骤一次性 add+commit 收口。幂等:resume 场景 spec 可能是"复用已提交文件、就地 1628 // 落盘后由本微步骤一次性 add+commit 收口。幂等:resume 场景 spec 可能是"复用已提交文件、就地
@@ -1693,6 +1736,24 @@ function executeMergePromptM(defaultBranch, branch, phaseId) { @@ -1693,6 +1736,24 @@ function executeMergePromptM(defaultBranch, branch, phaseId) {
1693 ].join('\n') 1736 ].join('\n')
1694 } 1737 }
1695 1738
  1739 +// reconcile(前端 jsdom-only 重叠 phase1→phase2 边界,设计 docs/superpowers/plans/2026-06-15-frontend-overlap-jsdom.md):
  1740 +// phase1 的 frontend-phase 分支自**重叠开始时**的 default 分叉,不含后端代码;phase2 的 e2e 要打真后端,
  1741 +// 故先把已完成的 default(含全部后端 milestone)合并进 frontend-phase。路径互斥(frontend/ vs backend/)
  1742 +// → 冲突概率极低;冲突按 milestone merge 同口径「留树 halt 给人工」,绝不 abort。幂等:default 已是
  1743 +// frontend-phase 祖先(无重叠 / resume 已 merge)→ git "Already up to date" → success。主根专属。
  1744 +function mergeDefaultIntoFrontendPromptM(branch, defaultBranch) {
  1745 + return [
  1746 + `# reconcile:把 \`${defaultBranch}\`(已完成后端)合并进 \`${branch}\``,
  1747 + microStepContract(ROOT),
  1748 + '',
  1749 + `1. 跑 \`git -C ${ROOT} checkout ${branch}\` 确保 HEAD 在 ${branch}(phase1 worktree 已移除,分支空闲;若仍被占用则整步失败留人工)。`,
  1750 + `2. 跑 \`git -C ${ROOT} merge --no-ff ${defaultBranch} -m "merge(reconcile): integrate backend into ${branch} for e2e"\`。`,
  1751 + '## 输出(ACTION_RESULT_SCHEMA)',
  1752 + '- 成功 / 已是最新(Already up to date)→ `{ "success": true }`',
  1753 + '- 合并冲突 / 其它失败 → `{ "success": false, "error": "<simplified>", "detail": "<冲突文件换行分隔 或 stderr 前 30 行>" }`;**不要**自动 abort / stash / 改文件。',
  1754 + ].join('\n')
  1755 +}
  1756 +
1696 function readDocs08FieldPromptM(fe, id) { 1757 function readDocs08FieldPromptM(fe, id) {
1697 const section = fe ? '§ 三' : '§ 二' 1758 const section = fe ? '§ 三' : '§ 二'
1698 const title = fe 1759 const title = fe
@@ -2190,8 +2251,13 @@ const useSpecPrefetch = ARGS?.parallel?.features === true || ARGS?.parallel?.mod @@ -2190,8 +2251,13 @@ const useSpecPrefetch = ARGS?.parallel?.features === true || ARGS?.parallel?.mod
2190 // 冲突风险、两开关任一开启即用;feature 级 tdd/fix 链并行只看本闸。 2251 // 冲突风险、两开关任一开启即用;feature 级 tdd/fix 链并行只看本闸。
2191 const useFeatureParallel = ARGS?.parallel?.features === true 2252 const useFeatureParallel = ARGS?.parallel?.features === true
2192 const featureMaxWidth = Math.max(1, ARGS?.parallel?.featureMaxWidth ?? 2) 2253 const featureMaxWidth = Math.max(1, ARGS?.parallel?.featureMaxWidth ?? 2)
2193 -async function featureLoop(items, phase, c) { 2254 +// feStage(前端 jsdom-only 重叠;设计 docs/superpowers/plans/2026-06-15-frontend-overlap-jsdom.md):
  2255 +// 'all'(缺省)= 现行完整链(spec→plan→tdd→verify→review→req-done),后端恒走此;前端 overlap 关时亦走此。
  2256 +// 'jsdom'(overlap phase1)= spec→plan→tdd(仅 jsdom 任务)→verify(仅 vitest)→tag fe-code-done;**不** review、**不** req-done。
  2257 +// 'e2e'(phase2,后端已就绪)= dedup 读 fe-code-done → spec/plan(复用)→tdd(仅 e2e 任务)→verify(unit+e2e)→review→req-done。
  2258 +async function featureLoop(items, phase, c, feStage = 'all') {
2194 const grp = phase === 'backend' ? 'Backend' : 'Frontend' 2259 const grp = phase === 'backend' ? 'Backend' : 'Frontend'
  2260 + const isPhase1 = feStage === 'jsdom' // 只推进到 fe-code-done(中间态)
2195 // spec 的 artifactPath 校验(主链与批量预生成共用同一份,确保两条路径产物口径一致)。 2261 // spec 的 artifactPath 校验(主链与批量预生成共用同一份,确保两条路径产物口径一致)。
2196 const specValidate = r => { 2262 const specValidate = r => {
2197 if (!r.artifactPath) return 'spec 返回 ok 但缺 artifactPath(流程靠它定位 spec 并派生下游日期前缀)' 2263 if (!r.artifactPath) return 'spec 返回 ok 但缺 artifactPath(流程靠它定位 spec 并派生下游日期前缀)'
@@ -2285,17 +2351,20 @@ async function featureLoop(items, phase, c) { @@ -2285,17 +2351,20 @@ async function featureLoop(items, phase, c) {
2285 } 2351 }
2286 // fc:执行上下文(串行 = 模块 ctx c;Phase F 并行批 = feature lane ctx)。rwOpts 透传 2352 // fc:执行上下文(串行 = 模块 ctx c;Phase F 并行批 = feature lane ctx)。rwOpts 透传
2287 // reviewWithFixLoop(feature lane 内跳过 docs/08 勾选,理由见该函数头注释)。 2353 // reviewWithFixLoop(feature lane 内跳过 docs/08 勾选,理由见该函数头注释)。
2288 - const runImplChain = async (id, planPath, specPath, fc, rwOpts) => { 2354 + // stage(末位,缺省 'all'=现行):透传 tddPrompt/verifyPrompt 的 feStage;'jsdom' 额外跳过 review(phase1
  2355 + // 只到 fe-code-done,review→req-done 延到 phase2)。'e2e'/'all' 含 review,一字不差。
  2356 + const runImplChain = async (id, planPath, specPath, fc, rwOpts, stage = 'all') => {
2289 // tdd allowContinue:false:tddPrompt 的 halt = 路径作用域越界护栏 / 同测试卡死 10 次——硬边界, 2357 // tdd allowContinue:false:tddPrompt 的 halt = 路径作用域越界护栏 / 同测试卡死 10 次——硬边界,
2290 // 仲裁不得 continue 放行(越界把前端实现混进后端分支 / 卡死等于测试没真过)。 2358 // 仲裁不得 continue 放行(越界把前端实现混进后端分支 / 卡死等于测试没真过)。
2291 - const impl = await runStage(g => tddPrompt(id, phase, planPath, fc) + g, { 2359 + const impl = await runStage(g => tddPrompt(id, phase, planPath, fc, stage) + g, {
2292 site:`tdd:${phase}:${id}`, grp, label:`tdd:${phase}:${id}`, allowContinue: false, dec: fc.decisions, root: fc.root, 2360 site:`tdd:${phase}:${id}`, grp, label:`tdd:${phase}:${id}`, allowContinue: false, dec: fc.decisions, root: fc.root,
2293 }) 2361 })
2294 // verify allowContinue:false:verifyPrompt 的 halt = 功能测试红色(exit!=0 / failed>0)——与 test-gate 红同级硬边界, 2362 // verify allowContinue:false:verifyPrompt 的 halt = 功能测试红色(exit!=0 / failed>0)——与 test-gate 红同级硬边界,
2295 // 绝不 continue 放行红色实现进 review→approve→打 req-done tag(否则红色功能被永久标记完成、resume 跳过)。 2363 // 绝不 continue 放行红色实现进 review→approve→打 req-done tag(否则红色功能被永久标记完成、resume 跳过)。
2296 - const v0 = await runStage(g => verifyPrompt(id, phase, impl.summary || '', specPath, 0, fc) + g, { 2364 + const v0 = await runStage(g => verifyPrompt(id, phase, impl.summary || '', specPath, 0, fc, stage) + g, {
2297 site:`verify:${phase}:${id}`, grp, label:`verify:${phase}:${id}`, allowContinue: false, dec: fc.decisions, root: fc.root, 2365 site:`verify:${phase}:${id}`, grp, label:`verify:${phase}:${id}`, allowContinue: false, dec: fc.decisions, root: fc.root,
2298 }) 2366 })
  2367 + if (stage === 'jsdom') return // overlap phase1:jsdom 绿即止,不 review(review→req-done 留 phase2)
2299 const reviewResult = await reviewWithFixLoop(id, phase, v0, specPath, fc, rwOpts) 2368 const reviewResult = await reviewWithFixLoop(id, phase, v0, specPath, fc, rwOpts)
2300 log(`review approved ${phase}:${id} after ${reviewResult.rounds} round(s)`) 2369 log(`review approved ${phase}:${id} after ${reviewResult.rounds} round(s)`)
2301 } 2370 }
@@ -2304,6 +2373,15 @@ async function featureLoop(items, phase, c) { @@ -2304,6 +2373,15 @@ async function featureLoop(items, phase, c) {
2304 const tagDone = async (id) => runAction(g => createReqDoneTagPromptM(id, phase, c) + g, { 2373 const tagDone = async (id) => runAction(g => createReqDoneTagPromptM(id, phase, c) + g, {
2305 site:`req-done-tag:${phase}:${id}`, grp, label:`reqdone:${phase}:${id}`, dec: c.decisions, root: c.root, 2374 site:`req-done-tag:${phase}:${id}`, grp, label:`reqdone:${phase}:${id}`, dec: c.decisions, root: c.root,
2306 }) 2375 })
  2376 + // overlap phase1 中间完成态 tag:fe-code-done/<id>(组件代码 + jsdom 绿)。
  2377 + const tagFeCodeDone = async (id) => runAction(g => createFeCodeDoneTagPromptM(id, c) + g, {
  2378 + site:`fe-code-done-tag:${id}`, grp, label:`fecodedone:${id}`, dec: c.decisions, root: c.root,
  2379 + })
  2380 + // phase1 入口 dedup:fe-code-done/<id> 已存在 → 本 FE phase1 已完成,跳过(phase2 用 req-done dedup,走 isDone)。
  2381 + const isPhase1Done = async (id) => {
  2382 + const r = await agentR(checkFeCodeDoneTagPromptM(id, c), {label:`fecodecheck:${phase}:${id}`, phase: grp, schema: EXISTS_SCHEMA})
  2383 + return r.exists
  2384 + }
2307 2385
2308 // ── Phase F(Task 12/13):后端 REQ 的 feature 级受限并行(设计见 featureBatchRun 头注释)。 2386 // ── Phase F(Task 12/13):后端 REQ 的 feature 级受限并行(设计见 featureBatchRun 头注释)。
2309 // frontend phase 不进准入(附录补 2);闸关 / 单 feature 时走下方现行串行链,一字不差。 2387 // frontend phase 不进准入(附录补 2);闸关 / 单 feature 时走下方现行串行链,一字不差。
@@ -2313,11 +2391,15 @@ async function featureLoop(items, phase, c) { @@ -2313,11 +2391,15 @@ async function featureLoop(items, phase, c) {
2313 } 2391 }
2314 2392
2315 for (const id of items) { 2393 for (const id of items) {
2316 - if (await isDone(id)) { log(`featureLoop skip ${phase}:${id} — tag req-done/${id} 已存在`); continue } 2394 + // dedup:phase1 用 fe-code-done;'all'/'e2e'(phase2)用 req-done。phase2 的 fe-code-done 必已存在
  2395 + // (phase1 落过),spec/plan 经 ensureSpec/runPlanStage 复用既有产物,tdd(e2e) 只处理延后的 e2e 任务。
  2396 + if (isPhase1 ? await isPhase1Done(id) : await isDone(id)) {
  2397 + log(`featureLoop skip ${phase}:${id} — tag ${isPhase1 ? 'fe-code-done' : 'req-done'}/${id} 已存在`); continue
  2398 + }
2317 const spec = await ensureSpec(id) 2399 const spec = await ensureSpec(id)
2318 const plan = await runPlanStage(id, spec) 2400 const plan = await runPlanStage(id, spec)
2319 - await runImplChain(id, plan.artifactPath, spec.artifactPath, c)  
2320 - await tagDone(id) 2401 + await runImplChain(id, plan.artifactPath, spec.artifactPath, c, undefined, feStage)
  2402 + await (isPhase1 ? tagFeCodeDone(id) : tagDone(id))
2321 } 2403 }
2322 } 2404 }
2323 2405
@@ -3054,6 +3136,28 @@ const withStackLock = makeChainLock() @@ -3054,6 +3136,28 @@ const withStackLock = makeChainLock()
3054 // phase(...) 全局 UI 分组只在主根模式调用(c.lane == null):lane 并行下兄弟模块会竞态改全局 phase 状态, 3136 // phase(...) 全局 UI 分组只在主根模式调用(c.lane == null):lane 并行下兄弟模块会竞态改全局 phase 状态,
3055 // 而 agent 级 opts.phase(grp)已全覆盖分组——注意不能写 `!c.lane`(lane 序号 0 是合法 lane,falsy 会误判主根)。 3137 // 而 agent 级 opts.phase(grp)已全覆盖分组——注意不能写 `!c.lane`(lane 序号 0 是合法 lane,falsy 会误判主根)。
3056 const resumeFlushed = new Set() // per-module RESUME flush 成功记账(替代 flushedCount 水位线;loop-end 据此补记失败模块) 3138 const resumeFlushed = new Set() // per-module RESUME flush 成功记账(替代 flushedCount 水位线;loop-end 据此补记失败模块)
  3139 +
  3140 +// ---- runFrontendPhase1:前端 jsdom-only 重叠的 phase1(设计 docs/superpowers/plans/2026-06-15-frontend-overlap-jsdom.md)----
  3141 +// 与后端波次循环**并发**跑,**只做不依赖后端**的事:branchSetup(建前端 worktree)→ skeleton → featureLoop(jsdom)。
  3142 +// 跑在**独立前端 worktree**(c.lane 非 null、root=前端 lane 路径、dbSchema=''——jsdom 无 DB,featureStageContract
  3143 +// 的 lane-DB 注入按 c.dbSchema 非空判定,此处不触发),与后端主根/lane 的 git index 互不干扰;**不**调全局
  3144 +// phase()(并发下会与后端波次竞态改全局 UI 状态——agent 级 grp='Frontend' 已覆盖分组),**不** merge/milestone/
  3145 +// RESUME(全部留 phase2)。**永不 throw**:catch 结构化返回,phase1 halt 不停后端(reason 带前端 worktree 取证前缀)。
  3146 +// 幂等(resume):skeleton 凭 fe-skeleton-done、各 FE 凭 fe-code-done 去重,已完成项整段 skip。
  3147 +async function runFrontendPhase1(feModule, c) {
  3148 + try {
  3149 + await runBranchSetup(feModule, c)
  3150 + await runFrontendSkeleton(feModule.feItems, c)
  3151 + await featureLoop(feModule.feItems, 'frontend', c, 'jsdom')
  3152 + log(`fe-overlap phase1 完成:${feModule.feItems.length} 个 FE 的组件代码 + jsdom 已绿(e2e/review 留 phase2)`)
  3153 + return { module: 'frontend-phase', status: 'done', decisions: c.decisions }
  3154 + } catch (e) {
  3155 + const reason = `[fe-overlap phase1 保留取证: ${c.root}] ${String(e?.message || e)}`
  3156 + log(`⛔ HALT — fe-overlap phase1:${reason}`)
  3157 + return { module: 'frontend-phase', status: 'halted', reason, decisions: c.decisions }
  3158 + }
  3159 +}
  3160 +
3057 async function runModule(module, c) { 3161 async function runModule(module, c) {
3058 try { 3162 try {
3059 if (c.lane == null) phase('Milestone') 3163 if (c.lane == null) phase('Milestone')
@@ -3082,7 +3186,9 @@ async function runModule(module, c) { @@ -3082,7 +3186,9 @@ async function runModule(module, c) {
3082 // 使逐 FE verify(e2e) 与阶段末尾行为门的可构建前提成立、tddPrompt 的 FeStub→真组件占位替换有真值起点。幂等(fe-skeleton-done tag)。 3186 // 使逐 FE verify(e2e) 与阶段末尾行为门的可构建前提成立、tddPrompt 的 FeStub→真组件占位替换有真值起点。幂等(fe-skeleton-done tag)。
3083 await runFrontendSkeleton(module.feItems, c) 3187 await runFrontendSkeleton(module.feItems, c)
3084 // featureLoop 的 review 循环只做静态验收(reviewer approve 即打 req-done)——行为验收不在内循环。 3188 // featureLoop 的 review 循环只做静态验收(reviewer approve 即打 req-done)——行为验收不在内循环。
3085 - await featureLoop(module.feItems, 'frontend', c) 3189 + // module.feStage(前端 jsdom-only 重叠):缺省 'all'=现行完整链;overlap phase2 传 'e2e'(phase1 已在
  3190 + // 前端 worktree 跑完 jsdom,本处只补 e2e 任务 + review + req-done)。skeleton 上方已幂等(fe-skeleton-done)。
  3191 + await featureLoop(module.feItems, 'frontend', c, module.feStage || 'all')
3086 // 阶段级行为门(v3):整个前端阶段只跑一次行为验收——起全栈 + 演示/sentinel 种子,按全部 FE spec 聚合 3192 // 阶段级行为门(v3):整个前端阶段只跑一次行为验收——起全栈 + 演示/sentinel 种子,按全部 FE spec 聚合
3087 // 作用域并集验「按钮真生效/文字对」;硬问题转 must-fix→fix→单测复验→重跑门(≤BEHAVIOR_STAGE_MAX 轮)。 3193 // 作用域并集验「按钮真生效/文字对」;硬问题转 must-fix→fix→单测复验→重跑门(≤BEHAVIOR_STAGE_MAX 轮)。
3088 // 放在 testGate 之前:行为 fix 改动 frontend/ 源码,绿后由 testGate 全量回归兜底,不让回归证据过期。 3194 // 放在 testGate 之前:行为 fix 改动 frontend/ 源码,绿后由 testGate 全量回归兜底,不让回归证据过期。
@@ -3288,6 +3394,24 @@ const results = [] @@ -3288,6 +3394,24 @@ const results = []
3288 const doneSet = new Set(routed.modules.filter(m => m.done).map(m => m.id)) 3394 const doneSet = new Set(routed.modules.filter(m => m.done).map(m => m.id))
3289 let remaining = useParallel ? await runScheduler(todo, routed.modules) : serialChainDeps(todo) 3395 let remaining = useParallel ? await runScheduler(todo, routed.modules) : serialChainDeps(todo)
3290 3396
  3397 +// ── 前端 jsdom-only 重叠(设计 docs/superpowers/plans/2026-06-15-frontend-overlap-jsdom.md)──
  3398 +// frontendOverlap 开 + 存在待跑 frontend-phase(有 feItems)时:把 frontend-phase 从波次图剔除,起 phase1
  3399 +// (前端 worktree,只跑 skeleton + jsdom,不依赖后端)与后端波次循环**并发**;屏障(后端全部 done)后再
  3400 +// reconcile(合 default→frontend-phase)+ 标准 runModule(feStage='e2e')(主根全栈,保住补 8/9/10)。缺省关 /
  3401 +// phase1 失败 / 后端未全 done → 完全回退现行终波路径,phase1 产物(fe-code-done)落 tag、resume 续跑,绝不新增 halt 点。
  3402 +const useFrontendOverlap = ARGS?.parallel?.frontendOverlap === true
  3403 +const feModule = routed.modules.find(m => m.id === 'frontend-phase' && !m.done && (m.feItems || []).length)
  3404 +const overlapOn = useFrontendOverlap && !!feModule
  3405 +const feDecisions = [] // phase1/phase2 共享决策收集器(RESUME flush 在 phase2 milestone 覆盖两段)
  3406 +const fePending = [] // 后端未全 done 时,frontend-phase 的 pending 记账(并入下方 pending)
  3407 +let fe1Promise = null
  3408 +if (overlapOn) {
  3409 + remaining = remaining.filter(m => m.id !== 'frontend-phase') // 从波次图剔除,后端波次独立推进
  3410 + const feCtx = makeCtx({ lane: 'fe', root: laneRoot('frontend-phase'), dbSchema: '', vBase: 0, decisions: feDecisions })
  3411 + fe1Promise = runFrontendPhase1(feModule, feCtx) // 不 await:与下方后端波次循环并发
  3412 + log(`fe-overlap: 前端 phase1 已与后端波次并发启动(worktree ${laneRoot('frontend-phase')})`)
  3413 +}
  3414 +
3291 while (remaining.length) { 3415 while (remaining.length) {
3292 let wave = nextWave(remaining, doneSet) 3416 let wave = nextWave(remaining, doneSet)
3293 if (!useParallel) wave = wave.slice(0, 1) // 防御:串行闸下恒单宽(链式图本就每轮恰出 1 个) 3417 if (!useParallel) wave = wave.slice(0, 1) // 防御:串行闸下恒单宽(链式图本就每轮恰出 1 个)
@@ -3346,13 +3470,49 @@ while (remaining.length) { @@ -3346,13 +3470,49 @@ while (remaining.length) {
3346 if (results.some(r => r.status === 'halted')) break 3470 if (results.some(r => r.status === 'halted')) break
3347 } 3471 }
3348 3472
  3473 +// ── 前端 jsdom-only 重叠:屏障 + phase2 ──(overlap 关时整段跳过,现行终波路径不变)
  3474 +if (overlapOn) {
  3475 + const fe1 = await fe1Promise // 屏障:等前端 phase1(与后端波次并发)收束
  3476 + // 后端是否全部 done:无 halted(含 sweep/runtime-null)且 remaining 已空(无 pending 后端)。
  3477 + const backendAllDone = !results.some(r => r.status === 'halted') && remaining.length === 0
  3478 + if (fe1.status === 'halted') {
  3479 + results.push(fe1) // phase1 halt:phase2 不跑;已完成 FE 的 fe-code-done 已保留,resume 续跑
  3480 + } else if (!backendAllDone) {
  3481 + // 后端未全 done(halt / 还有 pending):phase1 产物(fe-code-done)已落 tag,phase2 留待 resume。
  3482 + fePending.push({ module: 'frontend-phase', status: 'pending',
  3483 + blockedBy: ['<全部后端 done>'], decisions: fe1.decisions })
  3484 + log('fe-overlap: 后端未全部完成 → 前端 phase2 延后;phase1 产物已保留,resume 续跑')
  3485 + } else {
  3486 + // 后端全部 done + phase1 done → phase2:reconcile(合 default→frontend-phase + 移除 phase1 worktree)→
  3487 + // 标准 runModule(feStage='e2e')(主根全栈,skeleton 幂等跳过,behaviorGate/testGate/milestone 原样)。
  3488 + phase('Frontend')
  3489 + const def = await agentR(detectDefaultBranchPromptM(), { label:'fe-overlap:default', phase:'Milestone', schema: DEFAULT_BRANCH_SCHEMA })
  3490 + await bestEffortAction(removeLaneWorktreePromptM(laneRoot('frontend-phase')),
  3491 + { label:'fe-overlap:rm-worktree', phase:'Milestone', okMsg:'fe-overlap: 已移除 phase1 worktree', failTag:'fe-overlap phase1 worktree 移除' })
  3492 + const rec = await agentR(mergeDefaultIntoFrontendPromptM('frontend-phase', def.branch),
  3493 + { label:'fe-overlap:reconcile', phase:'Milestone', schema: ACTION_RESULT_SCHEMA })
  3494 + if (!rec.success) {
  3495 + // reconcile merge 冲突/失败:与 milestone merge 同口径,结构化 halt 留人工(phase1 产物已在 frontend-phase 分支)。
  3496 + const reason = `fe-overlap reconcile(merge ${def.branch}→frontend-phase)失败:${rec.error || ''}${rec.detail ? '\n' + rec.detail : ''}`
  3497 + log(`⛔ HALT — frontend-phase:${reason}`)
  3498 + results.push({ module: 'frontend-phase', status: 'halted', reason, decisions: fe1.decisions })
  3499 + } else {
  3500 + feModule.feStage = 'e2e' // 让 runModule 前端段走 e2e(jsdom 已在 phase1 绿)
  3501 + const fe2 = await runModule(feModule, makeCtx({ decisions: feDecisions }))
  3502 + results.push(fe2)
  3503 + if (fe2.status === 'done') doneSet.add(fe2.module)
  3504 + }
  3505 + }
  3506 +}
  3507 +
3349 // pending:图余量(Task 10)——halt 收敛后 remaining 中每项标注 blockedBy(尚未满足的依赖), 3508 // pending:图余量(Task 10)——halt 收敛后 remaining 中每项标注 blockedBy(尚未满足的依赖),
3350 // caller / coding-start 据此告知"修好后还有哪些待跑、各自被谁挡着",而非线性 slice 误导 3509 // caller / coding-start 据此告知"修好后还有哪些待跑、各自被谁挡着",而非线性 slice 误导
3351 // (波次图下 halt 模块之后的 docs/02 顺位可能与 halt 毫无依赖关系)。 3510 // (波次图下 halt 模块之后的 docs/02 顺位可能与 halt 毫无依赖关系)。
  3511 +// fePending:前端 jsdom-only 重叠下「phase1 done 但后端未全 done」的 frontend-phase pending(并入图余量)。
3352 const pending = remaining.map(m => ({ 3512 const pending = remaining.map(m => ({
3353 module: m.id, status: 'pending', 3513 module: m.id, status: 'pending',
3354 blockedBy: (m.deps || []).filter(d => !doneSet.has(d)), 3514 blockedBy: (m.deps || []).filter(d => !doneSet.has(d)),
3355 -})) 3515 +})).concat(fePending)
3356 3516
3357 // Workflow 结果:跑完 / halt 的逐模块摘要 + halt 后未跑的 pending 模块列表 + 全流程自主决策日志 3517 // Workflow 结果:跑完 / halt 的逐模块摘要 + halt 后未跑的 pending 模块列表 + 全流程自主决策日志
3358 // (decisions:stage 缺值时未停而自主取的默认/解读,供 coding-start / 人工事后审阅,可能含错误假设)。 3518 // (decisions:stage 缺值时未停而自主取的默认/解读,供 coding-start / 人工事后审阅,可能含错误假设)。