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

Commit a5b6f47

Browse files
authored
fix: use artifact slug for worktree/branch naming (#24)
All repo-based commands (PLAN, EXECUTE, REQUEST_CHANGES) now share a single worktree keyed by artifact slug (e.g., symphony/PLAN-5) instead of per-loop UUIDs (symphony/loop-019cfa96-...). This means PLAN creates the worktree, and subsequent REQUEST_CHANGES and EXECUTE loops automatically reuse it — no parent chain traversal needed. Branches are now human-readable.
1 parent f99819e commit a5b6f47

1 file changed

Lines changed: 36 additions & 50 deletions

File tree

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

Lines changed: 36 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ interface LoopRequestBody {
4848
artifacts: LoopArtifact[];
4949
repo?: LoopRepo;
5050
committer?: LoopCommitter;
51+
artifactSlug?: string;
5152
parentLoopId?: string;
5253
parentBranchName?: string;
5354
parentSessionId?: string;
@@ -926,67 +927,52 @@ async function handleLoopRequest(
926927
error: "Repository required for PLAN, EXECUTE, and REQUEST_CHANGES commands",
927928
});
928929
return;
929-
} else if (body.command === "PLAN") {
930-
// PLAN: always create a fresh worktree (matches ECS harness which always clones fresh).
931-
// No reuse — each PLAN loop gets its own worktree keyed by loopId.
932-
const loopBranch = `symphony/loop-${pickStableId(body)}`;
933-
worktreeDir = resolveLoopWorktreeDir(expandedRepoPath, pickStableId(body));
934-
await ensureWorktree(
935-
expandedRepoPath,
936-
worktreeDir,
937-
loopBranch,
938-
body.repo?.branch ?? "main"
939-
);
940-
loopLog(body.loopId, `Created loop worktree: ${worktreeDir}`);
941-
claudeWorkDir = path.join(worktreeDir, ".claude", "work");
942-
await fs.mkdir(claudeWorkDir, { recursive: true });
943-
await writeArtifactsForPlan(claudeWorkDir, body.artifacts, body.prompt);
944-
} else if (body.command === "EXECUTE" || body.command === "REQUEST_CHANGES") {
945-
// EXECUTE/REQUEST_CHANGES: reuse parent's worktree.
946-
// Derive the parent's worktree path from parentLoopId (deterministic naming),
947-
// falling back to parentBranchName for backwards compat.
948-
const parentStableId = body.parentLoopId ? slugifyLoopId(body.parentLoopId) : null;
949-
if (parentStableId) {
950-
const parentBranch = `symphony/loop-${parentStableId}`;
951-
worktreeDir = findWorktreeForBranch(expandedRepoPath, parentBranch);
952-
if (worktreeDir) {
953-
loopLog(body.loopId, `Reusing parent worktree via parentLoopId: ${worktreeDir}`);
954-
}
955-
}
956-
if (!worktreeDir && body.parentBranchName) {
957-
worktreeDir = findWorktreeForBranch(expandedRepoPath, body.parentBranchName);
958-
if (worktreeDir) {
959-
loopLog(body.loopId, `Reusing parent worktree via parentBranchName: ${worktreeDir}`);
960-
}
961-
}
962-
if (worktreeDir) {
963-
try {
964-
assertPathAllowed(worktreeDir, allowedDirs);
965-
} catch (e) {
966-
if (e instanceof DirectoryNotAllowedError) {
967-
json(context, 403, { error: `Worktree path not allowed: ${worktreeDir}` });
968-
return;
969-
}
970-
throw e;
971-
}
972-
}
973-
if (!worktreeDir) {
974-
// No parent worktree found — create a new one
975-
const loopBranch = `symphony/loop-${pickStableId(body)}`;
976-
worktreeDir = resolveLoopWorktreeDir(expandedRepoPath, pickStableId(body));
930+
} 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}`
939+
: `symphony/loop-${pickStableId(body)}`;
940+
941+
worktreeDir = resolveLoopWorktreeDir(expandedRepoPath, worktreeKey);
942+
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 {
977949
await ensureWorktree(
978950
expandedRepoPath,
979951
worktreeDir,
980-
loopBranch,
952+
branchName,
981953
body.repo?.branch ?? "main"
982954
);
955+
loopLog(body.loopId, `Created worktree: ${worktreeDir} (branch: ${branchName})`);
956+
}
957+
958+
try {
959+
assertPathAllowed(worktreeDir, allowedDirs);
960+
} catch (e) {
961+
if (e instanceof DirectoryNotAllowedError) {
962+
json(context, 403, { error: `Worktree path not allowed: ${worktreeDir}` });
963+
return;
964+
}
965+
throw e;
983966
}
984967
claudeWorkDir = path.join(worktreeDir, ".claude", "work");
985968
await fs.mkdir(claudeWorkDir, { recursive: true });
986969

987-
if (body.command === "EXECUTE") {
970+
if (body.command === "PLAN") {
971+
await writeArtifactsForPlan(claudeWorkDir, body.artifacts, body.prompt);
972+
} else if (body.command === "EXECUTE") {
988973
await writeArtifactsForExecuteOrAmend(claudeWorkDir, body.artifacts);
989974
} else {
975+
// REQUEST_CHANGES
990976
await writeArtifactsForExecuteOrAmend(
991977
claudeWorkDir,
992978
body.artifacts,

0 commit comments

Comments
 (0)