FN-5794: run worktree init for fresh merge workspaces

Run merge worktree initialization automatically when a merge workspace is newly created.

- detect when integration worktree creation is fresh and invoke the configured init command
- preserve existing behavior for already-initialized worktrees while improving setup reliability
- add integration tests covering init command execution paths for merge worktree setup

Files changed:
 .changeset/fn-5794-merge-worktree-init.md          |  5 +++
 .../__tests__/merger-integration-worktree.test.ts  | 16 +++++++++
 packages/engine/src/merger.ts                      | 39 +++++++++++++++++++++-
 3 files changed, 59 insertions(+), 1 deletion(-)

Fusion-Task-Id: FN-5794

Fusion-Task-Lineage: f368099f-a179-466e-b994-2a503167112c
This commit is contained in:
gsxdsm
2026-05-31 17:52:57 -07:00
parent fc4fc9c3cf
commit fa428a487b
3 changed files with 59 additions and 1 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Run the configured `worktreeInitCommand` when the merger has to create a fresh merge worktree during reuse-worktree reacquisition. This bootstraps newly created merge workspaces before merge verification/workflow steps run, while leaving pooled/reused existing worktrees unchanged.

View File

@@ -1,4 +1,5 @@
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import { readFileSync } from "node:fs";
import { import {
createMockStore, createMockStore,
@@ -222,6 +223,21 @@ describe("probeIntegrationWorktreeState", () => {
}); });
}); });
describe("merger fresh reacquire init wiring", () => {
it("wires fresh acquireTaskWorktree calls with init command execution", () => {
const mergerSource = readFileSync(new URL("../merger.ts", import.meta.url), "utf-8");
const freshAcquireBlock = mergerSource.match(/const acquisition = await acquireTaskWorktree\(\{[\s\S]*?\n\s*\}\);/);
expect(freshAcquireBlock?.[0]).toContain("runInitCommand: true");
expect(freshAcquireBlock?.[0]).toContain("runConfiguredCommand:");
expect(freshAcquireBlock?.[0]).toContain("runConfiguredMergeWorktreeCommand");
});
it("keeps direct-reuse shortcut on existing registrations", () => {
const mergerSource = readFileSync(new URL("../merger.ts", import.meta.url), "utf-8");
expect(mergerSource).toContain("Skip acquireTaskWorktree's");
});
});
describe("acquireReuseHandoff", () => { describe("acquireReuseHandoff", () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();

View File

@@ -7781,7 +7781,9 @@ export async function aiMergeTask(
logger: mergerLog, logger: mergerLog,
audit, audit,
runContext: engineRunContext, runContext: engineRunContext,
runInitCommand: false, runInitCommand: true,
runConfiguredCommand: async (command, cwd, timeoutMs, env) =>
runConfiguredMergeWorktreeCommand(command, cwd, timeoutMs, env, audit),
createWorktree: async (branch, path, _taskId, _startPoint, _allowSiblingBranchRename) => { createWorktree: async (branch, path, _taskId, _startPoint, _allowSiblingBranchRename) => {
await execAsync(`git worktree add -f ${quoteArg(path)} ${quoteArg(branch)}`, { await execAsync(`git worktree add -f ${quoteArg(path)} ${quoteArg(branch)}`, {
cwd: projectRootDir, cwd: projectRootDir,
@@ -11929,6 +11931,41 @@ function getPostMergeScriptSandboxBackend(auditor?: RunAuditor): SandboxBackend
return resolveSandboxBackend({ auditor }); return resolveSandboxBackend({ auditor });
} }
async function runConfiguredMergeWorktreeCommand(
command: string,
cwd: string,
timeoutMs: number,
extraEnv?: NodeJS.ProcessEnv,
auditor?: RunAuditor,
): Promise<{
stdout?: string;
stderr?: string;
exitCode?: number | null;
signal?: NodeJS.Signals | null;
timedOut?: boolean;
bufferExceeded?: boolean;
spawnError?: Error;
}> {
const backend = getPostMergeScriptSandboxBackend(auditor);
const result = await backend.run(command, {
cwd,
encoding: "utf-8",
timeoutMs,
maxBuffer: 10 * 1024 * 1024,
...(extraEnv !== undefined && { env: extraEnv }),
});
return {
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.exitCode,
signal: result.signal,
timedOut: result.timedOut,
bufferExceeded: result.bufferExceeded,
spawnError: result.spawnError,
};
}
/** Execute a script-mode post-merge workflow step in the provided execution directory. */ /** Execute a script-mode post-merge workflow step in the provided execution directory. */
async function executePostMergeScriptStep( async function executePostMergeScriptStep(
store: TaskStore, store: TaskStore,