feat(FN-4958): complete Step 3 — wire auto-prerebase into merger

Fusion-Task-Id: FN-4958
Fusion-Task-Lineage: 2f1bc742-14e0-4fdd-a980-00051fc3323c
This commit is contained in:
Fusion (runfusion.ai)
2026-05-18 06:00:40 -07:00
committed by gsxdsm
parent 760690c588
commit e93863ca42
3 changed files with 183 additions and 0 deletions

View File

@@ -91,6 +91,7 @@ import { detectMergeOverlap, restoreBranchWinsFiles } from "./merger-overlap-gua
import { checkDiffVolume, DiffVolumeRegressionError } from "./merger-diff-volume-gate.js";
import { ReadonlyViolationError, filterCustomToolsForReadonly } from "./workflow-step-tool-policy.js";
import { detectAlreadyLandedOnMain, type AlreadyMergedDetectionStrategy } from "./already-merged-detector.js";
import { decideAutoPrerebase, probeDivergence, runAutoPrerebase } from "./merger-auto-prerebase.js";
export { DiffVolumeRegressionError } from "./merger-diff-volume-gate.js";
@@ -6530,6 +6531,99 @@ export async function aiMergeTask(
let rebaseHappened = false;
let preferMainRebaseFailureMessage: string | undefined;
if (worktreePath && task.baseCommitSha) {
try {
throwIfAborted(options.signal, taskId);
const { stdout: mainHeadOut } = await execAsync("git rev-parse HEAD", {
cwd: rootDir,
encoding: "utf-8",
});
const mainHead = mainHeadOut.trim();
if (mainHead) {
const divergence = await probeDivergence({
rootDir,
baseCommitSha: task.baseCommitSha,
mainRef: mainHead,
});
const prerebaseDecision = decideAutoPrerebase({
settings,
baseCommitSha: task.baseCommitSha,
commitsBehind: divergence.commitsBehind,
changedFiles: divergence.changedFiles,
worktrunkEnabled: settings.worktrunk?.enabled === true,
});
const prerebaseMetadata = {
reason: prerebaseDecision.reason,
commitsBehind: prerebaseDecision.commitsBehind,
hotMatches: prerebaseDecision.hotMatches,
baseCommitSha: task.baseCommitSha,
mainHead,
taskId,
};
if (!prerebaseDecision.fire) {
await audit.git({
type: "merge:auto-prerebase:skipped",
target: taskId,
metadata: prerebaseMetadata,
});
} else {
throwIfAborted(options.signal, taskId);
const prerebaseResult = await runAutoPrerebase({
rootDir,
worktreePath,
branch,
taskId,
mainHead,
logger: mergerLog,
});
if (prerebaseResult.ok) {
rebaseHappened = true;
await store.appendAgentLog(
taskId,
`Pre-merge auto-prerebase: ${branch} → local HEAD ${mainHead.slice(0, 8)} (${prerebaseDecision.reason})`,
"text",
undefined,
"merger",
);
await audit.git({
type: "merge:auto-prerebase:applied",
target: taskId,
metadata: prerebaseMetadata,
});
} else {
mergerLog.warn(`${taskId}: auto-prerebase failed (${prerebaseResult.error ?? "unknown"}) — proceeding to existing rebase cascade`);
await audit.git({
type: "merge:auto-prerebase:failed",
target: taskId,
metadata: {
...prerebaseMetadata,
error: prerebaseResult.error ?? "unknown",
},
});
}
}
}
} catch (err: unknown) {
rethrowIfMergeAborted(err);
mergerLog.warn(`${taskId}: auto-prerebase probe failed (${getCommandErrorMessage(err)}) — proceeding to existing rebase cascade`);
await audit.git({
type: "merge:auto-prerebase:failed",
target: taskId,
metadata: {
reason: "no-divergence",
commitsBehind: 0,
hotMatches: [],
baseCommitSha: task.baseCommitSha,
mainHead: "",
taskId,
error: getCommandErrorMessage(err),
},
});
}
}
// Semantic guards: prefer-main with no rebase available is incoherent —
// the strategy depends on rebase to honor main's deletions. Fail fast
// before we waste work attempting a merge that can't deliver its promise.