From ca2fbbd8f8e151e84b9b9dfa2660f2578259f3d4 Mon Sep 17 00:00:00 2001 From: fusion-merge-train Date: Sun, 5 Jul 2026 20:09:25 +0200 Subject: [PATCH] fix(engine): short-circuit zero-commits-ahead branch before AI-merge clean-room churn An AI-merge land of a branch with zero commits ahead of the integration tip (the shape a coding agent that produced no commits leaves behind) builds a clean-room worktree and runs a dependency install before reaching the empty outcome via mergeAndReview. On a non-workspace land the dep install throws hard, so the merge is transient-retried to exhaustion (3/3) and the card is terminally parked failed. Short-circuit on a CONFIDENT zero-ahead count (a git failure yields NaN and falls through) and return the identical outcome:"empty" shape before the throw-prone churn. Co-Authored-By: Claude Opus 4.8 --- .../engine/src/__tests__/merger-ai.test.ts | 26 +++++++++++++++++++ packages/engine/src/merger-ai.ts | 13 ++++++++++ 2 files changed, 39 insertions(+) diff --git a/packages/engine/src/__tests__/merger-ai.test.ts b/packages/engine/src/__tests__/merger-ai.test.ts index 1b509a4bd..3953c918c 100644 --- a/packages/engine/src/__tests__/merger-ai.test.ts +++ b/packages/engine/src/__tests__/merger-ai.test.ts @@ -434,6 +434,32 @@ describe("runAiMerge", () => { expect(store.moveTask).toHaveBeenCalledWith("FN-1", "done", expect.objectContaining({ moveSource: "engine", preserveProgress: true })); }); + it("short-circuits a zero-commits-ahead branch before the clean-room/merge-agent churn (empty-branch wedge)", async () => { + const { dir } = initRepoWithBranch({ branch: "fusion/fn-1" }); + // Move the task branch back onto main's tip → 0 commits ahead of the + // integration branch (the empty-branch shape a coding agent that produced + // no commits leaves behind). + git(dir, "branch -f fusion/fn-1 main"); + const { store } = makeStore(dir); + const mainBefore = git(dir, "rev-parse main"); + const mergeAgent = vi.fn(async () => { /* would build a squash if reached */ }); + + const result = await runAiMerge(store, dir, "FN-1", { manual: true }, { + mergeAgent, + reviewAgent: vi.fn(async () => "REVIEW_VERDICT: approve"), + }); + + // The zero-ahead short-circuit fires BEFORE the clean-room build + dep + // install, so the merge agent is never invoked. In prod that dep install + // throws and gets transient-retried to exhaustion, terminally parking the + // card failed — skipping it is the wedge fix. + expect(mergeAgent).not.toHaveBeenCalled(); + expect(result.noOp).toBe(true); + expect(result.merged).toBe(false); + expect(git(dir, "rev-parse main")).toBe(mainBefore); + expect(store.moveTask).toHaveBeenCalledWith("FN-1", "done", expect.objectContaining({ moveSource: "engine", preserveProgress: true })); + }); + it("demotes a no-commits task with skipped-out work instead of AI empty-merge finalizing done", async () => { const { dir } = initRepoWithBranch({ branch: "fusion/fn-1" }); git(dir, "merge -q fusion/fn-1"); diff --git a/packages/engine/src/merger-ai.ts b/packages/engine/src/merger-ai.ts index 27c5ab95a..306a426d8 100644 --- a/packages/engine/src/merger-ai.ts +++ b/packages/engine/src/merger-ai.ts @@ -622,6 +622,19 @@ export async function landOneRepo( throwIfAborted(signal, taskId); const tipSha = await git(["rev-parse", "--verify", `refs/heads/${integrationBranch}`], repoRootDir); + // Short-circuit a branch with zero commits ahead of the integration tip + // BEFORE building a clean room + installing deps. A truly-empty branch would + // reach the identical `outcome: "empty"` return below via mergeAndReview → + // no squashSha, but only after the throw-prone dep-install churn (which a + // non-workspace land hard-fails), so the merge gets transient-retried to + // exhaustion and the card is parked failed. Only short-circuit on a CONFIDENT + // 0: a git failure yields "" → parseInt → NaN (≠ 0) and falls through. + const aheadRaw = await git(["rev-list", "--count", `${integrationBranch}..${branch}`], repoRootDir).catch(() => ""); + if (Number.parseInt(aheadRaw.trim(), 10) === 0) { + await audit.git({ type: "merge:ai-empty", target: integrationBranch, metadata: { taskId, tipSha } }); + return { outcome: "empty", tipSha, integrationBranch }; + } + // 1. Clean-room worktree at the integration tip. let mergeRoot: string | undefined; let worktreeAdded = false;