#!/bin/bash
# Cleanup merged Claude Code background-session worktrees.
#
# `claude agents` / `claude --bg` создают `.claude/worktrees/agent-*` для
# каждой background session. После merge PR worktree остаётся locked
# и не удаляется автоматически — за неделю накапливаются десятки.
#
# Скрипт определяет, что worktree безопасно удалить, ЕСЛИ ОБА условия:
#   1. branch отсутствует на remote (см. ниже, почему не ancestor-check)
#   2. working tree чист (`git status --porcelain` пуст — untracked-only
#      мусор типа node_modules/__pycache__ игнорируется, но staged/modified
#      TRACKED-изменения блокируют удаление — там может быть незакоммиченная
#      работа, которую стоит разобрать руками)
# Затем `git worktree prune` чистит references.
#
# ПОЧЕМУ НЕ `git merge-base --is-ancestor branch main` (ревизия 2026-07-04):
# репозиторий мержит PR через SQUASH (см. git-pr.md § Auto-merge policy) —
# squash создаёт НОВЫЙ коммит в main, оригинальный коммит ветки НИКОГДА не
# становится предком main, даже если PR давно смержен. Ancestor-check
# систематически давал ложный "не смержено" на каждой squash-смерженной
# ветке — это и есть причина, по которой скрипт годами не расчищал
# 60+ забытых worktree. Прокси "ветка удалена на remote" надёжен, потому что
# конвенция репо — всегда `delete_branch: true` при мерже (git-pr.md).
#
# ВАЖНО (2026-07-04): удаление worktree, управляемого супервизором Claude
# Code, из ОБЫЧНОЙ сессионной Bash-песочницы систематически не срабатывает
# (`git worktree remove -f -f` тихо не удаляет каталог, даже когда
# ответственный agent-pid уже мёртв) — похоже на намеренную защиту
# harness-managed состояния. Запускай этот скрипт ЛИБО из терминала вне
# активной Claude Code сессии, ЛИБО через `Ctrl+X` дважды в agent view
# для конкретной сессии (см. CLAUDE.md § Multi-session workflow).
#
# Запускать вручную или через cron (weekly recommended):
#   0 9 * * MON cd /path/to/gendesign && ./scripts/cleanup-merged-worktrees.sh
#
# Safe:
# - Не трогает branches, ещё живые на remote (in-progress work, PR ещё не merged)
# - Не трогает worktree с незакоммиченными TRACKED-изменениями (dirty tree)
# - Логи action — каждый удалённый branch выводится отдельной строкой
#
# Reference: https://code.claude.com/docs/en/worktrees#clean-up-worktrees

set -e

REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$REPO_ROOT"

# Remote-алиас непереносим между чекаутами: где-то `forgejo`, где-то `origin`
# (см. git-pr.md — конвенция документирует `forgejo`, но не все клоны его
# заводят). Автоопределяем вместо жёсткого хардкода.
if git remote | grep -qx forgejo; then
  REMOTE=forgejo
else
  REMOTE=origin
fi

echo "Fetching $REMOTE (remote alias auto-detected)..."
git fetch "$REMOTE" --quiet

BEFORE=$(git worktree list | wc -l)
SIZE_BEFORE=$(du -sh .claude/worktrees/ 2>/dev/null | awk '{print $1}')

echo "Scanning $((BEFORE - 1)) worktrees (skip main)..."

REMOVED=0
KEPT=0
DIRTY=0

# Parse `git worktree list --porcelain` into "path branch" pairs.
git worktree list --porcelain | awk '/^worktree/{w=$2} /^branch/{b=$2} /^$/{print w" "b}' | \
while read path branch; do
  # Skip main worktree (no branch field) and detached HEADs.
  [ -z "$branch" ] && continue

  short="${branch#refs/heads/}"

  # Don't touch the worktree we're standing in.
  if [ "$path" = "$REPO_ROOT" ]; then
    continue
  fi

  # "Merged" proxy: branch no longer exists on remote (squash-merge convention
  # means ancestor-check would false-negative on every merged branch — see
  # header comment). A branch a human/bot deliberately deleted without
  # merging is rare and the local copy is stale garbage either way.
  if git ls-remote --exit-code --heads "$REMOTE" "$short" >/dev/null 2>&1; then
    KEPT=$((KEPT + 1))
    continue
  fi

  # Dirty gate: only untracked noise (node_modules, __pycache__, build
  # artifacts) is tolerated. Any staged/modified TRACKED file blocks
  # removal — that may be unlanded work worth rescuing by hand.
  if [ -n "$(git -C "$path" status --porcelain | grep -v '^??')" ]; then
    echo "  SKIP (uncommitted tracked changes): $short ($path)"
    DIRTY=$((DIRTY + 1))
    continue
  fi

  if git worktree remove -f -f "$path" 2>/dev/null; then
    echo "  removed: $short"
    REMOVED=$((REMOVED + 1))
  else
    echo "  failed:  $short ($path) — see header comment re: harness-managed removal"
  fi
done

git worktree prune

AFTER=$(git worktree list | wc -l)
SIZE_AFTER=$(du -sh .claude/worktrees/ 2>/dev/null | awk '{print $1}')

echo ""
echo "Before: $((BEFORE - 1)) worktrees, $SIZE_BEFORE"
echo "After:  $((AFTER - 1)) worktrees, $SIZE_AFTER"
echo "Dirty (skipped, needs manual review): $DIRTY"
