Commit 13000c553c25645361022f04741614567ff52368

Authored by yanghl
1 parent b65f384b

fix(coding): runBranchSetup 复用已存在分支时必须先合默认分支

缺陷:新建分支以默认分支为起点(createBranchFromPromptM),但复用已存在分支的
两条路径——主根 checkout(coding.mjs:2167 分支)与 lane worktree add(分支 2)——
都不带任何同步动作。上一轮跑完遗留的功能分支遂停在原位,其后进入默认分支的
REQ 卡片 / migration / docs 增量对本轮全部不可见。

实测后果(xlyClaude15DDD 2026-07-20):module-CFG 停在旧点,spec 子代理报
「全仓 grep <req> 零命中」后照自身理解另实现了一套设计,直到 milestone merge
才以三文件内容冲突暴露,整轮返工。子代理并未跑偏——它被喂了残缺的世界。

修法:HEAD 确认后统一跑 syncBranchWithDefaultPromptM(lane / 主根共用)。
新建分支走到这里恒为 up-to-date,故可无条件执行、幂等。冲突时保留现场返回失败,
经 runAction 仲裁 halt 交人工——陈旧分支与默认分支真冲突意味着两边都动了同一处,
只有人能判断取舍;自动 abort 会抹掉这个信号,让下一轮再撞同一个坑。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Showing 1 changed file with 36 additions and 1 deletions
workflows/coding.mjs
... ... @@ -1517,6 +1517,34 @@ function createBranchFromPromptM(fromBranch, newBranch, c) {
1517 1517 ].join('\n')
1518 1518 }
1519 1519  
  1520 +// syncBranchWithDefaultPromptM:把默认分支合进当前功能分支(幂等)。
  1521 +//
  1522 +// 为什么必须有这一步:runBranchSetup 对**新建**分支以默认分支为起点(createBranchFromPromptM),
  1523 +// 但对**已存在**分支只做 checkout(主根)/ worktree add(lane 分支 2),二者都不带任何同步动作。
  1524 +// 于是「上一轮跑完留下的功能分支」会停在它当时的位置,其后提交到默认分支的一切——新 REQ 卡片、
  1525 +// 新 migration、docs 增量——对本轮**全部不可见**。子代理据残缺事实作业不会报错,只会自行发明
  1526 +// 需求(实测:spec 阶段报「全仓 grep <req> 零命中」后照自己的理解另实现一套),到 milestone
  1527 +// merge 才以内容冲突暴露,此时整轮返工。故此步须在功能分支上作业**之前**执行。
  1528 +//
  1529 +// 冲突处置:不 abort、不 stash——保留冲突现场返回失败,由 runAction 仲裁后 halt 交人工。
  1530 +// 陈旧分支与默认分支真冲突意味着两边都动了同一处,只有人能判断取舍;自动 abort 会把这个
  1531 +// 信号抹掉,让下一轮再次撞上同一个坑。
  1532 +function syncBranchWithDefaultPromptM(branch, defaultBranch, c) {
  1533 + return [
  1534 + `# 把默认分支 \`${defaultBranch}\` 合进功能分支 \`${branch}\`(幂等)`,
  1535 + microStepContract(c.root),
  1536 + '',
  1537 + `前置:确认 \`git -C ${c.root} rev-parse --abbrev-ref HEAD\` 等于 \`${branch}\`;不等则直接返回失败(error 写明实际分支),**绝不**自行切分支。`,
  1538 + `跑 \`git -C ${c.root} merge --no-edit ${defaultBranch}\`。`,
  1539 + '按结果分三种:',
  1540 + `1. 输出含 \`Already up to date\` → 分支本就包含默认分支全部提交,返回 \`{ "success": true, "detail": "up-to-date" }\`。`,
  1541 + `2. 合并成功(fast-forward 或产生合并提交)→ 返回 \`{ "success": true, "detail": "merged: <被合入的提交数,取 git -C ${c.root} rev-list --count ${branch}@{1}..${branch}>" }\`。`,
  1542 + `3. 冲突或其它失败 → **保留冲突现场**(绝不跑 \`merge --abort\` / \`reset\` / \`stash\`),返回 \`{ "success": false, "error": "merge conflict", "detail": "<git -C ${c.root} diff --name-only --diff-filter=U 的冲突文件清单>" }\`。`,
  1543 + '## 输出(ACTION_RESULT_SCHEMA)',
  1544 + '- 见上三种分支;`detail` 必填,供上层日志与人工诊断。',
  1545 + ].join('\n')
  1546 +}
  1547 +
1520 1548 // ── lane 物理隔离基础设施(Phase C:worktree 生命周期 + migration 版本段 + lane 测试库)──
1521 1549 // 启用时机:Phase D 波次执行器在 ≥2 宽波次的前置串行段调用(读 maxV / 读 schema 基底 / 探测支持 /
1522 1550 // 建 lane),本阶段只提供确定性原语;串行 / 主根路径(c.lane == null)不触达其中任何一个,行为不变。
... ... @@ -2184,7 +2212,14 @@ async function runBranchSetup(module, c) {
2184 2212 }
2185 2213 if (head.branch !== branch) throw new Error(`HALT branchSetup-branch-mismatch ${branch}: ${ADJUDICATE_MAX} 轮后 HEAD 仍在 ${head.branch}`)
2186 2214  
2187   - log(`branch-setup: ${id} → ${branch}`)
  2215 + // 默认分支 → 功能分支同步(幂等)。新建分支走到这里恒为 up-to-date(起点就是默认分支);
  2216 + // 真正吃这一步的是**复用已存在分支**的两条路径——主根 checkout 与 lane worktree add——
  2217 + // 它们此前不带任何同步,会让上一轮遗留的功能分支看不到其后进入默认分支的 REQ 卡片 /
  2218 + // migration / docs 增量,子代理据残缺事实自行发明实现,直到 milestone merge 才以冲突暴露。
  2219 + // 冲突时保留现场 halt(见 prompt 说明),由人工判断取舍。
  2220 + const sync = await runAction(g => syncBranchWithDefaultPromptM(branch, def.branch, c) + g,
  2221 + {site:`branchSetup-sync-default:${branch}`, grp:'Milestone', label: lbl('sync'), dec: c.decisions, root: c.root})
  2222 + log(`branch-setup: ${id} → ${branch}(同步默认分支 ${def.branch}:${sync.detail || 'ok'})`)
2188 2223 }
2189 2224  
2190 2225 // ---- runMilestone:原 milestonePrompt 的 6 步散文流程 → JS 编排 ----
... ...