# retro_leftovers.py: хвосты чеклиста закрытия по состоянию репозиториев (спека harness-retro, #29). L="$ROOT/docs/retro/scripts/retro_leftovers.py" G="$TMP/lo"; mkdir -p "$G" git init -q --bare -b main "$G/o.git" git clone -q "$G/o.git" "$G/c" 2>/dev/null; git -C "$G/c" symbolic-ref HEAD refs/heads/main printf '.superpowers/\n.claude/worktrees/\n' > "$G/c/.gitignore" git -C "$G/c" add .gitignore && git -C "$G/c" commit -q -m init && git -C "$G/c" push -q origin main 2>/dev/null git -C "$G/c" switch -q -c feat/1 && git -C "$G/c" commit -q --allow-empty -m f1 && git -C "$G/c" push -q -u origin feat/1 2>/dev/null git -C "$G/c" switch -q main && git -C "$G/c" merge -q --no-ff feat/1 -m "merge feat/1" mkdir -p "$G/c/openspec/changes/foo" "$G/c/openspec/changes/archive/2026-01-01-bar" echo x > "$G/c/openspec/changes/foo/proposal.md"; echo y > "$G/c/openspec/changes/archive/2026-01-01-bar/p.md" git -C "$G/c" add openspec && git -C "$G/c" commit -q -m spec && git -C "$G/c" push -q origin main 2>/dev/null git -C "$G/c" worktree add -q "$G/c/.claude/worktrees/w1" feat/1 mkdir -p "$G/c/.superpowers/sdd/plan-x" && touch "$G/c/.superpowers/sdd/plan-x/progress.md" # активная задача: worktree на неслитой ветке со своим SDD workspace — не хвост git -C "$G/c" worktree add -q "$G/c/.claude/worktrees/w2" -b feat/2 && git -C "$G/c/.claude/worktrees/w2" commit -q --allow-empty -m wip mkdir -p "$G/c/.claude/worktrees/w2/.superpowers/sdd/plan-y" && touch "$G/c/.claude/worktrees/w2/.superpowers/sdd/plan-y/progress.md" git clone -q "$G/o.git" "$G/c2" 2>/dev/null && git -C "$G/c2" commit -q --allow-empty -m ahead && git -C "$G/c2" push -q origin main 2>/dev/null git -C "$G/c" fetch -q origin echo x > "$G/c/untracked.txt" J="$TMP/lo.json" python3 "$L" "$G/c" > "$J"; rc=$? assert_eq "хвосты: exit 0" "$rc" "0" k(){ python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(eval(sys.argv[2]))' "$J" "$1"; } assert_eq "виды хвостов" "$(k 'sorted(d["totals"])')" "['local-branch', 'main-behind', 'main-dirty', 'openspec-change', 'remote-branch', 'sdd-workspace', 'worktree']" assert_eq "ветка на сервере и подсказка" "$(k '[(i["item"], i["hint"]) for i in d["items"] if i["kind"] == "remote-branch"]')" "[('origin/feat/1', 'git push origin --delete feat/1')]" assert_eq "локальная ветка" "$(k '[i["item"] for i in d["items"] if i["kind"] == "local-branch"]')" "['feat/1']" assert_eq "worktree на слитой ветке" "$(k '[i["item"].endswith("/.claude/worktrees/w1") for i in d["items"] if i["kind"] == "worktree"]')" "[True]" assert_eq "SDD workspace" "$(k '[i["item"].endswith("/sdd/plan-x") for i in d["items"] if i["kind"] == "sdd-workspace"]')" "[True]" assert_eq "неархивный change — без archive" "$(k '[i["item"] for i in d["items"] if i["kind"] == "openspec-change"]')" "['openspec/changes/foo']" assert_grep "отставание от origin/main" 'позади origin/main на 1' "$J" assert_grep "грязное дерево с путём" 'untracked.txt' "$J" # только чтение: без --fetch ссылки и дерево не меняются git -C "$G/c2" commit -q --allow-empty -m ahead2 && git -C "$G/c2" push -q origin main 2>/dev/null before="$(git -C "$G/c" for-each-ref)$(git -C "$G/c" status --porcelain)" python3 "$L" "$G/c" > /dev/null assert_eq "без --fetch ничего не меняется" "$(git -C "$G/c" for-each-ref)$(git -C "$G/c" status --porcelain)" "$before" python3 "$L" "$G/c" --fetch > "$J" assert_grep "--fetch видит новые коммиты" 'позади origin/main на 2' "$J" md=$(python3 "$L" "$G/c" --markdown) assert_grep "таблица для черновика" '^\| remote-branch \| c \| `origin/feat/1` \| `git push origin --delete feat/1` \|$' <(printf '%s' "$md") # чистый репозиторий: свежая ветка на вершине origin/main — не хвост git init -q --bare -b main "$G/k.git"; git clone -q "$G/k.git" "$G/k" 2>/dev/null; git -C "$G/k" symbolic-ref HEAD refs/heads/main git -C "$G/k" commit -q --allow-empty -m init && git -C "$G/k" push -q origin main 2>/dev/null && git -C "$G/k" branch feat/new python3 "$L" "$G/k" > "$J" assert_eq "чистый репозиторий — хвостов нет" "$(k 'd["items"]')" "[]" assert_eq "чистый репозиторий — markdown" "$(python3 "$L" "$G/k" --markdown)" "Хвостов закрытия нет." # без origin git init -q -b main "$G/n" && git -C "$G/n" commit -q --allow-empty -m init python3 "$L" "$G/n" > "$J"; rc=$? assert_eq "без origin: exit 0 и no-origin-main" "$rc $(k '[i["kind"] for i in d["items"]]')" "0 ['no-origin-main']" # workspace: репозитории из таблицы CLAUDE.md W="$TMP/ws"; mkdir -p "$W"; git clone -q "$G/o.git" "$W/r" 2>/dev/null printf '# w\n\n| Каталог | Репозиторий | Роль |\n|---|---|---|\n| r/ | o/r | dev |\n| - | x | qa |\n' > "$W/CLAUDE.md" python3 "$L" "$W" > "$J" assert_eq "workspace: репозитории из таблицы" "$(k '[r.endswith("/ws/r") for r in d["repos"]]')" "[True]" assert_eq "workspace: ветка на сервере видна" "$(k '[i["item"] for i in d["items"] if i["kind"] == "remote-branch"]')" "['origin/feat/1']" # не проект и не workspace mkdir -p "$TMP/empty"; python3 "$L" "$TMP/empty" > /dev/null 2>&1; rc=$? assert_eq "не репозиторий и не workspace: exit 1" "$rc" "1"