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

Commit 0344d98

Browse files
authored
Merge pull request #25 from closedloop-ai/fix/pr24-review-followup
fix: address PR #24 review — sanitize slug, fresh PLAN, parentLoopId fallback
2 parents a5b6f47 + 420e6d5 commit 0344d98

1 file changed

Lines changed: 61 additions & 15 deletions

File tree

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

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -928,31 +928,77 @@ async function handleLoopRequest(
928928
});
929929
return;
930930
} else if (body.command === "PLAN" || body.command === "EXECUTE" || body.command === "REQUEST_CHANGES") {
931-
// All repo-based commands share a single worktree keyed by artifact slug.
932-
// PLAN creates it; EXECUTE/REQUEST_CHANGES reuse it. Human-readable branches
933-
// like symphony/PLAN-5 instead of symphony/loop-019cfa96-...
934-
const worktreeKey = body.artifactSlug
935-
? body.artifactSlug.toLowerCase()
936-
: pickStableId(body);
937-
const branchName = body.artifactSlug
938-
? `symphony/${body.artifactSlug}`
931+
// Worktree keyed by artifact slug (e.g., symphony/PLAN-5).
932+
// PLAN always creates fresh; EXECUTE/REQUEST_CHANGES reuse.
933+
// Sanitize slug the same way we sanitize loopId to prevent path traversal.
934+
const sanitizedSlug = body.artifactSlug
935+
? slugifyLoopId(body.artifactSlug)
936+
: null;
937+
const worktreeKey = sanitizedSlug ?? pickStableId(body);
938+
const branchName = sanitizedSlug
939+
? `symphony/${sanitizedSlug}`
939940
: `symphony/loop-${pickStableId(body)}`;
940941

941942
worktreeDir = resolveLoopWorktreeDir(expandedRepoPath, worktreeKey);
942943

943-
// Try to find existing worktree for this artifact's branch
944-
const existingWorktree = findWorktreeForBranch(expandedRepoPath, branchName);
945-
if (existingWorktree) {
946-
worktreeDir = existingWorktree;
947-
loopLog(body.loopId, `Reusing existing worktree: ${worktreeDir} (branch: ${branchName})`);
948-
} else {
944+
if (body.command === "PLAN") {
945+
// PLAN always starts fresh — remove stale worktree if it exists.
946+
// PLAN has requiresParent: false, so it must not inherit prior state.
947+
const staleWorktree = findWorktreeForBranch(expandedRepoPath, branchName);
948+
if (staleWorktree) {
949+
loopLog(body.loopId, `Removing stale worktree for fresh PLAN: ${staleWorktree}`);
950+
try {
951+
execSync(`git worktree remove --force ${shellEscape(staleWorktree)}`, {
952+
cwd: expandedRepoPath,
953+
stdio: "pipe",
954+
timeout: 15_000,
955+
});
956+
} catch (wtErr) {
957+
loopLog(body.loopId, `git worktree remove failed, falling back to fs.rm: ${wtErr instanceof Error ? wtErr.message : wtErr}`);
958+
// Force-remove the directory so ensureWorktree can recreate it
959+
await fs.rm(staleWorktree, { recursive: true, force: true });
960+
// Prune stale worktree entries from git's tracking
961+
try {
962+
execSync("git worktree prune", { cwd: expandedRepoPath, stdio: "pipe", timeout: 10_000 });
963+
} catch {
964+
// Best-effort
965+
}
966+
}
967+
}
949968
await ensureWorktree(
950969
expandedRepoPath,
951970
worktreeDir,
952971
branchName,
953972
body.repo?.branch ?? "main"
954973
);
955-
loopLog(body.loopId, `Created worktree: ${worktreeDir} (branch: ${branchName})`);
974+
loopLog(body.loopId, `Created fresh worktree for PLAN: ${worktreeDir} (branch: ${branchName})`);
975+
} else {
976+
// EXECUTE/REQUEST_CHANGES: reuse existing worktree.
977+
// Try artifact slug first, then parentLoopId fallback, then create new.
978+
const existingWorktree = findWorktreeForBranch(expandedRepoPath, branchName);
979+
if (existingWorktree) {
980+
worktreeDir = existingWorktree;
981+
loopLog(body.loopId, `Reusing worktree via artifact slug: ${worktreeDir} (branch: ${branchName})`);
982+
} else if (body.parentLoopId) {
983+
// Fallback: try parent's loopId-based branch (pre-slug deployments or missing slug)
984+
const parentBranch = `symphony/loop-${slugifyLoopId(body.parentLoopId)}`;
985+
const parentWorktree = findWorktreeForBranch(expandedRepoPath, parentBranch);
986+
if (parentWorktree) {
987+
worktreeDir = parentWorktree;
988+
loopLog(body.loopId, `Reusing worktree via parentLoopId fallback: ${worktreeDir} (branch: ${parentBranch})`);
989+
}
990+
}
991+
if (!worktreeDir || !existsSync(worktreeDir)) {
992+
// No existing worktree found — create new
993+
worktreeDir = resolveLoopWorktreeDir(expandedRepoPath, worktreeKey);
994+
await ensureWorktree(
995+
expandedRepoPath,
996+
worktreeDir,
997+
branchName,
998+
body.repo?.branch ?? "main"
999+
);
1000+
loopLog(body.loopId, `Created new worktree: ${worktreeDir} (branch: ${branchName})`);
1001+
}
9561002
}
9571003

9581004
try {

0 commit comments

Comments
 (0)