Merge pull request 'Архив OpenSpec harness-retro → openspec/specs/harness-retro, hook-guards (#29)' (#32) from chore/archive-harness-retro into main
This commit is contained in:
commit
c7934fb9f3
237
openspec/specs/harness-retro/spec.md
Normal file
237
openspec/specs/harness-retro/spec.md
Normal file
@ -0,0 +1,237 @@
|
||||
# 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`
|
||||
@ -313,17 +313,33 @@ require-plan SHALL защищать по умолчанию любой ката
|
||||
- **THEN** require-plan завершается с кодом 0
|
||||
|
||||
### Requirement: Регрессионный корпус хуков
|
||||
Пакет SHALL содержать корпус вызовов хуков из реальных транскриптов и живых случаев; каждая запись MUST иметь
|
||||
идентификатор, ожидаемый вердикт `allow|block` и код класса. Корпус MUST прогоняться через хуки в песочнице с раскладкой
|
||||
репозиториев из корпуса: тестами пакета по источникам и `doctor.sh` по установленным копиям.
|
||||
Пакет SHALL содержать корпус вызовов хуков из реальных транскриптов и живых случаев. Каждая запись MUST иметь идентификатор, ожидаемый вердикт `allow|block` и код класса.
|
||||
|
||||
Корпус MUST состоять из сегментов: строка-шапка `{home, layout}`, за ней записи этого сегмента. Для каждого сегмента строится своя песочница с раскладкой из его шапки. Корпус MUST прогоняться через хуки тестами пакета по источникам и `doctor.sh` по установленным копиям.
|
||||
|
||||
#### Scenario: Расхождение в тестах пакета
|
||||
- **WHEN** хук из `hooks/` выносит по записи корпуса вердикт, отличный от ожидаемого
|
||||
- **WHEN** хук из `hooks/` выносит по записи корпуса без `known` вердикт, отличный от ожидаемого
|
||||
- **THEN** `bash tests/run.sh` печатает FAIL с идентификатором и классом записи и завершается с ненулевым кодом
|
||||
|
||||
#### Scenario: Отчёт doctor
|
||||
- **WHEN** запущен `doctor.sh` при установленных хуках
|
||||
- **THEN** в разделе самотеста есть строка `корпус хуков: <совпало>/<всего>`, расхождения перечислены с идентификаторами
|
||||
- **AND** при наличии записей `known` строка продолжается `, известных ошибок <K>`
|
||||
|
||||
#### Scenario: Сегменты с разной раскладкой
|
||||
- **WHEN** корпус содержит сегмент пилота 12.10 и сегмент ретро с другим домашним каталогом и другими репозиториями
|
||||
- **THEN** каждая запись прогоняется в песочнице своего сегмента, и вердикты совпадают с ожидаемыми
|
||||
|
||||
### Requirement: Известные ошибки корпуса
|
||||
Запись корпуса с полем `known: "<маркер находки>"` SHALL считаться известной ошибкой текущих хуков: её расхождение с ожидаемым вердиктом MUST NOT давать FAIL, а совпадение вердикта при наличии `known` MUST давать FAIL `KNOWN-FIXED`, чтобы пометку сняли.
|
||||
|
||||
#### Scenario: Известная ошибка
|
||||
- **WHEN** запись имеет `expected: allow`, `known: "RT-21"`, а текущий guard-main её блокирует
|
||||
- **THEN** тест корпуса не падает, в выводе запись отмечена как известная ошибка
|
||||
|
||||
#### Scenario: Известная ошибка исправлена
|
||||
- **WHEN** запись имеет `known: "RT-21"`, а текущие хуки выносят ожидаемый вердикт
|
||||
- **THEN** тест корпуса печатает FAIL `KNOWN-FIXED` с идентификатором записи
|
||||
|
||||
### Requirement: Выгрузка только настоящих блокировок
|
||||
Экстрактор блокировок из транскриптов SHALL выгружать только результаты инструмента, которые являются ошибкой хука, и
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user