@@ -589,11 +589,21 @@ public async Task<RepositoryInfo> AddRepositoryFromLocalAsync(
589589 var id = RepoIdFromUrl ( url ) ;
590590
591591 RepositoryInfo repo ;
592+ string ? oldBareClonePath = null ;
592593 lock ( _stateLock )
593594 {
594595 var existing = _state . Repositories . FirstOrDefault ( r => r . Id == id ) ;
595596 if ( existing != null )
596597 {
598+ // If the old BareClonePath was a managed bare clone, remember it for cleanup.
599+ if ( ! string . IsNullOrWhiteSpace ( existing . BareClonePath )
600+ && ! PathsEqual ( existing . BareClonePath , localPath ) )
601+ {
602+ var fullOld = Path . GetFullPath ( existing . BareClonePath ) ;
603+ var managedPrefix = Path . GetFullPath ( ReposDir ) + Path . DirectorySeparatorChar ;
604+ if ( fullOld . StartsWith ( managedPrefix , StringComparison . OrdinalIgnoreCase ) )
605+ oldBareClonePath = existing . BareClonePath ;
606+ }
597607 existing . BareClonePath = localPath ;
598608 BackfillWorktreeClonePaths ( existing ) ;
599609 repo = existing ;
@@ -614,6 +624,13 @@ public async Task<RepositoryInfo> AddRepositoryFromLocalAsync(
614624 Save ( ) ;
615625 OnStateChanged ? . Invoke ( ) ;
616626
627+ // Clean up orphaned managed bare clone (if any) after state is saved.
628+ if ( oldBareClonePath != null && Directory . Exists ( oldBareClonePath ) )
629+ {
630+ try { Directory . Delete ( oldBareClonePath , recursive : true ) ; }
631+ catch ( Exception ex ) { Console . WriteLine ( $ "[RepoManager] Failed to clean up old bare clone at '{ oldBareClonePath } ': { ex . Message } ") ; }
632+ }
633+
617634 // Register the local folder as an external worktree so it also appears in the
618635 // "📂 Existing" picker when creating repo-based sessions.
619636 await RegisterExternalWorktreeAsync ( repo , localPath , ct ) ;
@@ -706,16 +723,18 @@ public virtual async Task<WorktreeInfo> CreateWorktreeAsync(string repoId, strin
706723 var repo = _state . Repositories . FirstOrDefault ( r => r . Id == repoId )
707724 ?? throw new InvalidOperationException ( $ "Repository '{ repoId } ' not found.") ;
708725
709- // Check if an existing registered worktree for this repo is already on the requested branch.
710- // This handles the common case where the user added their repo via "Existing Folder" and
711- // wants to create a session on the same branch — no need to create a duplicate worktree .
726+ // Check if an existing PolyPilot-managed worktree for this repo is already on the requested branch.
727+ // Only reuse worktrees under the centralized WorktreesDir — never return the user's own
728+ // checkout (registered as an external worktree) to avoid multiple sessions sharing it .
712729 WorktreeInfo ? existingMatch ;
730+ var managedWorktreePrefix = Path . GetFullPath ( WorktreesDir ) + Path . DirectorySeparatorChar ;
713731 lock ( _stateLock )
714732 {
715733 existingMatch = _state . Worktrees . FirstOrDefault ( w =>
716734 w . RepoId == repoId
717735 && string . Equals ( w . Branch , branchName , StringComparison . OrdinalIgnoreCase )
718736 && ! string . IsNullOrWhiteSpace ( w . Path )
737+ && Path . GetFullPath ( w . Path ) . StartsWith ( managedWorktreePrefix , StringComparison . OrdinalIgnoreCase )
719738 && Directory . Exists ( w . Path ) ) ;
720739 }
721740 if ( existingMatch != null )
@@ -1091,7 +1110,15 @@ public async Task RemoveRepositoryAsync(string repoId, bool deleteFromDisk, Canc
10911110
10921111 if ( deleteFromDisk && Directory . Exists ( repo . BareClonePath ) )
10931112 {
1094- try { Directory . Delete ( repo . BareClonePath , recursive : true ) ; } catch { }
1113+ // Only delete if BareClonePath is under the managed ReposDir.
1114+ // Repos added via "Existing Folder" have BareClonePath pointing at the user's
1115+ // real project directory — we must NEVER delete that.
1116+ var fullClonePath = Path . GetFullPath ( repo . BareClonePath ) ;
1117+ var managedPrefix = Path . GetFullPath ( ReposDir ) + Path . DirectorySeparatorChar ;
1118+ if ( fullClonePath . StartsWith ( managedPrefix , StringComparison . OrdinalIgnoreCase ) )
1119+ {
1120+ try { Directory . Delete ( repo . BareClonePath , recursive : true ) ; } catch { }
1121+ }
10951122 }
10961123
10971124 OnStateChanged ? . Invoke ( ) ;
@@ -1154,7 +1181,20 @@ private async Task<string> GetDefaultBranch(string barePath, CancellationToken c
11541181 {
11551182 try
11561183 {
1157- // Get the default branch name (e.g. "main")
1184+ // Prefer origin/HEAD which points at the canonical default branch regardless
1185+ // of which branch is currently checked out (important for non-bare repos).
1186+ try
1187+ {
1188+ var originHead = ( await RunGitAsync ( barePath , ct , "rev-parse" , "--abbrev-ref" , "origin/HEAD" ) ) . Trim ( ) ;
1189+ if ( ! string . IsNullOrWhiteSpace ( originHead ) && originHead != "origin/HEAD" )
1190+ {
1191+ Console . WriteLine ( $ "[RepoManager] Using origin/HEAD: { originHead } ") ;
1192+ return $ "refs/remotes/{ originHead } ";
1193+ }
1194+ }
1195+ catch { /* origin/HEAD not set — fall through */ }
1196+
1197+ // Fallback: use symbolic-ref HEAD (correct for bare repos, may be wrong for non-bare)
11581198 var headRef = await RunGitAsync ( barePath , ct , "symbolic-ref" , "HEAD" ) ;
11591199 var branchName = headRef . Trim ( ) . Replace ( "refs/heads/" , "" ) ;
11601200
@@ -1208,7 +1248,9 @@ private static async Task<string> RunGhAsync(string? workDir, CancellationToken
12081248 if ( workDir != null )
12091249 {
12101250 psi . WorkingDirectory = workDir ;
1211- // Bare repos need GIT_DIR set explicitly for gh to find the remote
1251+ // Bare repos (paths ending in .git) need GIT_DIR set explicitly for gh
1252+ // to find the remote. Non-bare repos (including those added via "Existing Folder")
1253+ // don't need this — gh discovers the remote from the working directory.
12121254 if ( workDir . EndsWith ( ".git" , StringComparison . OrdinalIgnoreCase ) )
12131255 psi . Environment [ "GIT_DIR" ] = workDir ;
12141256 }
0 commit comments