gendesign/scripts/cleanup-merged-worktrees.sh

116 lines
5.6 KiB
Bash
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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"