@@ -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