#!/usr/bin/env bash # worktree-cleanup.sh — Reduce git worktrees from 421+ to <20 # Issue: timmy-home #507 # # Removes stale agent worktrees from ~/worktrees/ and .claude/worktrees/. # # Usage: # ./worktree-cleanup.sh [--dry-run] [--execute] # Default is --dry-run. set -euo pipefail DRY_RUN=true REPORT_FILE="worktree-cleanup-report.md" RECENT_HOURS=48 while [[ $# -gt 0 ]]; do case "$1" in --dry-run) DRY_RUN=true; shift ;; --execute) DRY_RUN=false; shift ;; -h|--help) echo "Usage: $0 [--dry-run|--execute]"; exit 0 ;; *) echo "Unknown: $1"; exit 1 ;; esac done log() { echo "$(date '+%H:%M:%S') $*"; } REMOVED=0 KEPT=0 FAILED=0 # Known stale agent patterns — always safe to remove STALE_PATTERNS="claude-|claw-code-|gemini-|kimi-|grok-|groq-|claude-base-" # Recent/important named worktrees to KEEP (created today or active) KEEP_NAMES="nexus-focus the-nexus the-nexus-1336-1338 the-nexus-1351 timmy-config-434-ssh-trust timmy-config-435-self-healing timmy-config-pr418" is_stale_pattern() { local name="$1" echo "$name" | grep -qE "^($STALE_PATTERNS)" } is_keeper() { local name="$1" for k in $KEEP_NAMES; do [[ "$name" == "$k" ]] && return 0 done return 1 } dir_age_hours() { local dir="$1" local mod mod=$(stat -f '%m' "$dir" 2>/dev/null) if [[ -z "$mod" ]]; then echo 999999 return fi echo $(( ($(date +%s) - mod) / 3600 )) } do_remove() { local dir="$1" local reason="$2" if $DRY_RUN; then log " WOULD REMOVE: $dir ($reason)" REMOVED=$((REMOVED + 1)) else if rm -rf "$dir" 2>/dev/null; then log " REMOVED: $dir ($reason)" REMOVED=$((REMOVED + 1)) else log " FAILED: $dir" FAILED=$((FAILED + 1)) fi fi } # ============================================ log "==========================================" log "Worktree Cleanup — Issue #507" log "Mode: $(if $DRY_RUN; then echo 'DRY RUN'; else echo 'EXECUTE'; fi)" log "==========================================" # === 1. ~/worktrees/ — the main cleanup === log "" log "--- ~/worktrees/ ---" if [[ -d "/Users/apayne/worktrees" ]]; then for dir in /Users/apayne/worktrees/*/; do [[ ! -d "$dir" ]] && continue name=$(basename "$dir") # Stale agent patterns → always remove if is_stale_pattern "$name"; then do_remove "$dir" "stale agent" continue fi # Named keepers → always keep if is_keeper "$name"; then log " KEEP (active): $dir" KEPT=$((KEPT + 1)) continue fi # Other named → keep if recent (<48h), remove if old age=$(dir_age_hours "$dir") if [[ "$age" -lt "$RECENT_HOURS" ]]; then log " KEEP (recent ${age}h): $dir" KEPT=$((KEPT + 1)) else do_remove "$dir" "old named, idle ${age}h" fi done fi # === 2. .claude/worktrees/ inside repos === log "" log "--- .claude/worktrees/ inside repos ---" for wt_dir in /Users/apayne/fleet-ops/.claude/worktrees \ /Users/apayne/Luna/.claude/worktrees; do [[ ! -d "$wt_dir" ]] && continue for dir in "$wt_dir"/*/; do [[ ! -d "$dir" ]] && continue do_remove "$dir" "claude worktree" done done # === 3. Prune orphaned git worktree references === log "" log "--- Git worktree prune ---" if ! $DRY_RUN; then find /Users/apayne -maxdepth 4 -name ".git" -type d \ -not -path "*/node_modules/*" 2>/dev/null | while read gitdir; do repo="${gitdir%/.git}" cd "$repo" 2>/dev/null && git worktree prune 2>/dev/null || true done log " Pruned all repos" else log " (skipped in dry-run)" fi # === RESULTS === log "" log "==========================================" log "RESULTS" log "==========================================" label=$(if $DRY_RUN; then echo "Would remove"; else echo "Removed"; fi) log "$label: $REMOVED" log "Kept: $KEPT" log "Failed: $FAILED" log "" # Generate report cat > "$REPORT_FILE" <48h idle) **.claude/worktrees/**: - fleet-ops: 5 Claude Code worktrees - Luna: 1 Claude Code worktree ## What was kept - Worktrees modified within 48h - Active named worktrees (nexus-focus, the-nexus-*, recent timmy-config-*) ## To execute \`\`\`bash ./scripts/worktree-cleanup.sh --execute \`\`\` REPORT log "Report: $REPORT_FILE" if $DRY_RUN; then log "" log "Dry run. To execute: ./scripts/worktree-cleanup.sh --execute" fi