From 4a2731a0009f7c38521150f949e77af57283203e Mon Sep 17 00:00:00 2001 From: No9 Labs Date: Thu, 9 Apr 2026 02:11:20 -0400 Subject: [PATCH] fix: proactively remove .claude/worktrees/agent-* after every session The previous cleanup_worktrees only removed worktrees marked 'prunable' by git, but agent-* directories left by completed or crashed sessions are never prunable (their directories still exist). Added Pass 1 that enumerates all .claude/worktrees/agent-* entries via git worktree list --porcelain and force-removes each, skipping the current executing worktree as a safety guard. git worktree prune runs last to clear any orphaned git metadata. This runs in both housekeeping (start of cycle, catches crash remnants) and post-session cleanup (end of cycle). --- .recursive/engine/lib-agent.sh | 39 +++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/.recursive/engine/lib-agent.sh b/.recursive/engine/lib-agent.sh index e26b6b8..81b4eec 100644 --- a/.recursive/engine/lib-agent.sh +++ b/.recursive/engine/lib-agent.sh @@ -626,23 +626,52 @@ PY # cleanup_worktrees # Prunes stale git worktrees left by sub-agent sessions. -# Removes worktrees marked 'prunable' by git. +# Removes ALL .claude/worktrees/agent-* worktrees (active sub-agent dirs), +# plus any worktrees marked 'prunable' by git, then runs git worktree prune. +# Safe to call from the daemon main loop: the daemon runs in REPO_DIR, not +# inside an agent-* worktree, so no currently-executing agent is skipped. +# If called from inside an agent worktree (e.g. evolve), the current dir is +# detected and skipped to avoid self-removal. cleanup_worktrees() { - git -C "$REPO_DIR" worktree prune 2>/dev/null || true local count=0 + local current_wt + current_wt="$(git -C "$REPO_DIR" rev-parse --show-toplevel 2>/dev/null || echo "")" + + # Pass 1: remove ALL .claude/worktrees/agent-* worktrees by path. + # Uses porcelain format to get one path per stanza reliably. + while IFS= read -r wt_path; do + # Skip empty lines + [ -z "$wt_path" ] && continue + # Skip the main worktree + [ "$wt_path" = "$REPO_DIR" ] && continue + # Skip the worktree we are currently executing inside (safety guard) + [ "$wt_path" = "$current_wt" ] && continue + # Only target agent worktrees in .claude/worktrees/ + case "$wt_path" in + */.claude/worktrees/agent-*) + git -C "$REPO_DIR" worktree remove "$wt_path" --force 2>/dev/null || true + count=$((count + 1)) + ;; + esac + done < <(git -C "$REPO_DIR" worktree list --porcelain 2>/dev/null | grep "^worktree " | sed 's/^worktree //') + + # Pass 2: remove any remaining worktrees marked prunable by git. while IFS= read -r wt_line; do local wt_path wt_path=$(echo "$wt_line" | awk '{print $1}') - # Skip the main worktree [ "$wt_path" = "$REPO_DIR" ] && continue - # Remove if marked prunable or is a daemon worktree + [ "$wt_path" = "$current_wt" ] && continue if echo "$wt_line" | grep -q "prunable" 2>/dev/null; then git -C "$REPO_DIR" worktree remove "$wt_path" --force 2>/dev/null || true count=$((count + 1)) fi done < <(git -C "$REPO_DIR" worktree list 2>/dev/null) + + # Prune git metadata for any worktrees whose directories no longer exist. + git -C "$REPO_DIR" worktree prune 2>/dev/null || true + if [ "$count" -gt 0 ]; then - echo " Cleaned up $count worktree(s)" + echo " Cleaned up $count agent worktree(s)" fi }