@@ -360,6 +360,9 @@ struct Launch: AsyncParsableCommand {
360360 private func ensureWorktreeExists( at path: String, branch: String, hasPRBranch: Bool, dryRun: Bool) async throws -> Bool {
361361 let gitService = GitService ( )
362362
363+ // Prune stale worktree references before checking
364+ try await gitService. pruneWorktrees ( )
365+
363366 // Check if worktree already exists at this path
364367 let existingWorktrees = try await gitService. listWorktrees ( )
365368 if existingWorktrees. contains ( where: { $0. path == path } ) {
@@ -395,30 +398,26 @@ struct Launch: AsyncParsableCommand {
395398 }
396399 try await gitService. fetchBranch ( branch)
397400
398- // Create worktree from existing branch
399- do {
400- try await gitService. createWorktreeFromExisting ( at: path, branch: branch)
401- if verbose {
402- print ( " Created worktree from existing branch: \( branch) " )
403- }
404- return false
405- } catch {
406- throw GitError . worktreeCreationFailed ( " Failed to create worktree from PR branch ' \( branch) ': \( error) " )
407- }
401+ // Create worktree from existing branch (with recovery for branch conflicts)
402+ try await createWorktreeWithRecovery (
403+ gitService: gitService,
404+ at: path,
405+ branch: branch,
406+ isPR: true
407+ )
408+ return false
408409 }
409410
410411 // For non-PR sources, try to create a new branch
411412 let branchExists = try await gitService. branchExists ( branch)
412413 if branchExists {
413- // Branch exists - create worktree from existing branch
414- do {
415- try await gitService. createWorktreeFromExisting ( at: path, branch: branch)
416- if verbose {
417- print ( " Created worktree from existing branch: \( branch) " )
418- }
419- } catch {
420- throw GitError . worktreeCreationFailed ( " Branch ' \( branch) ' exists but worktree creation failed: \( error) " )
421- }
414+ // Branch exists - create worktree from existing branch (with recovery)
415+ try await createWorktreeWithRecovery (
416+ gitService: gitService,
417+ at: path,
418+ branch: branch,
419+ isPR: false
420+ )
422421 } else {
423422 // Create new branch with worktree
424423 do {
@@ -434,6 +433,68 @@ struct Launch: AsyncParsableCommand {
434433 return false
435434 }
436435
436+ /// Create a worktree from an existing branch, with automatic recovery if the branch
437+ /// is already checked out in another worktree (stale or active).
438+ private func createWorktreeWithRecovery(
439+ gitService: GitService,
440+ at path: String,
441+ branch: String,
442+ isPR: Bool
443+ ) async throws {
444+ do {
445+ try await gitService. createWorktreeFromExisting ( at: path, branch: branch)
446+ if verbose {
447+ print ( " Created worktree from existing branch: \( branch) " )
448+ }
449+ } catch {
450+ let errorMessage = String ( describing: error)
451+ guard errorMessage. contains ( " is already used by worktree " ) else {
452+ let context = isPR ? " PR branch " : " Branch "
453+ throw GitError . worktreeCreationFailed ( " \( context) ' \( branch) ' worktree creation failed: \( error) " )
454+ }
455+
456+ // Branch is locked by another worktree - attempt recovery
457+ let conflictingPath = parseConflictingWorktreePath ( from: errorMessage)
458+ let conflictingExists = conflictingPath. map { FileManager . default. fileExists ( atPath: $0) } ?? false
459+
460+ if !conflictingExists {
461+ // The conflicting worktree no longer exists on disk - prune stale reference and retry
462+ if verbose {
463+ let displayPath = conflictingPath ?? " unknown path "
464+ print ( " Conflicting worktree at \( displayPath) no longer exists on disk, pruning... " )
465+ }
466+ try await gitService. pruneWorktrees ( )
467+
468+ // Re-fetch the branch now that it's no longer "checked out"
469+ if isPR {
470+ try await gitService. fetchBranch ( branch)
471+ }
472+
473+ try await gitService. createWorktreeFromExisting ( at: path, branch: branch)
474+ if verbose {
475+ print ( " Created worktree from existing branch after pruning: \( branch) " )
476+ }
477+ } else {
478+ // Branch is genuinely checked out in another active worktree - force create
479+ if verbose {
480+ print ( " Branch ' \( branch) ' is checked out at \( conflictingPath!) , force-creating worktree... " )
481+ }
482+ try await gitService. createWorktreeFromExistingForce ( at: path, branch: branch)
483+ if verbose {
484+ print ( " Created worktree (forced) from existing branch: \( branch) " )
485+ }
486+ }
487+ }
488+ }
489+
490+ /// Parse the conflicting worktree path from a git "already used by worktree" error message.
491+ private func parseConflictingWorktreePath( from errorMessage: String) - > String? {
492+ if let match = errorMessage. firstMatch ( of: /is already used by worktree at '([^']+)'/ ) {
493+ return String ( match. 1 )
494+ }
495+ return nil
496+ }
497+
437498 // MARK: - Script Execution
438499
439500 private func executeScript( _ script: String) async throws {
0 commit comments