From cd8a91c77ed49810a4ce49c4ae50dea477fc3e6b Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 17 Jul 2026 11:08:34 +0000 Subject: [PATCH] fix(worktrees): make lifecycle operations transactional --- packages/server/src/server/bootstrap.test.ts | 31 +++++++ packages/server/src/server/bootstrap.ts | 10 ++- .../src/server/paseo-worktree-service.test.ts | 56 +++++++++++- .../src/server/paseo-worktree-service.ts | 82 +++++++++++------- .../workspace-recovery-service.test.ts | 56 ++++++++++++ .../workspace-recovery-service.ts | 40 ++++++--- .../server/workspace-archive-service.test.ts | 41 ++++++++- .../src/server/workspace-archive-service.ts | 26 ++++-- packages/server/src/utils/worktree.ts | 85 +++++++++++++------ 9 files changed, 343 insertions(+), 84 deletions(-) diff --git a/packages/server/src/server/bootstrap.test.ts b/packages/server/src/server/bootstrap.test.ts index 3103b5f48..6f705ec55 100644 --- a/packages/server/src/server/bootstrap.test.ts +++ b/packages/server/src/server/bootstrap.test.ts @@ -23,3 +23,34 @@ test("reconciliation emits workspace updates when observer sync fails", async () expect(emittedWorkspaceIds).toEqual([["ws-reclassified"]]); }); + +test("reconciliation isolates workspace update failures between sessions", async () => { + const emittedWorkspaceIds: string[][] = []; + const warnings: unknown[] = []; + + await fanOutReconciledWorkspaceUpdates({ + sessions: [ + { + syncWorkspaceGitObserversForExternalWorkspaceIds: async () => {}, + emitWorkspaceUpdatesForExternalWorkspaceIds: async () => { + throw new Error("session closed"); + }, + }, + { + syncWorkspaceGitObserversForExternalWorkspaceIds: async () => {}, + emitWorkspaceUpdatesForExternalWorkspaceIds: async (workspaceIds) => { + emittedWorkspaceIds.push(Array.from(workspaceIds)); + }, + }, + ], + workspaceIds: ["ws-reclassified"], + logger: { + warn: (context) => { + warnings.push(context); + }, + }, + }); + + expect(emittedWorkspaceIds).toEqual([["ws-reclassified"]]); + expect(warnings).toHaveLength(1); +}); diff --git a/packages/server/src/server/bootstrap.ts b/packages/server/src/server/bootstrap.ts index 09033626a..135f0efc4 100644 --- a/packages/server/src/server/bootstrap.ts +++ b/packages/server/src/server/bootstrap.ts @@ -110,9 +110,13 @@ export async function fanOutReconciledWorkspaceUpdates(input: { "Failed to sync workspace Git observers after reconciliation", ); } - await session.emitWorkspaceUpdatesForExternalWorkspaceIds(input.workspaceIds, { - skipReconcile: true, - }); + try { + await session.emitWorkspaceUpdatesForExternalWorkspaceIds(input.workspaceIds, { + skipReconcile: true, + }); + } catch (error) { + input.logger.warn({ err: error }, "Failed to emit workspace updates after reconciliation"); + } }), ); } diff --git a/packages/server/src/server/paseo-worktree-service.test.ts b/packages/server/src/server/paseo-worktree-service.test.ts index e02d1dff3..bd30a28a1 100644 --- a/packages/server/src/server/paseo-worktree-service.test.ts +++ b/packages/server/src/server/paseo-worktree-service.test.ts @@ -1,5 +1,5 @@ import { execFileSync } from "node:child_process"; -import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, expect, test, vi } from "vitest"; @@ -223,6 +223,30 @@ test("creates a worktree workspace at the selected project subdirectory", async }); }); +test("seeds an uncommitted exact-project config into the mapped worktree directory", async () => { + const { repoDir, tempDir } = createGitRepo(); + cleanupPaths.push(tempDir); + const sourceDir = path.join(repoDir, "packages", "app"); + mkdirSync(sourceDir, { recursive: true }); + writeFileSync(path.join(sourceDir, "package.json"), "{}\n"); + commitAll(repoDir, "add subproject"); + const config = JSON.stringify({ worktree: { setup: ["npm install"] } }); + writeFileSync(path.join(sourceDir, "paseo.json"), config); + + const result = await createPaseoWorktree( + { + cwd: sourceDir, + worktreeSlug: "seed-nested-config", + runSetup: false, + paseoHome: path.join(tempDir, ".paseo"), + }, + createDeps(), + ); + + expect(readFileSync(path.join(result.workspace.cwd, "paseo.json"), "utf8")).toBe(config); + expect(existsSync(path.join(result.worktree.worktreePath, "paseo.json"))).toBe(false); +}); + test("removes a new worktree when its ref does not contain the selected project directory", async () => { const { repoDir, tempDir } = createGitRepo(); cleanupPaths.push(tempDir); @@ -261,6 +285,36 @@ test("removes a new worktree when its ref does not contain the selected project ).toBe(false); }); +test("removes a new worktree when workspace persistence fails", async () => { + const { repoDir, tempDir } = createGitRepo(); + cleanupPaths.push(tempDir); + const paseoHome = path.join(tempDir, ".paseo"); + const worktreePath = path.join( + await getPaseoWorktreesRoot(repoDir, paseoHome), + "persistence-failure", + ); + + await expect( + createPaseoWorktree( + { + cwd: repoDir, + projectId: "missing-project", + worktreeSlug: "persistence-failure", + runSetup: false, + paseoHome, + }, + createDeps(), + ), + ).rejects.toThrow("Unknown project: missing-project"); + + expect(existsSync(worktreePath)).toBe(false); + expect( + execFileSync("git", ["worktree", "list", "--porcelain"], { cwd: repoDir, stdio: "pipe" }) + .toString() + .includes("persistence-failure"), + ).toBe(false); +}); + test("maps a nested cwd from an existing Paseo worktree into the next worktree", async () => { const { repoDir, tempDir } = createGitRepo(); cleanupPaths.push(tempDir); diff --git a/packages/server/src/server/paseo-worktree-service.ts b/packages/server/src/server/paseo-worktree-service.ts index 534f7dbc2..43a8570d9 100644 --- a/packages/server/src/server/paseo-worktree-service.ts +++ b/packages/server/src/server/paseo-worktree-service.ts @@ -12,7 +12,8 @@ import { } from "./worktree-core.js"; import { mapWorkspaceRelativeCwdToWorktree, - deletePaseoWorktree, + rollbackCreatedPaseoWorktree, + seedPaseoConfigFile, validateBranchSlug, type WorktreeConfig, } from "../utils/worktree.js"; @@ -64,42 +65,57 @@ export async function createPaseoWorktree( ): Promise { const workspaceCwdPlan = await planWorkspaceCwdForWorktree(input.cwd, deps.workspaceGitService); const createdWorktree = await createWorktreeCore(input, deps); - maybeMarkFirstAgentBranchAutoNameEligible({ createdWorktree }); - const workspaceCwd = mapWorkspaceRelativeCwdToWorktree({ - relativeWorkspaceCwd: workspaceCwdPlan.relativeWorkspaceCwd, - targetWorktreePath: createdWorktree.worktree.worktreePath, - }); - if (!(await isDirectory(workspaceCwd))) { + try { + maybeMarkFirstAgentBranchAutoNameEligible({ createdWorktree }); + const workspaceCwd = mapWorkspaceRelativeCwdToWorktree({ + relativeWorkspaceCwd: workspaceCwdPlan.relativeWorkspaceCwd, + targetWorktreePath: createdWorktree.worktree.worktreePath, + }); + if (!(await isDirectory(workspaceCwd))) { + throw new Error(`Selected project directory is missing from the worktree: ${workspaceCwd}`); + } + if (createdWorktree.created) { - await deletePaseoWorktree({ - cwd: createdWorktree.repoRoot, - worktreePath: createdWorktree.worktree.worktreePath, - paseoHome: input.paseoHome, - worktreesBaseRoot: input.worktreesRoot, + await seedPaseoConfigFile({ + sourceCwd: workspaceCwdPlan.inputCwd, + targetCwd: workspaceCwd, }); } - throw new Error(`Selected project directory is missing from the worktree: ${workspaceCwd}`); + const workspace = await deps.workspaceProvisioning.createWorkspaceForWorktree({ + sourceCwd: workspaceCwdPlan.inputCwd, + projectId: input.projectId, + repoRoot: createdWorktree.repoRoot, + cwd: workspaceCwd, + worktreeRoot: createdWorktree.worktree.worktreePath, + branch: createdWorktree.worktree.branchName || null, + baseBranch: resolveIntentBaseBranch(createdWorktree.intent), + title: resolveFirstAgentPromptTitle(input.firstAgentContext), + }); + + deps.github.invalidate({ cwd: createdWorktree.worktree.worktreePath }); + + return { + worktree: createdWorktree.worktree, + intent: createdWorktree.intent, + workspace, + repoRoot: createdWorktree.repoRoot, + created: createdWorktree.created, + }; + } catch (error) { + if (!createdWorktree.created) { + throw error; + } + return rollbackCreatedPaseoWorktree( + { + cwd: createdWorktree.repoRoot, + worktreePath: createdWorktree.worktree.worktreePath, + ...(input.runSetup === false ? { teardownCwds: [] } : {}), + paseoHome: input.paseoHome, + worktreesBaseRoot: input.worktreesRoot, + }, + error, + ); } - const workspace = await deps.workspaceProvisioning.createWorkspaceForWorktree({ - sourceCwd: workspaceCwdPlan.inputCwd, - projectId: input.projectId, - repoRoot: createdWorktree.repoRoot, - cwd: workspaceCwd, - worktreeRoot: createdWorktree.worktree.worktreePath, - branch: createdWorktree.worktree.branchName || null, - baseBranch: resolveIntentBaseBranch(createdWorktree.intent), - title: resolveFirstAgentPromptTitle(input.firstAgentContext), - }); - - deps.github.invalidate({ cwd: createdWorktree.worktree.worktreePath }); - - return { - worktree: createdWorktree.worktree, - intent: createdWorktree.intent, - workspace, - repoRoot: createdWorktree.repoRoot, - created: createdWorktree.created, - }; } async function isDirectory(targetPath: string): Promise { diff --git a/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.test.ts b/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.test.ts index 992cb90c7..91631a563 100644 --- a/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.test.ts +++ b/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.test.ts @@ -191,6 +191,62 @@ describe("workspace recovery", () => { expect(unarchived).toEqual([workspace.workspaceId]); }); + test("keeps an exact-subdirectory workspace archived when its branch lacks that directory", async () => { + const { tempDir, repoDir } = createGitRepository(); + const branch = "feature/without-subproject"; + execFileSync("git", ["branch", branch], { cwd: repoDir, stdio: "pipe" }); + const paseoHome = join(tempDir, "paseo-home"); + const worktreesRoot = join(tempDir, "worktrees"); + const created = await createWorktree({ + cwd: repoDir, + worktreeSlug: "without-subproject", + source: { kind: "checkout-branch", branchName: branch }, + runSetup: false, + paseoHome, + worktreesRoot, + }); + const worktreeRoot = realpathSync(created.worktreePath); + const workspaceCwd = join(worktreeRoot, "packages", "app"); + rmSync(worktreeRoot, { recursive: true, force: true }); + execFileSync("git", ["worktree", "prune"], { cwd: repoDir, stdio: "pipe" }); + + const project = createProject({ rootPath: repoDir }); + const workspace = createWorkspace({ + workspaceId: "ws-missing-restored-subdirectory", + cwd: workspaceCwd, + branch, + worktreeRoot, + mainRepoRoot: repoDir, + }); + const unarchived: string[] = []; + const service = createWorkspaceRecoveryService({ + paseoHome, + worktreesRoot, + getWorkspace: async (workspaceId) => + workspaceId === workspace.workspaceId ? workspace : null, + getProject: async (projectId) => (projectId === project.projectId ? project : null), + isDirectory: async (targetPath) => + existsSync(targetPath) && statSync(targetPath).isDirectory(), + unarchiveWorkspace: async (record) => { + unarchived.push(record.workspaceId); + }, + }); + + await expect(service.restore(workspace.workspaceId)).rejects.toThrow( + "Selected project directory is missing from the restored worktree", + ); + expect(unarchived).toEqual([]); + expect(existsSync(worktreeRoot)).toBe(false); + expect( + execFileSync("git", ["worktree", "list", "--porcelain"], { + cwd: repoDir, + stdio: "pipe", + }) + .toString() + .includes("without-subproject"), + ).toBe(false); + }); + test("keeps the workspace archived when its persisted source repository is missing", async () => { const workspace = createWorkspace({ mainRepoRoot: "/missing-source" }); const { service, unarchived } = createHarness({ diff --git a/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.ts b/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.ts index edb1b9a2e..1d98a9128 100644 --- a/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.ts +++ b/packages/server/src/server/session/workspace-recovery/workspace-recovery-service.ts @@ -1,4 +1,3 @@ -import { mkdir } from "node:fs/promises"; import { basename } from "node:path"; import { createRealpathAwarePathMatcher } from "../../../utils/path.js"; @@ -7,6 +6,7 @@ import { createWorktree, isPaseoOwnedWorktreeCwd, mapWorkspaceCwdToWorktree, + rollbackCreatedPaseoWorktree, } from "../../../utils/worktree.js"; import { WorktreeRequestError, toWorktreeRequestError } from "../../worktree-errors.js"; import { @@ -199,18 +199,36 @@ export function createWorkspaceRecoveryService(deps: { throw toWorktreeRequestError(error); } - const recreatedWorkspacePath = mapWorkspaceCwdToWorktree({ - sourceWorktreePath: previousWorktreePath, - workspaceCwd: workspace.cwd, - targetWorktreePath: recreatedWorktreePath, - }); - if (!createRealpathAwarePathMatcher(workspace.cwd)(recreatedWorkspacePath)) { - throw new WorktreeRequestError({ - code: "unknown", - message: `Recreated worktree diverged from ${workspace.cwd}: ${recreatedWorkspacePath}`, + try { + const recreatedWorkspacePath = mapWorkspaceCwdToWorktree({ + sourceWorktreePath: previousWorktreePath, + workspaceCwd: workspace.cwd, + targetWorktreePath: recreatedWorktreePath, }); + if (!createRealpathAwarePathMatcher(workspace.cwd)(recreatedWorkspacePath)) { + throw new WorktreeRequestError({ + code: "unknown", + message: `Recreated worktree diverged from ${workspace.cwd}: ${recreatedWorkspacePath}`, + }); + } + if (!(await deps.isDirectory(recreatedWorkspacePath))) { + throw new WorktreeRequestError({ + code: "unknown", + message: `Selected project directory is missing from the restored worktree: ${recreatedWorkspacePath}`, + }); + } + } catch (error) { + return rollbackCreatedPaseoWorktree( + { + cwd: sourceRepoRoot, + worktreePath: recreatedWorktreePath, + teardownCwds: [], + paseoHome: deps.paseoHome, + worktreesBaseRoot: deps.worktreesRoot, + }, + error, + ); } - await mkdir(recreatedWorkspacePath, { recursive: true }); } return { inspect, restore }; diff --git a/packages/server/src/server/workspace-archive-service.test.ts b/packages/server/src/server/workspace-archive-service.test.ts index 26ba55b54..a678fd45c 100644 --- a/packages/server/src/server/workspace-archive-service.test.ts +++ b/packages/server/src/server/workspace-archive-service.test.ts @@ -388,13 +388,42 @@ describe("archiveByScope", () => { test("worktree scope archives root and subdirectory workspaces before removing the backing worktree", async () => { const { tempDir, repoDir } = createGitRepo(); + const nestedRelative = path.join("packages", "app"); + const sourceNested = path.join(repoDir, nestedRelative); + mkdirSync(sourceNested, { recursive: true }); + writeFileSync( + path.join(repoDir, "paseo.json"), + JSON.stringify({ + worktree: { + teardown: [ + "node -e \"require('fs').appendFileSync(process.env.PASEO_SOURCE_CHECKOUT_PATH + '/root-scope-teardown.log', process.cwd() + '\\\\n')\"", + ], + }, + }), + ); + writeFileSync( + path.join(sourceNested, "paseo.json"), + JSON.stringify({ + worktree: { + teardown: [ + "node -e \"require('fs').writeFileSync(process.env.PASEO_SOURCE_CHECKOUT_PATH + '/nested-scope-teardown.log', process.cwd())\"", + ], + }, + }), + ); + execFileSync("git", ["add", "."], { cwd: repoDir, stdio: "pipe" }); + execFileSync("git", ["-c", "commit.gpgsign=false", "commit", "-m", "scope teardown"], { + cwd: repoDir, + stdio: "pipe", + }); const paseoHome = path.join(tempDir, ".paseo"); const worktree = await createPaseoOwnedWorktree(repoDir, paseoHome, "worktree-scope"); const workspaceA = "ws-worktree-a"; const workspaceB = "ws-worktree-b"; const workspaceC = "ws-worktree-subdirectory"; - const subdirectory = path.join(worktree.worktreePath, "packages", "app"); - mkdirSync(subdirectory, { recursive: true }); + const subdirectory = path.join(worktree.worktreePath, nestedRelative); + const matchesRoot = createRealpathAwarePathMatcher(worktree.worktreePath); + const matchesSubdirectory = createRealpathAwarePathMatcher(subdirectory); const result = await archiveByScope( createArchiveDeps({ @@ -435,6 +464,14 @@ describe("archiveByScope", () => { expect(result.archivedWorkspaceIds).toHaveLength(3); expect(result.removedDirectory).toBe(true); expect(existsSync(worktree.worktreePath)).toBe(false); + const rootTeardownCwds = readFileSync(path.join(repoDir, "root-scope-teardown.log"), "utf8") + .trim() + .split("\n"); + expect(rootTeardownCwds).toHaveLength(1); + expect(matchesRoot(rootTeardownCwds[0] ?? "")).toBe(true); + expect( + matchesSubdirectory(readFileSync(path.join(repoDir, "nested-scope-teardown.log"), "utf8")), + ).toBe(true); }); test("workspace scope never removes a non-Paseo-owned directory", async () => { diff --git a/packages/server/src/server/workspace-archive-service.ts b/packages/server/src/server/workspace-archive-service.ts index a7498df3b..3cdc3816a 100644 --- a/packages/server/src/server/workspace-archive-service.ts +++ b/packages/server/src/server/workspace-archive-service.ts @@ -75,7 +75,7 @@ interface BackingDirectory { interface ArchiveTarget { backing: BackingDirectory | null; - teardownCwd: string | null; + teardownCwds: string[]; workspaceIds: string[]; } @@ -170,11 +170,11 @@ async function resolveArchiveTarget( { workspaceId }, "Workspace not found for archive-by-scope; skipping", ); - return { backing: null, teardownCwd: null, workspaceIds: [] }; + return { backing: null, teardownCwds: [], workspaceIds: [] }; } return { backing: await resolveWorkspaceBackingDirectory(record, dependencies), - teardownCwd: record.cwd, + teardownCwds: [record.cwd], workspaceIds: [workspaceId], }; } @@ -189,8 +189,6 @@ async function resolveArchiveTarget( }), ) ).filter((workspace): workspace is ActiveWorkspaceRef => workspace !== null); - const exactTarget = createRealpathAwarePathMatcher(scope.targetPath); - const teardownWorkspace = targetWorkspaces.find((workspace) => exactTarget(workspace.cwd)); const persistedMainRepoRoot = targetWorkspaces.find( (workspace) => workspace.mainRepoRoot, )?.mainRepoRoot; @@ -199,7 +197,11 @@ async function resolveArchiveTarget( ...backing, mainRepoRoot: persistedMainRepoRoot ?? backing.mainRepoRoot, }, - teardownCwd: teardownWorkspace?.cwd ?? scope.targetPath, + teardownCwds: uniqueFilesystemPaths( + targetWorkspaces.length > 0 + ? targetWorkspaces.map((workspace) => workspace.cwd) + : [scope.targetPath], + ), workspaceIds: targetWorkspaces.map((workspace) => workspace.workspaceId), }; } @@ -311,7 +313,7 @@ async function maybeRemoveDirectory( await deletePaseoWorktree({ cwd: backing.mainRepoRoot, worktreePath: backing.path, - teardownCwd: target.teardownCwd ?? backing.path, + teardownCwds: target.teardownCwds, worktreesRoot: backing.paseoWorktreesRoot ?? undefined, paseoHome: dependencies.paseoHome, worktreesBaseRoot: dependencies.paseoWorktreesBaseRoot, @@ -330,6 +332,16 @@ async function maybeRemoveDirectory( } } +function uniqueFilesystemPaths(paths: string[]): string[] { + const unique: string[] = []; + for (const candidate of paths) { + if (!unique.some((existing) => createRealpathAwarePathMatcher(existing)(candidate))) { + unique.push(candidate); + } + } + return unique; +} + export type ArchiveWorkspaceContentsDependencies = Pick< ArchiveDependencies, "agentManager" | "agentStorage" | "killTerminalsForWorkspace" | "sessionLogger" diff --git a/packages/server/src/utils/worktree.ts b/packages/server/src/utils/worktree.ts index d99602f38..64d5ae2c7 100644 --- a/packages/server/src/utils/worktree.ts +++ b/packages/server/src/utils/worktree.ts @@ -776,6 +776,23 @@ export async function runWorktreeTeardownCommands(options: { return results; } +export async function seedPaseoConfigFile(options: { + sourceCwd: string; + targetCwd: string; +}): Promise { + const sourceConfigPath = join(options.sourceCwd, "paseo.json"); + const targetConfigPath = join(options.targetCwd, "paseo.json"); + try { + await stat(targetConfigPath); + return; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; + } + await copyFile(sourceConfigPath, targetConfigPath).catch((error) => { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; + }); +} + /** * Get the git common directory (shared across worktrees) for a given cwd. * This is where refs, objects, etc. are stored. @@ -1050,23 +1067,25 @@ export async function resolveExistingWorktreeForSlug({ }; } -export async function deletePaseoWorktree({ - cwd, - worktreePath, - teardownCwd, - worktreeSlug, - worktreesRoot, - paseoHome, - worktreesBaseRoot, -}: { +export interface DeletePaseoWorktreeOptions { cwd: string | null; worktreePath?: string; - teardownCwd?: string; + teardownCwds?: string[]; worktreeSlug?: string; worktreesRoot?: string; paseoHome?: string; worktreesBaseRoot?: string; -}): Promise { +} + +export async function deletePaseoWorktree({ + cwd, + worktreePath, + teardownCwds, + worktreeSlug, + worktreesRoot, + paseoHome, + worktreesBaseRoot, +}: DeletePaseoWorktreeOptions): Promise { if (!worktreePath && !worktreeSlug) { throw new Error("worktreePath or worktreeSlug is required"); } @@ -1101,10 +1120,12 @@ export async function deletePaseoWorktree({ } if (await pathExists(resolvedWorktree)) { - await runWorktreeTeardownCommands({ - worktreePath: resolvedWorktree, - teardownCwd, - }); + for (const teardownCwd of teardownCwds ?? [resolvedWorktree]) { + await runWorktreeTeardownCommands({ + worktreePath: resolvedWorktree, + teardownCwd, + }); + } } if (cwd) { @@ -1132,6 +1153,27 @@ export async function deletePaseoWorktree({ } } +export async function rollbackCreatedPaseoWorktree( + options: DeletePaseoWorktreeOptions, + cause: unknown, +): Promise { + let cleanupError: unknown; + try { + await deletePaseoWorktree(options); + } catch (error) { + cleanupError = error; + } + if (cleanupError) { + const failure = new Error( + `${cause instanceof Error ? cause.message : "Worktree workflow failed"}; rollback also failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`, + { cause }, + ); + Object.assign(failure, { cleanupError }); + throw failure; + } + throw cause; +} + async function pathExists(path: string): Promise { try { await stat(path); @@ -1225,18 +1267,7 @@ export const createWorktree = async ({ : {}), }); - // If paseo.json exists in the main repo but wasn't checked into the worktree - // (e.g. uncommitted on first-time setup), seed the worktree with it so setup - // commands and scripts pick up the user's intended config. - const mainConfigPath = join(cwd, "paseo.json"); - const worktreeConfigPath = join(worktreePath, "paseo.json"); - try { - await stat(worktreeConfigPath); - } catch { - await copyFile(mainConfigPath, worktreeConfigPath).catch((err) => { - if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err; - }); - } + await seedPaseoConfigFile({ sourceCwd: cwd, targetCwd: worktreePath }); if (runSetup) { await runWorktreeSetupCommands({