andrey.epifantsev 4bbf6ebb3a feat(retro): retro_leftovers.py — хвосты чеклиста закрытия по состоянию репозиториев (#29)
SDD workspace в worktree активной задачи хвостом не считается (O-19, живой прогон).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 01:25:14 +04:00

20 KiB
Raw Blame History

Design: /harness-retro

Context

Ретро 11.10.2026 (docs/retro/2026-10-11.md) сделано вручную. Таблицу «Цифры» собрали из вывода retro_stats.py и retro_blocks.py, находки — по прочтению 47 файлов памяти. В скриптах зашиты имена репозиториев awp-ca, вывод — текст, тестов нет. Подсчёт блокировок завышен: в 103 вошли выводы команд со словом BLOCKED, настоящих ошибок хука было 76 (O-1, #6).

После ретро вышли хуки по цели записи (0.4.0, корпус 129 записей), роли (0.3.0), allow-правила (0.5.0) и worktree в .claude/worktrees/ (0.5.1). Что из этого помогло, видно только по сравнимым цифрам.

Владелец решил (brainstorming 24.10.2026):

  • результат — черновик ретро-документа;
  • срез — один проект или workspace;
  • запуск — из репозитория обвязки, проект передаётся аргументом;
  • ложные блокировки размечает агент по спеке, и они идут в корпус;
  • хвосты закрытия ищутся по текущему состоянию репозиториев;
  • поправки владельца по процессу собираются в ретро;
  • подход A — проектный skill и модуль метрик с тестами.

Транскрипты Claude Code: ~/.claude/projects/<slug>/<session>.jsonl, по строке на запись. Типы записей: user, assistant (message.content: text, tool_use; message.usage), system (subtype: compact_boundary), ai-title, permission-mode. У записей есть cwd, gitBranch, timestamp.

Сессия, перешедшая в worktree, пишет транскрипт в каталог со slug пути worktree. Пример — эта сессия: -Users-andrejepifancev-Projects-cc-harness-wt-6-guard-main-target.

Goals / Non-Goals

Goals

  • Повторяемый срез метрик одного проекта в JSON и сравнение с прошлым ретро.
  • Блокировки среза размечаются и становятся регрессионными записями корпуса; известные ошибки хуков видны в doctor.
  • Кандидаты в поправки владельца с контекстом; агент отбирает и группирует.
  • Хвосты чеклиста закрытия по состоянию репозиториев.
  • Черновик ретро по шаблону, утверждение через PR.

Non-Goals

  • Сторона Gitea в хвостах: открытые issue после слитого PR, пропущенные follow-up.
  • Срез по нескольким проектам сразу.
  • Автоматические находки по порогам.
  • Автоматическая очистка хвостов: скрипт только подсказывает команду.
  • Переписывать ретро 11.10.

Decisions

1. Проектный skill, без установки

/harness-retro нужен только в сессии в репозитории обвязки: туда пишется черновик, там корпус и hook_cases_build.py. Поэтому skill проектный (.claude/skills/harness-retro/), как skills openspec. install.sh и ~/.claude не меняются.

.claude/skills/ не входит в .claude/code-paths, поэтому SKILL.md и шаблон — документация. Код лежит в docs/retro/scripts/, как и сейчас.

2. retro_metrics.py: модуль на функциях

Только stdlib, Python 3.8+. CLI:

retro_metrics.py <папка проекта> [--since ГГГГ-ММ-ДД] [--harness <репозиторий обвязки>]
                 [--projects-root <каталог>] [--out <metrics.json>]
                 [--markdown [--prev <прошлый.metrics.json>]] [--corrections]
  • --projects-root по умолчанию — ${CLAUDE_CONFIG_DIR:-~/.claude}/projects; тесты подставляют свой.
  • --harness по умолчанию — репозиторий, в котором лежит скрипт. Из него берутся даты прошлых ретро для --since.

Функции:

  • slug(path);
  • project_sessions(project, root, since) — список транскриптов среза;
  • read_session(path) — генератор записей, битые строки считаются;
  • session_stats(records, code_of) — словарь счётчиков сессии;
  • aggregate(stats) — метрики среза;
  • corrections(records) — кандидаты;
  • to_markdown(metrics, prev);
  • main(argv).

Каждая метрика считается в одном месте, в session_stats, и проверяется отдельным тестом.

3. Сессии проекта

Каталоги-кандидаты — те, чьё имя начинается со slug проекта. В срез сессия входит по первому cwd: путь внутри проекта или внутри <проект>.wt/ (старое место worktree). Префикс slug нужен, чтобы найти каталоги worktree; проверка cwd отсекает чужой проект /p/repo-tools при проекте /p/repo.

4. Что такое «код»

Пути в транскриптах относятся к репозиториям, которых может уже не быть: worktree удалены. Относительный путь считается от корня worktree или репозитория. Корень определяется в таком порядке:

  1. часть пути после /.claude/worktrees/<имя>/;
  2. затем после <репо>.wt/<имя>/;
  3. затем после <workspace>/.wt/<задача>/<репо>/;
  4. затем после ближайшего существующего каталога с .git.

Префиксы кода берутся из .claude/code-paths основного репозитория, без него — правило src/ на любой глубине. Так же решает require-plan.

Bash-команда считается чтением кода, если хотя бы один её простой сегмент (разбиение по &&, ||, ;, |) начинается с grep, rg, find, cat, sed -n или head и хотя бы один аргумент после разбора shlex (при ошибке — split) указывает в каталог кода относительно cwd.

5. Блокировки, отказы, роли

  • Блокировка: результат инструмента совпадает с ^\s*PreToolUse:\S+ hook error и содержит BLOCKED. Хук определяется так же, как в hook_blocks_export.hook_of, функция импортируется.
  • Отказ классификатора: denied by the Claude Code auto mode classifier. Reason: \[([^\]]+)\].
  • Отказ владельца: результат инструмента содержит The user doesn't want to proceed with this tool use. Прерывание ([Request interrupted by user…) считается отдельной метрикой, а не отказом.
  • Ход модели — уникальный message.id ассистента. Claude Code пишет одно сообщение несколькими строками с общим id и одинаковым usage (проверено на транскриптах этой машины: 538 id на 1381 строку). Поэтому токены считаются один раз на id. Строки без id считаются по одной.
  • Сессия — файл <id>.jsonl. Транскрипт переезжает в каталог worktree (запись relocated); если один id встретился в нескольких каталогах, берётся самый большой файл.
  • Роль: первый tool_use с name: Skill и input.skill = role (или …:role), аргумент — input.args.

6. Кандидаты в поправки

Признаки ищутся в первых 200 символах промпта, без учёта регистра и с границами слов. Список признаков — в спеке. Промпт длиннее 2000 символов признаком не проверяется: это вставка материала.

Последнее действие — последний tool_use ассистента перед промптом: Bash: <команда>, Edit|Write|Read: <путь>, <Имя> для остальных. Хвост ответа — последние 200 символов последнего текстового блока ассистента.

Весь вывод проходит через redact() из hook_blocks_export.py.

7. retro_leftovers.py

Репозитории: сам git-проект или каталоги из таблицы workspace (разбор как в doctor.sh). Для каждого:

  • git branch -r --merged origin/main без origin/main и origin/HEAD;
  • git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads, где слитые в origin/main или [gone];
  • git worktree list --porcelain — ветка из слитых или prunable;
  • ls .superpowers/sdd/*/ в основном checkout и в worktree, которые сами хвост (слитая или gone ветка, prunable). SDD workspace в worktree активной задачи — работа в процессе, не хвост;
  • git ls-tree -d --name-only origin/main openspec/changes/ без archive;
  • git rev-list --count HEAD..origin/main и git status --porcelain основного checkout.

Нет origin/main — у репозитория одна строка kind: no-origin-main. --fetch перед проверками делает git fetch --prune origin.

8. Корпус: сегменты и known

Формат hook-cases.jsonl: строка с ключом layout — шапка сегмента {home, layout}, следующие строки — записи этого сегмента. Первый сегмент — пилот 12.10 и живые случаи, без изменений.

hook_corpus.py строит песочницу на каждый сегмент (<tmp>/seg-<n>/home). Поле known:

  • расхождение при known — KNOWN <id>, тест не падает;
  • совпадение при known — KNOWN-FIXED <id>, тест падает.

Итог: корпус хуков: N/M, при известных ошибках — корпус хуков: N/M, известных ошибок K. N — записи без расхождений, считая известные; M — все записи.

hook-labels-<дата>.json:

{"home": "/Users/x", "layout": [...], "cases": {"2026-11-01-001": {"expected": "allow", "why": "devnull"},
 "2026-11-01-002": {"skip": "ветка репозитория на момент вызова неизвестна"},
 "2026-11-01-003": {"expected": "allow", "why": "read-only", "known": "RT-21"}}}

hook_cases_build.py падает, если у записи выгрузки нет разметки: ретро не уходит с неразмеченными блокировками.

9. metrics.json и сравнение

Схема {"schema": 1, "period": {"since", "until"}, "project", "sources": {...}, "metrics": {...}}. Ключи metrics стабильны: по ним --prev сопоставляет строки. Метрика, которая больше не считается, хранится как {"retired": "<причина>"}.

2026-10-11.metrics.json заполняется из таблицы «Цифры» 11.10. Блокировки — по разметке корпуса: 76 настоящих ошибок хуков, из них 4 по делу, ложных 72 (95 %). В таблице 11.10 было 103 — это завышено (O-1). cd в main — retired.

10. Шаблон и порядок skill

template.md повторяет разделы ретро 11.10 и добавляет «Поправки владельца» и «Что не измерялось». SKILL.md — шаги:

  1. issue и worktree;
  2. retro_metrics.py --out;
  3. hook_blocks_export.py --since → разметка → hook_cases_build.py → tests/run.sh;
  4. retro_leftovers.py --fetch;
  5. --corrections → отбор и группировка;
  6. чтение памяти целиком;
  7. черновик и --markdown --prev;
  8. коммит и PR.

Маркеры: находки RT-n по сквозной нумерации ретро.

Risks / Trade-offs

  • Формат транскриптов Claude Code не документирован. Признаки (hook error, compact_boundary, текст отказа) могут смениться. Смягчение: тесты на синтетических транскриптах фиксируют известный формат, а неожиданные записи пропускаются. Изменение формата увидят нулевые метрики при новом ретро.
  • Эвристика поправок шумит. «Почему» часто бывает вопросом по содержанию. Скрипт только предлагает кандидатов, решает агент, проверяет владелец в PR.
  • Корень пути у удалённого worktree определяется по шаблону пути (решение 4). Нестандартный путь даёт правило src/, и чтения кода могут не посчитаться.
  • Разметка блокировок требует раскладки песочницы, которую агент восстанавливает по транскриптам. Ошибка в раскладке даёт неверный вердикт в корпусе. Смягчение: skip и ревью PR.
  • KNOWN-FIXED валит тесты при исправлении хука, пока не снята пометка. Это сделано намеренно: исправление хука включает снятие known.

Migration Plan

  1. Добавить скрипты, тесты, skill и шаблон; изменить hook_corpus.py и hook_cases_build.py — корпус пересобирается без изменения записей.
  2. 2026-10-11.metrics.json — из таблицы ретро 11.10.
  3. Удалить retro_stats.py и retro_blocks.py, поправить ссылки в CLAUDE.md.
  4. После merge — bash install.sh --no-permissions: doctor и раннер корпуса. Первое ретро — по команде владельца на машине с полигоном.

Откат — revert PR. Корпус возвращается к одному сегменту.

Decisions made without discussion

Дата Решение Обоснование Цена ошибки
2026-10-24 Skill проектный, install.sh не меняется Запуск решён «из репозитория обвязки»; данные корпуса и черновик там же Чтобы запускать из другого каталога, придётся ставить skill глобально
2026-10-24 Сессия входит в срез по cwd первой записи, а каталоги ищутся по префиксу slug Префикс находит каталоги worktree, cwd отсекает чужой проект с тем же префиксом Сессия, начатая вне проекта и перешедшая в него, не войдёт в срез
2026-10-24 Корень пути удалённого worktree — по шаблонам .claude/worktrees/, .wt/ Каталогов уже нет, а code-paths нужен для «кода» Нестандартное место worktree: чтения кода не посчитаются
2026-10-24 Признаки поправок — в первых 200 символах; промпты > 2000 символов не проверяются Длинные промпты — вставки материала, признаки в них случайны Пропуск поправки внутри длинного сообщения
2026-10-24 KNOWN-FIXED — FAIL Иначе пометка known переживёт исправление и скроет регрессию Исправление хука требует ещё одной правки (снять known)
2026-10-24 hook_cases_build.py падает на записи без разметки Ретро не должно уходить с неразмеченными блокировками Нельзя закоммитить частичную разметку
2026-10-24 Метрика cd в main — retired RT-2 снят хуками 0.4.0; счёт был по именам репозиториев awp-ca Потеря одного ряда сравнения
2026-10-24 Блокировки 11.10 в metrics.json — 76 ошибок хука, 72 ложных (по корпусу), а не 103 из таблицы 103 завышено выводами со словом BLOCKED (O-1) Сравнение с таблицей 11.10 по этой строке расходится с документом
2026-10-24 retro_stats.py и retro_blocks.py удаляются Их заменяет retro_metrics.py; в ретро 11.10 они упомянуты как история Чтобы воспроизвести 11.10 буква в букву, нужен git history
2026-10-24 Ходы и токены — по уникальному message.id, а не по строкам транскрипта Строки одного сообщения повторяют usage; подсчёт по строкам завышает токены в 2–3 раза Ходы и токены 11.10 (по строкам) несравнимы; в 2026-10-11.metrics.json они помечены
2026-10-24 Записи ассистента с isApiErrorMessage или model: <synthetic> — не ход модели: не считаются в ходах, токенах и стартовом контексте Это сообщения Claude Code об ошибке API с нулевым usage; на живых транскриптах давали стартовый контекст 0 (O-18) Если Claude Code пометит так настоящий ход, он выпадет из подсчёта
2026-10-24 SDD workspace в worktree неслитой ветки — не хвост Живой прогон показал рабочий каталог текущего плана как хвост (O-19); это работа в процессе Брошенная неслитая задача со своим SDD workspace не попадёт в хвосты (её видно по worktree)
2026-10-24 Свежая ветка, совпадающая с origin/main, — не хвост --merged считает слитой и ветку без коммитов, например только что созданный worktree Брошенная пустая ветка не попадёт в хвосты
2026-10-24 Сторона Gitea в хвостах не проверяется Хрупко (формат описаний PR и issue); агент видит это при чтении Незакрытая issue после merge не попадёт в метрику