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

22 KiB
Raw Blame History

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, 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