andrey.epifantsev 76e2d9e884 chore(openspec): архив harness-retro, спека harness-retro в openspec/specs, hook-guards обновлена (#29)
openspec archive harness-retro: change → openspec/changes/archive/2026-10-24-harness-retro/; openspec/specs/harness-retro/spec.md (22 требования), hook-guards — MODIFIED «Регрессионный корпус хуков»; известные ошибки корпуса вынесены в отдельное требование, чтобы текст требования был ≤ 500 символов (validate --strict).

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

238 lines
22 KiB
Markdown
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.

# 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/<D>.metrics.json`, создан черновик `docs/retro/<D>.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` и строка `<command-name>…`
- **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/<slug проекта>/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 <metrics.json>` MUST печатать таблицу из сохранённого среза без пересчёта.
#### Scenario: Новая метрика
- **WHEN** в прошлом файле нет метрики `corrections`
- **THEN** строка поправок владельца показывает текущее значение и «—» в колонках прошлого и разницы
### Requirement: Секреты не попадают в черновик
Всё, что скрипты ретро выводят из транскриптов (промпты, команды, сообщения), SHALL проходить замену похожих на секреты подстрок на `<redacted>` — той же функцией, что в `hook_blocks_export.py`, и MUST проходить её до обрезки, чтобы секрет на границе обрезки не утёк частично.
#### Scenario: Токен в промпте
- **WHEN** промпт-кандидат содержит `tea login add --token 0123456789abcdef0123456789abcdef01234567`
- **THEN** в выводе `--corrections` вместо токена стоит `<redacted>`
### Requirement: Хвосты чеклиста закрытия
`retro_leftovers.py <папка проекта>` SHALL искать хвосты по текущему состоянию каждого репозитория проекта (для workspace — из таблицы репозиториев в `CLAUDE.md`). Хвост — это:
- ветка `origin/<ветка>`, слитая в `origin/main`;
- локальная ветка, слитая или с upstream `gone`;
- worktree на такой ветке или `prunable`;
- `.superpowers/sdd/*` в основном checkout или в worktree такой ветки;
- неархивный `openspec/changes/<id>/` в `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`