gendesign/scripts/cleanup-merged-worktrees.sh
bot-backend b03a12204a
All checks were successful
CI / changes (pull_request) Successful in 8s
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
fix(devops): cleanup-merged-worktrees.sh — 3 bugs found live-diagnosing 60+ stale worktrees
1. remote-alias хардкод 'forgejo' — этот чекаут использует 'origin', скрипт
   падал на первой же команде. Автодетект вместо хардкода.
2. merge-check через 'git merge-base --is-ancestor branch main' структурно
   несовместим со squash-merge конвенцией репо (git-pr.md): squash создаёт
   НОВЫЙ коммит в main, оригинальный коммит ветки никогда не становится
   предком — ancestor-check ложно-отрицателен на КАЖДОЙ смерженной ветке.
   Заменено на надёжный прокси: ветка отсутствует на remote (конвенция репо
   — всегда delete_branch:true при мерже).
3. 'git worktree remove --force' (одинарный) недостаточен для worktree,
   залоченного супервизором Claude Code — нужен двойной '-f -f' (force
   применяется дважды: снятие lock + подтверждение удаления).

Плюс dirty-gate: worktree с незакоммиченными TRACKED-изменениями (не просто
untracked-мусором вроде node_modules) теперь пропускается с явным SKIP —
не удаляется вслепую, если там могла остаться нерасхваченная работа.

Найдено вживую при диагностике: 69 worktree накопилось, 21 безопасны
(branch снят с remote + чистое дерево), 30 dirty (требуют разбора вручную),
13 всё ещё в работе (branch жив на remote). Само удаление НЕ проведено этим
коммитом — попытка force-remove из текущей сессии не сработала (см. комментарий
в скрипте про harness-managed worktree).
2026-07-04 17:58:38 +05:00

116 lines
5.6 KiB
Bash
Executable file
Raw 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"