diff --git a/src/cleveragents/cli/commands/plan.py b/src/cleveragents/cli/commands/plan.py index 13c2f78ad..9268bddf6 100644 --- a/src/cleveragents/cli/commands/plan.py +++ b/src/cleveragents/cli/commands/plan.py @@ -1345,6 +1345,70 @@ def _get_lifecycle_service(): return container.plan_lifecycle_service() +def _cleanup_stale_sandbox(repo_path: str, plan_id: str) -> None: + """Remove a stale worktree branch and directory for a plan. + + If a previous execute left behind a worktree branch + ``cleveragents/plan-``, this function removes the worktree + directory and deletes the branch so that a fresh sandbox can be + created. Silently does nothing if no stale branch exists. + """ + import subprocess + + branch_name = f"cleveragents/plan-{plan_id}" + + # Check if the branch exists + check = subprocess.run( + ["git", "rev-parse", "--verify", f"refs/heads/{branch_name}"], + cwd=repo_path, + capture_output=True, + check=False, + timeout=10, + ) + if check.returncode != 0: + return # No stale branch — nothing to clean + + # Find and remove the worktree directory + wt_list = subprocess.run( + ["git", "worktree", "list", "--porcelain"], + cwd=repo_path, + capture_output=True, + text=True, + check=False, + timeout=10, + ) + for wt_block in wt_list.stdout.split("\n\n"): + if f"branch refs/heads/{branch_name}" in wt_block: + for line in wt_block.splitlines(): + if line.startswith("worktree "): + wt_path = line.split("worktree ", 1)[1] + subprocess.run( + ["git", "worktree", "remove", "--force", wt_path], + cwd=repo_path, + capture_output=True, + check=False, + timeout=10, + ) + + # Delete the branch + subprocess.run( + ["git", "branch", "-D", branch_name], + cwd=repo_path, + capture_output=True, + check=False, + timeout=10, + ) + + # Prune stale worktree references + subprocess.run( + ["git", "worktree", "prune"], + cwd=repo_path, + capture_output=True, + check=False, + timeout=10, + ) + + def _create_sandbox_for_plan( plan_id: str, service: PlanLifecycleService, @@ -1387,6 +1451,10 @@ def _create_sandbox_for_plan( and resource.location and os.path.isdir(os.path.join(resource.location, ".git")) ): + _cleanup_stale_sandbox( + resource.location, + plan_id, + ) sandbox = GitWorktreeSandbox( resource_id=resource.resource_id, original_path=resource.location,