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>
22 KiB
Spec Delta: harness-retro
Purpose
Регулярное ретро обвязки claude-harness. Срез метрик берётся из транскриптов, памяти и состояния репозиториев одного проекта или workspace. Из него получается черновик ретро-документа со сравнением с прошлым ретро, разметкой блокировок хуков в регрессионный корпус и списком поправок владельца по процессу.
ADDED 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, Readhooks/a.pyпри.claude/code-paths=hooks/, Readdocs/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