# harness-retro Specification ## Purpose Регулярное ретро обвязки claude-harness. Срез метрик берётся из транскриптов, памяти и состояния репозиториев одного проекта или workspace. Из него получается черновик ретро-документа со сравнением с прошлым ретро, разметкой блокировок хуков в регрессионный корпус и списком поправок владельца по процессу. ## Requirements ### Requirement: Запуск из репозитория обвязки `/harness-retro` SHALL быть проектным skill репозитория обвязки (`.claude/skills/harness-retro/`). Он принимает папку проекта или workspace и необязательный `--since ГГГГ-ММ-ДД`. Работа MUST идти по циклу фичи: issue, worktree через `EnterWorktree`, коммит, PR. Skill MUST NOT делать merge. #### Scenario: Черновик и метрики в PR - **WHEN** владелец запускает `/harness-retro ~/projects/awp-ca` в сессии в репозитории обвязки - **THEN** в ветке ретро появляются `docs/retro/<дата>.md` и `docs/retro/<дата>.metrics.json`, открыт PR со ссылкой на issue ретро - **AND** main не меняется до merge владельцем ### Requirement: Каталоги транскриптов проекта `retro_metrics.py` SHALL брать сессии из каталогов `~/.claude/projects/<имя>`, где `<имя>` начинается со slug папки проекта. Slug — абсолютный путь, в котором каждый символ кроме `[A-Za-z0-9]` заменён на `-`. Сессия MUST входить в срез, только если `cwd` её первой записи с этим полем лежит внутри папки проекта или внутри `<папка проекта>.wt/`. #### Scenario: Сессии в worktree проекта - **WHEN** у проекта `/p/repo` есть транскрипты в `~/.claude/projects/-p-repo/` и в `~/.claude/projects/-p-repo--claude-worktrees-x/` - **THEN** сессии обоих каталогов входят в срез #### Scenario: Чужой проект с тем же префиксом - **WHEN** в каталоге `~/.claude/projects/-p-repo-tools/` первая запись сессии имеет `cwd` `/p/repo-tools` - **THEN** сессия в срез проекта `/p/repo` не входит ### Requirement: Транскрипты сабагентов Транскрипты сабагентов сессии `<каталог>/<сессия>/subagents/*.jsonl` SHALL входить в сессию-родителя: ходы, токены, вызовы инструментов, codegraph, блокировки, отказы. Промпты, роль, стартовый контекст и кандидаты в поправки MUST браться только из основного транскрипта. #### Scenario: codegraph у implementer-а - **WHEN** сабагент сессии вызвал `codegraph_explore` и прочитал файл кода, а его хук заблокировал запись - **THEN** вызов, чтение и блокировка считаются в срезе, а сообщение контроллера сабагенту промптом владельца не считается ### Requirement: Период среза Запись SHALL входить в срез, если её метка времени (у записи без метки — предыдущая) не раньше `--since`; сессия — если в ней есть записи периода. Стартовый контекст сессии MUST считаться, только если её первый ход в периоде. Без `--since` начало периода MUST браться из `period.since` существующего `--out`, иначе из даты последнего `docs/retro/ГГГГ-ММ-ДД*.md`. Если сессий нет — код 1 и сообщение со slug и периодом. #### Scenario: Период по умолчанию - **WHEN** последний ретро-документ — `docs/retro/2026-10-11.md`, `--since` не указан и `--out` ещё нет - **THEN** в срез входят записи с меткой времени 2026-10-11 и позже #### Scenario: Длинная сессия на стыке периодов - **WHEN** сессия начата 2026-10-30, а `--since` — 2026-11-01 - **THEN** её записи с 2026-11-01 входят в срез, записи до этой даты и её стартовый контекст — нет #### Scenario: Черновик текущего ретро не сдвигает период - **WHEN** срез уже записан в `docs/retro/.metrics.json`, создан черновик `docs/retro/.md`, и срез запускается снова с тем же `--out` без `--since` - **THEN** период тот же, что в существующем `--out` #### Scenario: Нет транскриптов - **WHEN** ни одного каталога со slug проекта нет или в периоде нет ни одной сессии - **THEN** `retro_metrics.py` завершается с кодом 1 и сообщением со slug и периодом ### Requirement: Формат метрик `retro_metrics.py` SHALL выводить JSON со схемой версии `1` и полями `period`, `project`, `sources`, `metrics`. В `sources` MUST быть использованные каталоги транскриптов, число сессий, объём и число пропущенных битых строк JSONL. Битая строка MUST NOT прерывать срез. #### Scenario: Битая строка - **WHEN** в транскрипте одна строка — не JSON - **THEN** срез считается по остальным строкам, а `sources.bad_lines` равно 1 ### Requirement: Промпты владельца Промпт владельца SHALL считаться так: сообщение пользователя без `tool_result`, не `isMeta`, не сводка компакции (`isCompactSummary`), не начинающееся с `<`. Прерывания `[Request interrupted by user]` MUST считаться отдельной метрикой, не промптами. #### Scenario: Служебные сообщения не считаются промптами - **WHEN** в транскрипте есть сообщение пользователя с `tool_result`, сообщение с `isMeta` и строка `…` - **THEN** ни одно из них не входит в число промптов владельца ### Requirement: Активность сессий Метрики SHALL включать: - ходы модели и вызовы инструментов — всего и по инструментам; - сессии по ролям — по первому вызову skill `role`, без вызова — «не выбрана»; - запуски сабагентов по полю `model`, включая «не указана»; - вызовы skills по имени; - токены вывода, чтения и записи кэша; - число записей `compact_boundary`. #### Scenario: Роль сессии - **WHEN** в сессии первый вызов skill `role` — с аргументом `dev` - **THEN** сессия считается в роли `dev` ### Requirement: Стартовый контекст Стартовый контекст сессии SHALL равняться сумме `input_tokens`, `cache_creation_input_tokens` и `cache_read_input_tokens` первого хода модели. Метрика MUST выводить медиану, минимум и максимум по сессиям среза. #### Scenario: Три сессии - **WHEN** стартовый контекст сессий — 50 000, 56 000 и 61 000 токенов - **THEN** медиана 56 000, минимум 50 000, максимум 61 000 ### Requirement: Блокировки и отказы Блокировкой хука SHALL считаться только результат инструмента, который является ошибкой хука (`PreToolUse:<инструмент> hook error` и `BLOCKED`), с разбивкой по хукам. Отказ классификатора auto mode MUST считаться по причине `Reason: [<причина>]`, отказы владельца — отдельно. #### Scenario: Вывод команды со словом BLOCKED - **WHEN** результат `cat hooks/require-plan.sh` содержит `BLOCKED` - **THEN** это не блокировка хука #### Scenario: Отказ классификатора - **WHEN** результат инструмента содержит `denied by the Claude Code auto mode classifier. Reason: [Self-Modification]` - **THEN** метрика отказов классификатора увеличивается по причине `Self-Modification` ### Requirement: codegraph против чтения кода Метрика codegraph SHALL считать команды `codegraph init`, вызовы `codegraph_explore` и чтения кода, а также долю `codegraph_explore` среди вызовов чтения. Чтение кода — Read файла в каталоге кода, Grep и Glob, Bash с `grep`, `rg`, `find`, `cat`, `sed -n` или `head` по каталогу кода. Каталог кода определяется как в require-plan: префиксы `.claude/code-paths`, без него — любой `src/`. #### Scenario: Доля explore - **WHEN** в сессии 2 вызова `codegraph_explore`, Read `hooks/a.py` при `.claude/code-paths` = `hooks/`, Read `docs/x.md` и `grep -rn x hooks/` - **THEN** вызовов explore 2, чтений кода 2, доля explore 0.5; Read `docs/x.md` чтением кода не считается #### Scenario: Разделитель внутри кавычек - **WHEN** Bash-команда `grep -n "a\|b" hooks/x.py` при `.claude/code-paths` = `hooks/` - **THEN** это одно чтение кода: `|` внутри кавычек команду не делит #### Scenario: Каталог src без code-paths - **WHEN** в репозитории без `.claude/code-paths` выполнены `grep -rn foo src/` и `find svc/src -name x` - **THEN** обе команды — чтение кода ### Requirement: Память проекта Метрика памяти SHALL выводить размер `~/.claude/projects//memory/MEMORY.md` в байтах, число и общий объём файлов в этом каталоге. Отсутствующий каталог MUST давать нули, а не ошибку. Отдельно SHALL считаться промпты-побуждения перечитать: «перечитай», «прочитай», «сверься», «вспомни», «освежи» рядом с «память», «контекст» или `.md`. #### Scenario: Памяти нет - **WHEN** каталога памяти проекта нет - **THEN** метрики памяти равны нулю, срез завершается с кодом 0 #### Scenario: Упоминание контекста — не побуждение - **WHEN** промпт «я сейчас смотрю на размер контекста» и сводка компакции со словами «перечитай память» - **THEN** ни то, ни другое не считается промптом «перечитай память/контекст» ### Requirement: Кандидаты в поправки владельца `retro_metrics.py --corrections` SHALL выгружать кандидатов в поправки владельца по процессу. Кандидат — это: - промпт владельца со словом-признаком: «нет», «не так», «почему», «зачем», «опять», «снова», «я же», «стоп», «подожди», «не надо», «сначала», «верни», «откати», «сделай сам» (агент переложил работу на владельца, H-1); - прерывание; - отказ владельца в разрешении. Отбор настоящих поправок делает агент, не скрипт. #### Scenario: Вопрос после действия агента - **WHEN** после вызова `EnterWorktree` владелец пишет «почему мне нужно это каждый раз подтверждать?» - **THEN** этот промпт выгружается как кандидат ### Requirement: Контекст кандидата Для каждого кандидата SHALL выводиться: - сессия и время; - текст, не длиннее 300 символов; - последнее действие агента перед ним: инструмент и команда или путь; - последние 200 символов последнего ответа агента. #### Scenario: Последнее действие - **WHEN** перед кандидатом агент вызвал `Bash` с командой `git branch -m feat/x` - **THEN** у кандидата указано действие `Bash: git branch -m feat/x` ### Requirement: Сравнение с прошлым ретро `retro_metrics.py --markdown --prev <прошлый>.metrics.json` SHALL печатать таблицу «Цифры» в три колонки: текущее значение, значение прошлого ретро, разница. Если метрики нет в прошлом файле или там `null`, MUST ставиться «—»; снятая или иначе считавшаяся метрика — с пометкой. `--from ` MUST печатать таблицу из сохранённого среза без пересчёта. #### Scenario: Новая метрика - **WHEN** в прошлом файле нет метрики `corrections` - **THEN** строка поправок владельца показывает текущее значение и «—» в колонках прошлого и разницы ### Requirement: Секреты не попадают в черновик Всё, что скрипты ретро выводят из транскриптов (промпты, команды, сообщения), SHALL проходить замену похожих на секреты подстрок на `` — той же функцией, что в `hook_blocks_export.py`, и MUST проходить её до обрезки, чтобы секрет на границе обрезки не утёк частично. #### Scenario: Токен в промпте - **WHEN** промпт-кандидат содержит `tea login add --token 0123456789abcdef0123456789abcdef01234567` - **THEN** в выводе `--corrections` вместо токена стоит `` ### Requirement: Хвосты чеклиста закрытия `retro_leftovers.py <папка проекта>` SHALL искать хвосты по текущему состоянию каждого репозитория проекта (для workspace — из таблицы репозиториев в `CLAUDE.md`). Хвост — это: - ветка `origin/<ветка>`, слитая в `origin/main`; - локальная ветка, слитая или с upstream `gone`; - worktree на такой ветке или `prunable`; - `.superpowers/sdd/*` в основном checkout или в worktree такой ветки; - неархивный `openspec/changes//` в `origin/main`; - отставший или грязный основной checkout. #### Scenario: Слитая ветка осталась на сервере - **WHEN** ветка `feat/6-x` слита в `origin/main` и есть на `origin` - **THEN** выводится хвост вида `remote-branch` с подсказкой `git push origin --delete feat/6-x` #### Scenario: SDD workspace активной задачи - **WHEN** worktree на неслитой ветке `feat/2` содержит `.superpowers/sdd/plan-y/` - **THEN** этот каталог хвостом не считается #### Scenario: Чистый репозиторий - **WHEN** у репозитория нет слитых веток кроме main, нет worktree, SDD workspace, неархивных changes, и основной checkout совпадает с `origin/main` - **THEN** список хвостов пуст ### Requirement: Хвосты только читаются `retro_leftovers.py` SHALL NOT менять репозитории и их рабочие деревья. Единственное исключение — `git fetch --prune origin` при флаге `--fetch`. Вывод MUST быть JSON со списком `{kind, repo, item, hint}` и итогами по видам; с `--markdown` — таблица для черновика. #### Scenario: Без --fetch - **WHEN** скрипт запущен без `--fetch` - **THEN** ссылки `refs/remotes/` и рабочее дерево репозитория не меняются ### Requirement: Выгрузка блокировок среза `hook_blocks_export.py --since <дата>` SHALL выгружать только блокировки вызовов не раньше даты, вместе с транскриптами сабагентов сессии, с идентификаторами `<дата>-NNN`. Выгрузка среза MUST сохраняться в `docs/retro/fixtures/hook-blocks-<дата>.jsonl`; прежняя выгрузка 12.10 не меняется. #### Scenario: Ранний вызов не выгружается - **WHEN** в длинной сессии блокировка до `--since` и блокировка после - **THEN** выгружается только вторая ### Requirement: Разметка блокировок в корпус Агент SHALL размечать выгрузку в `docs/retro/fixtures/hook-labels-<дата>.json`: - для записи — `expected` (`allow|block`) и `why` по спеке `hook-guards`, либо `skip` с причиной; - для файла — раскладку песочницы. `hook_cases_build.py` MUST добавлять по сегменту на каждую пару выгрузка + разметка и падать на выгрузке без разметки. Записи со `skip` в корпус не входят. #### Scenario: Размеченная выгрузка в корпусе - **WHEN** в `hook-labels-2026-11-01.json` три записи размечены и одна имеет `skip` - **THEN** после сборки в корпусе новый сегмент с тремя записями, а `bash tests/run.sh` зелёный ### Requirement: Метрика ложных блокировок Ложной SHALL считаться блокировка среза, размеченная `expected: allow`. Метрика MUST выводить: - число блокировок; - число ложных и их долю; - разбивку ложных на уже исправленные (текущие хуки дают `allow`) и ещё ложные (с `known`). #### Scenario: Исправленная и неисправленная - **WHEN** из 4 блокировок среза 3 размечены `allow`, по двум текущие хуки дают `allow`, одна помечена `known` - **THEN** ложных 3 из 4 (75 %): исправлено 2, ещё ложных 1 ### Requirement: Разделы черновика Черновик SHALL создаваться по шаблону `.claude/skills/harness-retro/template.md`. Разделы: - шапка (период, проект, источники); - «Цифры» из `retro_metrics.py --markdown --prev`; - «Находки»; - «Поправки владельца»; - «Что сработало»; - «Бэклог»; - «Решения владельца» — пустой, заполняется на ревью; - «Что не измерялось», в том числе сторона Gitea чеклиста закрытия. #### Scenario: Пустые решения - **WHEN** черновик создан - **THEN** раздел «Решения владельца» пуст и помечен «заполняется на ревью PR» ### Requirement: Нумерация находок и сохранение метрик Находки SHALL нумероваться `RT-n`, продолжая максимальный номер в прошлых `docs/retro/*.md`. Повторяющийся вид поправки владельца MUST становиться находкой. Метрики среза MUST сохраняться в `docs/retro/<дата>.metrics.json`. #### Scenario: Продолжение нумерации - **WHEN** в `docs/retro/2026-10-11.md` максимальный маркер `RT-20` - **THEN** первая находка нового черновика — `RT-21`