Skip to content
This repository was archived by the owner on Jun 8, 2026. It is now read-only.

Commit 08d31f8

Browse files
authored
fix: remove greedy worktree reuse — PLAN always creates fresh worktree (#22)
findExistingLoopWorktree grabbed ANY prior loop's worktree, causing new PLAN loops to reuse stale worktrees from unrelated loops. Removed it. PLAN now always creates a fresh worktree keyed by loopId (matches ECS harness which always clones fresh). EXECUTE/REQUEST_CHANGES still reuse the parent's worktree via findWorktreeForBranch(parentBranchName).
1 parent fd46a14 commit 08d31f8

1 file changed

Lines changed: 15 additions & 50 deletions

File tree

apps/desktop/src/server/operations/symphony-loop.ts

Lines changed: 15 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -373,32 +373,10 @@ function findWorktreeForBranch(
373373
return null;
374374
}
375375

376-
/** Find any existing symphony loop worktree for a repo (reuse across loops). */
377-
function findExistingLoopWorktree(
378-
expandedRepoPath: string
379-
): string | null {
380-
try {
381-
const output = execSync("git worktree list --porcelain", {
382-
cwd: expandedRepoPath,
383-
encoding: "utf-8",
384-
stdio: "pipe",
385-
timeout: 10_000,
386-
});
387-
388-
let currentWorktree: string | null = null;
389-
for (const line of output.split("\n")) {
390-
if (line.startsWith("worktree ")) {
391-
currentWorktree = line.slice("worktree ".length);
392-
}
393-
if (line.startsWith("branch ") && line.includes("/symphony/loop-")) {
394-
return currentWorktree;
395-
}
396-
}
397-
} catch {
398-
// fall through
399-
}
400-
return null;
401-
}
376+
// findExistingLoopWorktree was removed — it greedy-matched ANY loop worktree
377+
// from ANY prior loop, causing new PLAN loops to reuse stale worktrees.
378+
// PLAN always creates a fresh worktree. EXECUTE/REQUEST_CHANGES reuse via
379+
// findWorktreeForBranch(parentBranchName) which matches the specific parent.
402380

403381
// ---------------------------------------------------------------------------
404382
// Per-command artifact writing
@@ -918,30 +896,17 @@ async function handleLoopRequest(
918896
});
919897
return;
920898
} else if (body.command === "PLAN") {
921-
// PLAN: reuse existing symphony loop worktree if available, else create new
922-
worktreeDir = findExistingLoopWorktree(expandedRepoPath);
923-
if (worktreeDir) {
924-
loopLog(body.loopId, `Reusing existing loop worktree: ${worktreeDir}`);
925-
try {
926-
assertPathAllowed(worktreeDir, allowedDirs);
927-
} catch (e) {
928-
if (e instanceof DirectoryNotAllowedError) {
929-
json(context, 403, { error: `Worktree path not allowed: ${worktreeDir}` });
930-
return;
931-
}
932-
throw e;
933-
}
934-
} else {
935-
const loopBranch = `symphony/loop-${pickStableId(body)}`;
936-
worktreeDir = resolveLoopWorktreeDir(expandedRepoPath, pickStableId(body));
937-
await ensureWorktree(
938-
expandedRepoPath,
939-
worktreeDir,
940-
loopBranch,
941-
body.repo?.branch ?? "main"
942-
);
943-
loopLog(body.loopId, `Created new loop worktree: ${worktreeDir}`);
944-
}
899+
// PLAN: always create a fresh worktree (matches ECS harness which always clones fresh).
900+
// No reuse — each PLAN loop gets its own worktree keyed by loopId.
901+
const loopBranch = `symphony/loop-${pickStableId(body)}`;
902+
worktreeDir = resolveLoopWorktreeDir(expandedRepoPath, pickStableId(body));
903+
await ensureWorktree(
904+
expandedRepoPath,
905+
worktreeDir,
906+
loopBranch,
907+
body.repo?.branch ?? "main"
908+
);
909+
loopLog(body.loopId, `Created loop worktree: ${worktreeDir}`);
945910
claudeWorkDir = path.join(worktreeDir, ".claude", "work");
946911
await fs.mkdir(claudeWorkDir, { recursive: true });
947912
await writeArtifactsForPlan(claudeWorkDir, body.artifacts, body.prompt);

0 commit comments

Comments
 (0)