From 76e2d9e88476c77c58208fc07049a0c4b787eab5 Mon Sep 17 00:00:00 2001 From: "andrey.epifantsev" Date: Sat, 24 Oct 2026 10:11:55 +0400 Subject: [PATCH] =?UTF-8?q?chore(openspec):=20=D0=B0=D1=80=D1=85=D0=B8?= =?UTF-8?q?=D0=B2=20harness-retro,=20=D1=81=D0=BF=D0=B5=D0=BA=D0=B0=20harn?= =?UTF-8?q?ess-retro=20=D0=B2=20openspec/specs,=20hook-guards=20=D0=BE?= =?UTF-8?q?=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20(#29)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../2026-10-24-harness-retro}/.openspec.yaml | 0 .../2026-10-24-harness-retro}/design.md | 0 .../2026-10-24-harness-retro}/proposal.md | 0 .../specs/harness-retro/spec.md | 0 .../specs/hook-guards/spec.md | 0 openspec/specs/harness-retro/spec.md | 237 ++++++++++++++++++ openspec/specs/hook-guards/spec.md | 24 +- 7 files changed, 257 insertions(+), 4 deletions(-) rename openspec/changes/{harness-retro => archive/2026-10-24-harness-retro}/.openspec.yaml (100%) rename openspec/changes/{harness-retro => archive/2026-10-24-harness-retro}/design.md (100%) rename openspec/changes/{harness-retro => archive/2026-10-24-harness-retro}/proposal.md (100%) rename openspec/changes/{harness-retro => archive/2026-10-24-harness-retro}/specs/harness-retro/spec.md (100%) rename openspec/changes/{harness-retro => archive/2026-10-24-harness-retro}/specs/hook-guards/spec.md (100%) create mode 100644 openspec/specs/harness-retro/spec.md diff --git a/openspec/changes/harness-retro/.openspec.yaml b/openspec/changes/archive/2026-10-24-harness-retro/.openspec.yaml similarity index 100% rename from openspec/changes/harness-retro/.openspec.yaml rename to openspec/changes/archive/2026-10-24-harness-retro/.openspec.yaml diff --git a/openspec/changes/harness-retro/design.md b/openspec/changes/archive/2026-10-24-harness-retro/design.md similarity index 100% rename from openspec/changes/harness-retro/design.md rename to openspec/changes/archive/2026-10-24-harness-retro/design.md diff --git a/openspec/changes/harness-retro/proposal.md b/openspec/changes/archive/2026-10-24-harness-retro/proposal.md similarity index 100% rename from openspec/changes/harness-retro/proposal.md rename to openspec/changes/archive/2026-10-24-harness-retro/proposal.md diff --git a/openspec/changes/harness-retro/specs/harness-retro/spec.md b/openspec/changes/archive/2026-10-24-harness-retro/specs/harness-retro/spec.md similarity index 100% rename from openspec/changes/harness-retro/specs/harness-retro/spec.md rename to openspec/changes/archive/2026-10-24-harness-retro/specs/harness-retro/spec.md diff --git a/openspec/changes/harness-retro/specs/hook-guards/spec.md b/openspec/changes/archive/2026-10-24-harness-retro/specs/hook-guards/spec.md similarity index 100% rename from openspec/changes/harness-retro/specs/hook-guards/spec.md rename to openspec/changes/archive/2026-10-24-harness-retro/specs/hook-guards/spec.md diff --git a/openspec/specs/harness-retro/spec.md b/openspec/specs/harness-retro/spec.md new file mode 100644 index 0000000..b71a282 --- /dev/null +++ b/openspec/specs/harness-retro/spec.md @@ -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/.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` diff --git a/openspec/specs/hook-guards/spec.md b/openspec/specs/hook-guards/spec.md index c4f0718..5b3fe75 100644 --- a/openspec/specs/hook-guards/spec.md +++ b/openspec/specs/hook-guards/spec.md @@ -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` строка продолжается `, известных ошибок ` + +#### 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 выгружать только результаты инструмента, которые являются ошибкой хука, и