# 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//.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 ] [--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. затем после `/.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` считаются по одной. - Сессия — файл `.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; - `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` строит песочницу на каждый сегмент (`/seg-/home`). Поле `known`: - расхождение при `known` — `KNOWN `, тест не падает; - совпадение при `known` — `KNOWN-FIXED `, тест падает. Итог: `корпус хуков: N/M`, при известных ошибках — `корпус хуков: N/M, известных ошибок K`. N — записи без расхождений, считая известные; M — все записи. `hook-labels-<дата>.json`: ```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: ` — не ход модели: не считаются в ходах, токенах и стартовом контексте | Это сообщения Claude Code об ошибке API с нулевым `usage`; на живых транскриптах давали стартовый контекст 0 (O-18) | Если Claude Code пометит так настоящий ход, он выпадет из подсчёта | | 2026-10-24 | Свежая ветка, совпадающая с `origin/main`, — не хвост | `--merged` считает слитой и ветку без коммитов, например только что созданный worktree | Брошенная пустая ветка не попадёт в хвосты | | 2026-10-24 | Сторона Gitea в хвостах не проверяется | Хрупко (формат описаний PR и issue); агент видит это при чтении | Незакрытая issue после merge не попадёт в метрику |