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