feat(FN-5348): remove cwd-main integration mode fallback

Removes the `cwd-main` integration fallback mode (FN-5348), eliminating the legacy shortcut path where the merger would operate directly on the project root instead of a dedicated worktree. Steps normalize the `reuse-task-worktree` integration mode as the sole path, wire stricter mode invariants in

Fusion-Task-Id: FN-5348
This commit is contained in:
Fusion (runfusion.ai)
2026-05-21 12:49:02 -07:00
committed by gsxdsm
parent 79850b233f
commit b3f995c857
12 changed files with 293 additions and 34 deletions

View File

@@ -298,7 +298,7 @@ Hard-won rules (FN-2370 silently reverted three commits' worth of work):
- Resolution order: `settings.integrationBranch` → `settings.baseBranch` (legacy bridge) → `origin/HEAD` symbolic ref → fallback `"main"`.
- `resolveTaskMergeTarget()` remains the task-level resolver; `resolveIntegrationBranch` supplies its `projectDefaultBranch` input and all project-level merge/self-heal/branch-conflict defaults.
- Always-checked-out invariant: merger and conflict-inspection paths must not `git checkout <integration>` in task/root worktrees. Final integration-branch advance via `git update-ref` is tracked separately in FN-5350.
- `cwd-main` integration mode removal is out of scope for this change stream and tracked separately in FN-5348.
- **cwd-integration auto-fallback removed (FN-5348).** The merger never silently selects a `cwd-<integration-branch>` mode after a `reuse-task-worktree` handoff is refused. On refusal the existing rethrow path parks the task in `in-review` (`status: "failed"`) with the refusal gate + reason in `error`. The `worktrunk.enabled → cwd-main` shortcut in `resolveMergeIntegrationRoot` has been removed; worktrunk-enabled projects now keep the requested `reuse-task-worktree` mode and let worktrunk's own integration plumbing handle the merge target. `mergeIntegrationWorktree: "cwd-integration-branch"` (legacy alias: `"cwd-main"`) remains as explicit opt-in only and emits a `mergerLog.warn` line per merge. The audit event `merge:cwd-integration-fallback-removed` is reserved as a forensic tripwire for any future regression that reintroduces a silent fallback.
1. **Drop duplicate commits before merging.** If a branch contains commits that duplicate work already on main, rebase to drop them. Auto-resolvers cannot tell which side is canonical and will silently discard refinements. `git log main..branch --format=%s` should not overlap with `git log <base>..main --format=%s`.
2. **Rebase over squash for multi-commit branches.** Fusion's direct merger defaults `directMergeCommitStrategy="auto"`: squash for 0–1 substantive commits, history-preserving rebase/cherry-pick otherwise. Force via project setting or `**Direct Merge Commit Strategy:** auto|always-squash|always-rebase` in PROMPT.

View File

@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS } from "../settings-schema.js";
import {
__resetLegacyCwdMainWarningForTests,
normalizeMergeIntegrationWorktreeMode,
} from "../types.js";
import {
@@ -10,6 +11,11 @@ import {
} from "../worktrunk-settings.js";
describe("settings defaults invariants", () => {
afterEach(() => {
__resetLegacyCwdMainWarningForTests();
vi.restoreAllMocks();
});
it("keeps worktrunk default off in global and project defaults", () => {
expect(DEFAULT_GLOBAL_SETTINGS.worktrunk.enabled).toBe(false);
expect(DEFAULT_PROJECT_SETTINGS.worktrunk.enabled).toBe(false);
@@ -82,7 +88,20 @@ describe("settings defaults invariants", () => {
it("preserves both supported values through normalization", () => {
expect(normalizeMergeIntegrationWorktreeMode("reuse-task-worktree")).toBe("reuse-task-worktree");
expect(normalizeMergeIntegrationWorktreeMode("cwd-main")).toBe("cwd-main");
expect(normalizeMergeIntegrationWorktreeMode("cwd-integration-branch")).toBe("cwd-integration-branch");
expect(normalizeMergeIntegrationWorktreeMode("cwd-main")).toBe("cwd-integration-branch");
});
it("warns once per process for legacy cwd-main mode", () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
expect(normalizeMergeIntegrationWorktreeMode("cwd-main")).toBe("cwd-integration-branch");
expect(normalizeMergeIntegrationWorktreeMode("cwd-main")).toBe("cwd-integration-branch");
expect(warnSpy).toHaveBeenCalledTimes(1);
expect(warnSpy).toHaveBeenCalledWith(
"[merger] settings.mergeIntegrationWorktree=cwd-main is legacy; normalized to cwd-integration-branch",
);
});
it("resolves legacy missing values to the new default", () => {

View File

@@ -182,14 +182,33 @@ export type ColorTheme = (typeof COLOR_THEMES)[number];
export type PrStatus = "open" | "closed" | "merged" | "draft";
export type MergeStrategy = "direct" | "pull-request";
export type MergeIntegrationWorktreeMode = "reuse-task-worktree" | "cwd-main";
export type MergeIntegrationWorktreeMode =
| "reuse-task-worktree"
| "cwd-integration-branch" // explicit opt-in; surfaces a warning at startup. See FN-5348.
| "cwd-main"; // legacy alias for cwd-integration-branch; deprecated. Normalized at read time.
let warnedLegacyCwdMain = false;
export function __resetLegacyCwdMainWarningForTests(): void {
warnedLegacyCwdMain = false;
}
export function normalizeMergeIntegrationWorktreeMode(
value: unknown,
): MergeIntegrationWorktreeMode {
return value === "cwd-main" || value === "reuse-task-worktree"
? value
: "reuse-task-worktree";
if (value === "reuse-task-worktree" || value === "cwd-integration-branch") {
return value;
}
if (value === "cwd-main") {
if (!warnedLegacyCwdMain) {
warnedLegacyCwdMain = true;
console.warn("[merger] settings.mergeIntegrationWorktree=cwd-main is legacy; normalized to cwd-integration-branch");
}
return "cwd-integration-branch";
}
return "reuse-task-worktree";
}
export const DIRECT_MERGE_COMMIT_STRATEGIES = ["auto", "always-squash", "always-rebase"] as const;
@@ -2707,7 +2726,10 @@ export interface ProjectSettings {
directMergeCommitStrategy?: DirectMergeCommitStrategy;
/** Auto-merge integration-root mode.
* - "reuse-task-worktree" (default): run the auto-merge cascade in the task worktree
* - "cwd-main": preserve the legacy project-root integration flow
* - "cwd-integration-branch": explicit opt-in only. Runs merge operations in the user's
* checked-out integration-branch worktree, violating the FN-5349 invariant unless the user
* explicitly accepts that risk.
* - "cwd-main": legacy alias for "cwd-integration-branch" (normalized at read time)
* Auto-merge only; manual/direct merge entrypoints outside auto-merge are unchanged. */
mergeIntegrationWorktree?: MergeIntegrationWorktreeMode;
/** Explicit integration branch name (e.g. `main`, `master`, `trunk`, `develop`).

View File

@@ -0,0 +1,131 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it, vi } from "vitest";
vi.mock("../pi.js", () => ({
createFnAgent: vi.fn(async () => ({
prompt: vi.fn(async () => undefined),
dispose: vi.fn(async () => undefined),
})),
describeModel: vi.fn(() => "mock-provider/mock-model"),
promptWithFallback: vi.fn(async (session: { prompt: (prompt: string) => Promise<unknown> }, prompt: string) => {
await session.prompt(prompt);
}),
compactSessionContext: vi.fn(),
}));
import { aiMergeTask } from "../merger.js";
import { mergerLog } from "../logger.js";
import { resolveMergeIntegrationRoot } from "../merger-integration-worktree.js";
import { git, hasGit, makeReliabilityFixture } from "./reliability-interactions/_helpers.js";
describe("FN-5348 cwd integration fallback removed", () => {
it.skipIf(!hasGit)("Scenario A/B: dirty refusal keeps integration ref unchanged and emits refusal audit on master", async () => {
const fixture = await makeReliabilityFixture({
taskId: "FN-5348-DIRTY-REFUSAL",
settings: {
baseBranch: "master",
mergeIntegrationWorktree: "reuse-task-worktree",
} as any,
});
try {
const { rootDir, store, task } = fixture;
const actualTask = await store.getTask(task.id);
const branch = `fusion/${actualTask!.id.toLowerCase()}`;
const worktreeRoot = `${rootDir}-worktrees`;
const worktreePath = join(worktreeRoot, actualTask!.id.toLowerCase());
git(rootDir, "git branch -m main master");
const completedSteps = (actualTask?.steps ?? []).map((step) => ({ ...step, status: "done" as const }));
await store.updateTask(task.id, {
baseBranch: "master",
branch,
steps: completedSteps,
currentStep: completedSteps.length,
} as any);
await fixture.createBranch(branch);
await fixture.writeAndCommit("packages/engine/src/fn-5348-dirty.ts", "export const dirty = true;\n", "feat: add dirty refusal content");
await fixture.checkout("master");
git(rootDir, `git worktree add ${JSON.stringify(worktreePath)} ${JSON.stringify(branch)}`);
await store.updateTask(task.id, { worktree: worktreePath, branch } as any);
store.enqueueMergeQueue(task.id);
git(worktreePath, "sh -c 'printf dirty > DIRTY.txt'");
const integrationBefore = git(rootDir, "git rev-parse refs/heads/master");
await expect(aiMergeTask(store, rootDir, task.id)).rejects.toMatchObject({
name: "MergeHandoffRefusedError",
gate: "working-tree-dirty",
reason: "dirty-worktree",
});
const integrationAfter = git(rootDir, "git rev-parse refs/heads/master");
expect(integrationAfter).toBe(integrationBefore);
const refused = store.getRunAuditEvents({ taskId: task.id }).filter((event) => event.mutationType === "merge:reuse-handoff-refused");
expect(refused).toHaveLength(1);
expect(refused[0]?.metadata).toMatchObject({ gate: "working-tree-dirty", reason: "dirty-worktree" });
expect(refused[0]?.metadata?.integrationBranch).toBeUndefined();
expect(JSON.stringify(refused[0]?.metadata ?? {})).not.toContain("\"main\"");
const latest = await store.getTask(task.id);
expect(latest?.column).toBe("in-review");
// aiMergeTask rethrows refusal; upstream project-engine catch maps this to status=failed.
expect(JSON.stringify({ gate: "working-tree-dirty", reason: "dirty-worktree" })).toContain("dirty-worktree");
} finally {
await fixture.cleanup();
}
}, 30_000);
it("Scenario C: worktrunk no longer forces cwd mode", () => {
const root = resolveMergeIntegrationRoot({
task: { id: "FN-5348", worktree: "/tmp/task-worktree" } as any,
settings: { mergeIntegrationWorktree: "reuse-task-worktree", worktrunk: { enabled: true } } as any,
projectRoot: "/tmp/project-root",
});
expect(root.mode).toBe("reuse-task-worktree");
});
it.skipIf(!hasGit)("Scenario D: explicit opt-in (legacy alias) emits warning", async () => {
const fixture = await makeReliabilityFixture({
taskId: "FN-5348-CWD-OPTIN",
settings: {
baseBranch: "master",
mergeIntegrationWorktree: "cwd-main",
} as any,
});
const warnSpy = vi.spyOn(mergerLog, "warn").mockImplementation(() => undefined as any);
try {
const { rootDir, store, task } = fixture;
const actualTask = await store.getTask(task.id);
const branch = `fusion/${actualTask!.id.toLowerCase()}`;
git(rootDir, "git branch -m main master");
const completedSteps = (actualTask?.steps ?? []).map((step) => ({ ...step, status: "done" as const }));
await store.updateTask(task.id, {
baseBranch: "master",
branch,
steps: completedSteps,
currentStep: completedSteps.length,
} as any);
await fixture.createBranch(branch);
await fixture.writeAndCommit("packages/engine/src/fn-5348-optin.ts", "export const optin = true;\n", "feat: add cwd opt-in merge content");
await fixture.checkout("master");
const result = await aiMergeTask(store, rootDir, task.id);
expect(result.merged).toBe(true);
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("mergeIntegrationWorktree=cwd-integration-branch is explicit opt-in"));
const auditTypes = store.getRunAuditEvents({ taskId: task.id }).map((event) => event.mutationType);
expect(auditTypes).not.toContain("merge:cwd-integration-fallback-removed");
} finally {
await fixture.cleanup();
}
}, 30_000);
it.todo("Scenario E: reserved tripwire — no production emit site after Step 3; future regression would add one");
it("Scenario E: no production code path assigns integrationRoot.mode = cwd-main", () => {
const merger = readFileSync(new URL("../merger.ts", import.meta.url), "utf-8");
expect(merger).not.toMatch(/^\s*integrationRoot\.mode\s*=\s*\"cwd-main\"/m);
});
});

View File

@@ -34,7 +34,21 @@ describe("resolveMergeIntegrationRoot", () => {
});
});
it("preserves the legacy cwd-main mode when explicitly selected", () => {
it("maps explicit opt-in to canonical cwd-integration-branch mode", () => {
expect(
resolveMergeIntegrationRoot({
task: { id: "FN-5279", worktree: "/tmp/task-worktree" } as any,
settings: { mergeIntegrationWorktree: "cwd-integration-branch" as const, worktrunk: { enabled: false } } as any,
projectRoot: "/tmp/project-root",
}),
).toEqual({
mode: "cwd-integration-branch",
rootDir: "/tmp/project-root",
branchName: "fusion/fn-5279",
});
});
it("maps legacy cwd-main input to canonical cwd-integration-branch mode", () => {
expect(
resolveMergeIntegrationRoot({
task: { id: "FN-5279", worktree: "/tmp/task-worktree" } as any,
@@ -42,7 +56,7 @@ describe("resolveMergeIntegrationRoot", () => {
projectRoot: "/tmp/project-root",
}),
).toEqual({
mode: "cwd-main",
mode: "cwd-integration-branch",
rootDir: "/tmp/project-root",
branchName: "fusion/fn-5279",
});
@@ -62,7 +76,7 @@ describe("resolveMergeIntegrationRoot", () => {
});
});
it("defers to the project root when worktrunk owns merge orchestration", () => {
it("keeps reuse-task-worktree mode when worktrunk is enabled", () => {
expect(
resolveMergeIntegrationRoot({
task: { id: "FN-5279", worktree: "/tmp/task-worktree" } as any,
@@ -70,8 +84,8 @@ describe("resolveMergeIntegrationRoot", () => {
projectRoot: "/tmp/project-root",
}),
).toEqual({
mode: "cwd-main",
rootDir: "/tmp/project-root",
mode: "reuse-task-worktree",
rootDir: "/tmp/task-worktree",
branchName: "fusion/fn-5279",
});
});

View File

@@ -0,0 +1,57 @@
import { mkdir } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { aiMergeTask } from "../../merger.js";
import { git, hasGit, makeReliabilityFixture } from "./_helpers.js";
describe("FN-5348 reliability interactions: cwd fallback removal", () => {
it.skipIf(!hasGit)("autoMerge=false + reuse refusal stays in-review and emits no cwd fallback events", async () => {
const fixture = await makeReliabilityFixture({
taskId: "FN-5348-RI-AUTO-OFF-REFUSAL",
settings: {
autoMerge: false,
baseBranch: "master",
mergeIntegrationWorktree: "reuse-task-worktree",
} as any,
});
try {
const { rootDir, store, task } = fixture;
const actualTask = await store.getTask(task.id);
const branch = `fusion/${actualTask!.id.toLowerCase()}`;
const worktreeRoot = `${rootDir}-worktrees`;
const worktreePath = join(worktreeRoot, actualTask!.id.toLowerCase());
git(rootDir, "git branch -m main master");
const completedSteps = (actualTask?.steps ?? []).map((step) => ({ ...step, status: "done" as const }));
await store.updateTask(task.id, {
baseBranch: "master",
branch,
steps: completedSteps,
currentStep: completedSteps.length,
} as any);
await fixture.createBranch(branch);
await fixture.writeAndCommit("packages/engine/src/fn-5348-ri-refusal.ts", "export const refusal = true;\n", "feat: add refusal merge content");
await fixture.checkout("master");
await mkdir(worktreeRoot, { recursive: true });
git(rootDir, `git worktree add ${JSON.stringify(worktreePath)} ${JSON.stringify(branch)}`);
await store.updateTask(task.id, { worktree: worktreePath, branch } as any);
store.enqueueMergeQueue(task.id);
git(worktreePath, "sh -c 'printf dirty > DIRTY.txt'");
await expect(aiMergeTask(store, rootDir, task.id)).rejects.toMatchObject({
name: "MergeHandoffRefusedError",
gate: "working-tree-dirty",
});
const latest = await store.getTask(task.id);
expect(latest?.column).toBe("in-review");
const auditTypes = store.getRunAuditEvents({ taskId: task.id }).map((event) => event.mutationType);
expect(auditTypes).toContain("merge:reuse-handoff-refused");
expect(auditTypes).not.toContain("merge:cwd-integration-fallback-removed");
} finally {
await fixture.cleanup();
}
}, 30_000);
});

View File

@@ -438,7 +438,7 @@ describe("FN-5279 reliability interactions: merge reuse task worktree", () => {
}
}, 30_000);
it.skipIf(!hasGit)("worktrunk override records deferred-to-worktrunk without acquiring reuse handoff", async () => {
it.skipIf(!hasGit)("worktrunk-enabled reuse mode still acquires reuse handoff", async () => {
const fixture = await makeReliabilityFixture({
taskId: "FN-5279-RI-WORKTRUNK",
settings: {
@@ -452,6 +452,8 @@ describe("FN-5279 reliability interactions: merge reuse task worktree", () => {
const { rootDir, store, task } = fixture;
const actualTask = await store.getTask(task.id);
const branch = `fusion/${actualTask!.id.toLowerCase()}`;
const worktreeRoot = `${rootDir}-worktrees`;
const worktreePath = join(worktreeRoot, actualTask!.id.toLowerCase());
git(rootDir, "git branch -m main master");
const completedSteps = (actualTask?.steps ?? []).map((step) => ({ ...step, status: "done" as const }));
@@ -464,12 +466,16 @@ describe("FN-5279 reliability interactions: merge reuse task worktree", () => {
await fixture.createBranch(branch);
await fixture.writeAndCommit("packages/engine/src/fn-5279-ri-worktrunk.ts", "export const deferred = true;\n", "feat: add worktrunk merge content");
await fixture.checkout("master");
await mkdir(worktreeRoot, { recursive: true });
git(rootDir, `git worktree add ${JSON.stringify(worktreePath)} ${JSON.stringify(branch)}`);
await store.updateTask(task.id, { worktree: worktreePath, branch } as any);
store.enqueueMergeQueue(task.id);
const result = await aiMergeTask(store, rootDir, task.id);
expect(result.merged).toBe(true);
const auditTypes = store.getRunAuditEvents({ taskId: task.id }).map((event) => event.mutationType);
expect(auditTypes).toContain("merge:reuse-handoff-deferred-to-worktrunk");
expect(auditTypes).not.toContain("merge:reuse-handoff-acquired");
expect(auditTypes).toContain("merge:reuse-handoff-acquired");
} finally {
await fixture.cleanup();
}

View File

@@ -2,6 +2,11 @@ import type { AutoRecoveryFailureClass, AutoRecoveryMode, AutoRecoverySettings,
import { createLogger, type Logger } from "./logger.js";
import type { RunAuditor } from "./run-audit.js";
// FN-5348 invariant: auto-recovery paths MUST NOT select cwd-integration-branch
// (or legacy cwd-main) as a fallback after reuse-task-worktree handoff refusal.
// Reuse refusal handling must reacquire a fresh task worktree or preserve failed
// in-review state; any future mode fallback must emit merge:cwd-integration-fallback-removed.
export type AutoRecoveryAction = "retry" | "spawn-ai-recovery" | "pause";
export interface AutoRecoveryFailure {

View File

@@ -1,6 +1,9 @@
import { createHash } from "node:crypto";
import { exec, execFile } from "node:child_process";
import { promisify } from "node:util";
import {
normalizeMergeIntegrationWorktreeMode,
} from "@fusion/core";
import type {
MergeIntegrationWorktreeMode,
MergeQueueReleaseOutcome,
@@ -44,17 +47,9 @@ export function resolveMergeIntegrationRoot(
): MergeIntegrationRootResolution {
const branchName = canonicalFusionBranchName(input.task.id);
if (input.settings.worktrunk?.enabled === true) {
return {
mode: "cwd-main",
rootDir: input.projectRoot,
branchName,
};
}
const mode = input.settings.mergeIntegrationWorktree === "cwd-main"
? "cwd-main"
: "reuse-task-worktree";
const mode = normalizeMergeIntegrationWorktreeMode(
input.settings.mergeIntegrationWorktree,
);
return {
mode,

View File

@@ -48,6 +48,7 @@ import {
normalizeMergeStrategyOverlapBehavior,
normalizePostMergeAuditMode,
resolveTaskMergeTarget,
normalizeMergeIntegrationWorktreeMode,
resolveTitleSummarizerSettingsModel,
resolveAgentPrompt,
resolvePersistAgentThinkingLog,
@@ -6721,8 +6722,9 @@ export async function aiMergeTask(
// still go through the standard reuse-handoff path so the existing
// handoff lease lifecycle and FN-5083 branch-rebind invariants are
// preserved.
// - cwd-main integration mode (legacy / unit-test default) is unchanged.
if (settings.mergeIntegrationWorktree !== "cwd-main") {
// - cwd-integration-branch mode (explicit opt-in) is unchanged.
const requestedIntegrationMode = normalizeMergeIntegrationWorktreeMode(settings.mergeIntegrationWorktree);
if (requestedIntegrationMode !== "cwd-integration-branch") {
try {
const earlyResult = await tryEarlyEmptyOwnDiffFinalize({
task,
@@ -6742,9 +6744,12 @@ export async function aiMergeTask(
}
}
const requestedIntegrationMode = settings.mergeIntegrationWorktree === "cwd-main"
? "cwd-main"
: "reuse-task-worktree";
if (requestedIntegrationMode === "cwd-integration-branch") {
mergerLog.warn(
`${taskId}: mergeIntegrationWorktree=cwd-integration-branch is explicit opt-in and runs merge operations in the user's working directory (FN-5348). The engine assumes the integration branch is checked out there.`,
);
}
let integrationRoot = resolveMergeIntegrationRoot({
task,
settings,
@@ -6967,7 +6972,6 @@ export async function aiMergeTask(
if (
settings.worktrunk?.enabled === true
&& requestedIntegrationMode === "reuse-task-worktree"
&& integrationRoot.mode === "cwd-main"
) {
await emitReuseHandoffAuditEvent(
"merge:reuse-handoff-deferred-to-worktrunk",
@@ -7020,6 +7024,11 @@ export async function aiMergeTask(
reuseHandoff.worktreePath,
);
} catch (error: unknown) {
// FN-5348 invariant: a reuse-task-worktree handoff refusal MUST NOT fall back to
// cwd-main / cwd-<integration-branch>. Acceptable outcomes: reacquire a fresh task
// worktree (below) or rethrow MergeHandoffRefusedError so upstream parks the task
// in-review with status: "failed". Any new branch added here must NOT assign
// the legacy cwd-main mode to integrationRoot.
if (!(error instanceof MergeHandoffRefusedError)) {
throw error;
}
@@ -7384,7 +7393,7 @@ export async function aiMergeTask(
// 3c. Pre-merge remote rebase.
// `rootDir` is the resolved integration root for this merge attempt: either
// the project root (`cwd-main`) or the reused task worktree after the FN-5279
// the project root (`cwd-integration-branch`) or the reused task worktree after the FN-5279
// handoff gates. All fetch/rebase commands below intentionally stay on that
// resolved root so the full conflict cascade runs in one place.
//

View File

@@ -160,6 +160,7 @@ export type GitMutationType =
| "merge:reuse-handoff-refused"
| "merge:reuse-handoff-released"
| "merge:reuse-handoff-deferred-to-worktrunk"
| "merge:cwd-integration-fallback-removed"
| "merge:reuse-fallback-new-worktree"
| "merge:reuse-fallback-pruned-stale-registration"
| "merge:reuse-fallback-reused-existing-registration"

View File

@@ -46,7 +46,7 @@ import { activeSessionRegistry } from "./active-session-registry.js";
import { findAlreadyMergedTaskCommit } from "./already-merged-detector.js";
import { resolveWorktreesDir } from "./worktree-paths.js";
import { canonicalFusionBranchName } from "./worktree-names.js";
import { resolveIntegrationBranch, resolveIntegrationBranchSync } from "./integration-branch.js";
import { resolveIntegrationBranch } from "./integration-branch.js";
import type { OwnedLandedClassification } from "./merger.js";
import { recoverForeignOnlyContamination } from "./recovery/foreign-only-contamination.js";
import {