docs(spec): /harness-retro — срез метрик, поправки владельца, хвосты закрытия, корпус по сегментам (#29)

OpenSpec change harness-retro: proposal, specs (harness-retro — новая
capability; hook-guards — MODIFIED «Регрессионный корпус хуков»: сегменты
и known), design с Decisions made without discussion. По итогам
brainstorming 24.10.2026: черновик ретро, срез одного проекта, запуск из
репозитория обвязки, разметка блокировок агентом в корпус, хвосты по
текущему состоянию, поправки владельца по процессу, подход A.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
andrey.epifantsev 2026-10-24 00:47:00 +04:00
parent 957ce1e007
commit b77776849e
5 changed files with 485 additions and 0 deletions

View File

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-24

View File

@ -0,0 +1,174 @@
# Design: `/harness-retro`
## Context
Ретро 11.10.2026 (`docs/retro/2026-10-11.md`) сделано вручную. Таблицу «Цифры» собрали из вывода `retro_stats.py` и `retro_blocks.py`, находки — по прочтению 47 файлов памяти. В скриптах зашиты имена репозиториев awp-ca, вывод — текст, тестов нет. Подсчёт блокировок завышен: в 103 вошли выводы команд со словом `BLOCKED`, настоящих ошибок хука было 76 (O-1, #6).
После ретро вышли хуки по цели записи (0.4.0, корпус 129 записей), роли (0.3.0), allow-правила (0.5.0) и worktree в `.claude/worktrees/` (0.5.1). Что из этого помогло, видно только по сравнимым цифрам.
Владелец решил (brainstorming 24.10.2026):
- результат — черновик ретро-документа;
- срез — один проект или workspace;
- запуск — из репозитория обвязки, проект передаётся аргументом;
- ложные блокировки размечает агент по спеке, и они идут в корпус;
- хвосты закрытия ищутся по текущему состоянию репозиториев;
- поправки владельца по процессу собираются в ретро;
- подход A — проектный skill и модуль метрик с тестами.
Транскрипты Claude Code: `~/.claude/projects/<slug>/<session>.jsonl`, по строке на запись. Типы записей: `user`, `assistant` (`message.content`: `text`, `tool_use`; `message.usage`), `system` (`subtype: compact_boundary`), `ai-title`, `permission-mode`. У записей есть `cwd`, `gitBranch`, `timestamp`.
Сессия, перешедшая в worktree, пишет транскрипт в каталог со slug пути worktree. Пример — эта сессия: `-Users-andrejepifancev-Projects-cc-harness-wt-6-guard-main-target`.
## Goals / Non-Goals
**Goals**
- Повторяемый срез метрик одного проекта в JSON и сравнение с прошлым ретро.
- Блокировки среза размечаются и становятся регрессионными записями корпуса; известные ошибки хуков видны в doctor.
- Кандидаты в поправки владельца с контекстом; агент отбирает и группирует.
- Хвосты чеклиста закрытия по состоянию репозиториев.
- Черновик ретро по шаблону, утверждение через PR.
**Non-Goals**
- Сторона Gitea в хвостах: открытые issue после слитого PR, пропущенные follow-up.
- Срез по нескольким проектам сразу.
- Автоматические находки по порогам.
- Автоматическая очистка хвостов: скрипт только подсказывает команду.
- Переписывать ретро 11.10.
## Decisions
### 1. Проектный skill, без установки
`/harness-retro` нужен только в сессии в репозитории обвязки: туда пишется черновик, там корпус и `hook_cases_build.py`. Поэтому skill проектный (`.claude/skills/harness-retro/`), как skills openspec. `install.sh` и `~/.claude` не меняются.
`.claude/skills/` не входит в `.claude/code-paths`, поэтому `SKILL.md` и шаблон — документация. Код лежит в `docs/retro/scripts/`, как и сейчас.
### 2. `retro_metrics.py`: модуль на функциях
Только stdlib, Python 3.8+. CLI:
```
retro_metrics.py <папка проекта> [--since ГГГГ-ММ-ДД] [--harness <репозиторий обвязки>]
[--projects-root <каталог>] [--out <metrics.json>]
[--markdown [--prev <прошлый.metrics.json>]] [--corrections]
```
- `--projects-root` по умолчанию — `${CLAUDE_CONFIG_DIR:-~/.claude}/projects`; тесты подставляют свой.
- `--harness` по умолчанию — репозиторий, в котором лежит скрипт. Из него берутся даты прошлых ретро для `--since`.
Функции:
- `slug(path)`;
- `project_sessions(project, root, since)` — список транскриптов среза;
- `read_session(path)` — генератор записей, битые строки считаются;
- `session_stats(records, code_of)` — словарь счётчиков сессии;
- `aggregate(stats)` — метрики среза;
- `corrections(records)` — кандидаты;
- `to_markdown(metrics, prev)`;
- `main(argv)`.
Каждая метрика считается в одном месте, в `session_stats`, и проверяется отдельным тестом.
### 3. Сессии проекта
Каталоги-кандидаты — те, чьё имя начинается со slug проекта. В срез сессия входит по первому `cwd`: путь внутри проекта или внутри `<проект>.wt/` (старое место worktree). Префикс slug нужен, чтобы найти каталоги worktree; проверка `cwd` отсекает чужой проект `/p/repo-tools` при проекте `/p/repo`.
### 4. Что такое «код»
Пути в транскриптах относятся к репозиториям, которых может уже не быть: worktree удалены. Относительный путь считается от корня worktree или репозитория. Корень определяется в таком порядке:
1. часть пути после `/.claude/worktrees/<имя>/`;
2. затем после `<репо>.wt/<имя>/`;
3. затем после `<workspace>/.wt/<задача>/<репо>/`;
4. затем после ближайшего существующего каталога с `.git`.
Префиксы кода берутся из `.claude/code-paths` основного репозитория, без него — правило `src/` на любой глубине. Так же решает require-plan.
Bash-команда считается чтением кода, если хотя бы один её простой сегмент (разбиение по `&&`, `||`, `;`, `|`) начинается с `grep`, `rg`, `find`, `cat`, `sed -n` или `head` и хотя бы один аргумент после разбора `shlex` (при ошибке — `split`) указывает в каталог кода относительно `cwd`.
### 5. Блокировки, отказы, роли
- Блокировка: результат инструмента совпадает с `^\s*PreToolUse:\S+ hook error` и содержит `BLOCKED`. Хук определяется так же, как в `hook_blocks_export.hook_of`, функция импортируется.
- Отказ классификатора: `denied by the Claude Code auto mode classifier. Reason: \[([^\]]+)\]`.
- Отказ владельца: результат инструмента начинается с `The user doesn't want to proceed with this tool use` или содержит `[Request interrupted by user for tool use]`.
- Роль: первый `tool_use` с `name: Skill` и `input.skill` = `role` (или `…:role`), аргумент — `input.args`.
### 6. Кандидаты в поправки
Признаки ищутся в первых 200 символах промпта, без учёта регистра и с границами слов. Список признаков — в спеке. Промпт длиннее 2000 символов признаком не проверяется: это вставка материала.
Последнее действие — последний `tool_use` ассистента перед промптом: `Bash: <команда>`, `Edit|Write|Read: <путь>`, `<Имя>` для остальных. Хвост ответа — последние 200 символов последнего текстового блока ассистента.
Весь вывод проходит через `redact()` из `hook_blocks_export.py`.
### 7. `retro_leftovers.py`
Репозитории: сам git-проект или каталоги из таблицы workspace (разбор как в `doctor.sh`). Для каждого:
- `git branch -r --merged origin/main` без `origin/main` и `origin/HEAD`;
- `git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads`, где слитые в `origin/main` или `[gone]`;
- `git worktree list --porcelain` — ветка из слитых или `prunable`;
- `ls .superpowers/sdd/*/` в основном checkout и в каждом worktree;
- `git ls-tree -d --name-only origin/main openspec/changes/` без `archive`;
- `git rev-list --count HEAD..origin/main` и `git status --porcelain` основного checkout.
Нет `origin/main` — у репозитория одна строка `kind: no-origin-main`. `--fetch` перед проверками делает `git fetch --prune origin`.
### 8. Корпус: сегменты и `known`
Формат `hook-cases.jsonl`: строка с ключом `layout` — шапка сегмента `{home, layout}`, следующие строки — записи этого сегмента. Первый сегмент — пилот 12.10 и живые случаи, без изменений.
`hook_corpus.py` строит песочницу на каждый сегмент (`<tmp>/seg-<n>/home`). Поле `known`:
- расхождение при `known` — `KNOWN <id>`, тест не падает;
- совпадение при `known` — `KNOWN-FIXED <id>`, тест падает.
Итог: `корпус хуков: N/M`, при известных ошибках — `корпус хуков: N/M, известных ошибок K`. N — записи без расхождений, считая известные; M — все записи.
`hook-labels-<дата>.json`:
```json
{"home": "/Users/x", "layout": [...], "cases": {"2026-11-01-001": {"expected": "allow", "why": "devnull"},
"2026-11-01-002": {"skip": "ветка репозитория на момент вызова неизвестна"},
"2026-11-01-003": {"expected": "allow", "why": "read-only", "known": "RT-21"}}}
```
`hook_cases_build.py` падает, если у записи выгрузки нет разметки: ретро не уходит с неразмеченными блокировками.
### 9. `metrics.json` и сравнение
Схема `{"schema": 1, "period": {"since", "until"}, "project", "sources": {...}, "metrics": {...}}`. Ключи `metrics` стабильны: по ним `--prev` сопоставляет строки. Метрика, которая больше не считается, хранится как `{"retired": "<причина>"}`.
`2026-10-11.metrics.json` заполняется из таблицы «Цифры» 11.10. Блокировки — по разметке корпуса: 76 настоящих ошибок хуков, из них 4 по делу, ложных 72 (95 %). В таблице 11.10 было 103 — это завышено (O-1). `cd` в main — `retired`.
### 10. Шаблон и порядок skill
`template.md` повторяет разделы ретро 11.10 и добавляет «Поправки владельца» и «Что не измерялось». `SKILL.md` — шаги:
1. issue и worktree;
2. `retro_metrics.py --out`;
3. `hook_blocks_export.py --since` → разметка → `hook_cases_build.py` → `tests/run.sh`;
4. `retro_leftovers.py --fetch`;
5. `--corrections` → отбор и группировка;
6. чтение памяти целиком;
7. черновик и `--markdown --prev`;
8. коммит и PR.
Маркеры: находки `RT-n` по сквозной нумерации ретро.
## Risks / Trade-offs
- **Формат транскриптов Claude Code не документирован.** Признаки (`hook error`, `compact_boundary`, текст отказа) могут смениться. Смягчение: тесты на синтетических транскриптах фиксируют известный формат, а неожиданные записи пропускаются. Изменение формата увидят нулевые метрики при новом ретро.
- **Эвристика поправок шумит.** «Почему» часто бывает вопросом по содержанию. Скрипт только предлагает кандидатов, решает агент, проверяет владелец в PR.
- **Корень пути у удалённого worktree** определяется по шаблону пути (решение 4). Нестандартный путь даёт правило `src/`, и чтения кода могут не посчитаться.
- **Разметка блокировок требует раскладки песочницы,** которую агент восстанавливает по транскриптам. Ошибка в раскладке даёт неверный вердикт в корпусе. Смягчение: `skip` и ревью PR.
- **`KNOWN-FIXED` валит тесты при исправлении хука,** пока не снята пометка. Это сделано намеренно: исправление хука включает снятие `known`.
## Migration Plan
1. Добавить скрипты, тесты, skill и шаблон; изменить `hook_corpus.py` и `hook_cases_build.py` — корпус пересобирается без изменения записей.
2. `2026-10-11.metrics.json` — из таблицы ретро 11.10.
3. Удалить `retro_stats.py` и `retro_blocks.py`, поправить ссылки в `CLAUDE.md`.
4. После merge — `bash install.sh --no-permissions`: doctor и раннер корпуса. Первое ретро — по команде владельца на машине с полигоном.
Откат — revert PR. Корпус возвращается к одному сегменту.
## Decisions made without discussion
| Дата | Решение | Обоснование | Цена ошибки |
|---|---|---|---|
| 2026-10-24 | Skill проектный, `install.sh` не меняется | Запуск решён «из репозитория обвязки»; данные корпуса и черновик там же | Чтобы запускать из другого каталога, придётся ставить skill глобально |
| 2026-10-24 | Сессия входит в срез по `cwd` первой записи, а каталоги ищутся по префиксу slug | Префикс находит каталоги worktree, `cwd` отсекает чужой проект с тем же префиксом | Сессия, начатая вне проекта и перешедшая в него, не войдёт в срез |
| 2026-10-24 | Корень пути удалённого worktree — по шаблонам `.claude/worktrees/`, `.wt/` | Каталогов уже нет, а `code-paths` нужен для «кода» | Нестандартное место worktree: чтения кода не посчитаются |
| 2026-10-24 | Признаки поправок — в первых 200 символах; промпты > 2000 символов не проверяются | Длинные промпты — вставки материала, признаки в них случайны | Пропуск поправки внутри длинного сообщения |
| 2026-10-24 | `KNOWN-FIXED` — FAIL | Иначе пометка `known` переживёт исправление и скроет регрессию | Исправление хука требует ещё одной правки (снять `known`) |
| 2026-10-24 | `hook_cases_build.py` падает на записи без разметки | Ретро не должно уходить с неразмеченными блокировками | Нельзя закоммитить частичную разметку |
| 2026-10-24 | Метрика `cd` в main — `retired` | RT-2 снят хуками 0.4.0; счёт был по именам репозиториев awp-ca | Потеря одного ряда сравнения |
| 2026-10-24 | Блокировки 11.10 в `metrics.json` — 76 ошибок хука, 72 ложных (по корпусу), а не 103 из таблицы | 103 завышено выводами со словом `BLOCKED` (O-1) | Сравнение с таблицей 11.10 по этой строке расходится с документом |
| 2026-10-24 | `retro_stats.py` и `retro_blocks.py` удаляются | Их заменяет `retro_metrics.py`; в ретро 11.10 они упомянуты как история | Чтобы воспроизвести 11.10 буква в букву, нужен git history |
| 2026-10-24 | Сторона Gitea в хвостах не проверяется | Хрупко (формат описаний PR и issue); агент видит это при чтении | Незакрытая issue после merge не попадёт в метрику |

View File

@ -0,0 +1,69 @@
# Proposal: `/harness-retro` — регулярное ретро обвязки
Issue: #29. Источники:
- ретро `docs/retro/2026-10-11.md` — B12, решение владельца 3 (замер codegraph);
- находки O-10, O-11 (#21), O-17 (#28);
- brainstorming 24.10.2026.
## Why
Ретро 11.10 считали два одноразовых скрипта, `docs/retro/scripts/retro_stats.py` и `retro_blocks.py`. У них нет функций и тестов. В них зашиты имена репозиториев awp-ca, и они ничего не знают про `.claude/worktrees/`. Вывод — текст для глаз, поэтому таблицу «Цифры» собирали вручную.
Повторить такое ретро и сравнить его с прошлым нельзя, а именно сравнение показывает, помогли ли изменения обвязки: хуки 0.4.0, роли 0.3.0, allow-правила 0.5.0. Решение владельца 3 требует замерить codegraph в одном срезе, и метрика должна входить в `/harness-retro`.
Владелец хочет видеть в ретро места, где он поправлял агента по процессу. Сейчас они теряются в транскриптах.
Блокировки, размеченные при ретро, в регрессионный корпус хуков не попадают. Ложный блок, найденный на ретро, тестом не становится.
## What Changes
- **Новый проектный skill `/harness-retro`** (`.claude/skills/harness-retro/`). Запускается в сессии в репозитории обвязки: `/harness-retro <папка проекта или workspace> [--since ГГГГ-ММ-ДД]`. Работа идёт по циклу фичи: issue, `EnterWorktree`, PR. Результат — черновик `docs/retro/<дата>.md` по шаблону и `docs/retro/<дата>.metrics.json`. Утверждение — merge владельца.
- **`docs/retro/scripts/retro_metrics.py`** — stdlib, на функциях, с тестами. Даёт метрики среза по транскриптам и памяти проекта:
- сессии, промпты, инструменты, роли, сабагенты, skills, токены, компакции;
- стартовый контекст;
- блокировки хуков;
- отказы классификатора и владельца;
- codegraph против чтения кода;
- поправки владельца;
- «перечитай память»;
- размер памяти.
Выводит JSON, таблицу «Цифры» со сравнением с прошлым `*.metrics.json` и кандидатов в поправки владельца с контекстом.
- **`docs/retro/scripts/retro_leftovers.py`** ищет хвосты чеклиста закрытия по текущему состоянию репозиториев проекта:
- слитые ветки на сервере и локально, worktree на них;
- `.superpowers/sdd/*`;
- неархивные changes в `origin/main`;
- отставший или грязный основной checkout.
Только чтение; `--fetch` по запросу.
- **`hook_blocks_export.py --since`** выгружает блокировки среза в `docs/retro/fixtures/hook-blocks-<дата>.jsonl`. Агент размечает их по спеке `hook-guards` в `hook-labels-<дата>.json` (вердикт, класс, раскладка песочницы), и `hook_cases_build.py` дописывает их в корпус.
- **Корпус хуков** состоит из сегментов: у каждого своя шапка `{home, layout}`, раннер строит песочницу на сегмент. Запись с `known: "RT-n"` — известная ошибка текущих хуков, тест на ней не падает. Doctor показывает число известных ошибок.
- `retro_stats.py` и `retro_blocks.py` удаляются. Их заменяет `retro_metrics.py`. Ретро 11.10 ссылается на них как на историю.
- `docs/retro/2026-10-11.metrics.json` переносится один раз из таблицы ретро 11.10, чтобы следующему ретро было с чем сравнивать.
## Capabilities
### New Capabilities
- `harness-retro`: срез метрик обвязки по транскриптам, памяти и состоянию репозиториев проекта; кандидаты в поправки владельца; разметка блокировок в корпус; черновик ретро и сравнение с прошлым.
### Modified Capabilities
- `hook-guards`: требование «Регрессионный корпус хуков» — сегменты с собственной раскладкой и известные ошибки `known`.
## Impact
- **Код:**
- новые `docs/retro/scripts/retro_metrics.py`, `retro_leftovers.py`;
- `hook_blocks_export.py` (`--since`, id);
- `hook_cases_build.py` (разметка из `hook-labels-*.json`, сегменты);
- `skills/harness-doctor/scripts/hook_corpus.py` (сегменты, `known`);
- `doctor.sh` (строка корпуса).
- **Skill:** `.claude/skills/harness-retro/SKILL.md` и `template.md`. Он проектный, поэтому `install.sh` не меняется.
- **Тесты:**
- новые `tests/test-retro-metrics.sh`, `tests/test-retro-leftovers.sh`;
- дополнения в `test-hook-corpus.sh`, `test-hook-export.sh`, `test-doctor.sh`.
- **Документация:** `CLAUDE.md` («Команды», «Архитектура», «Тесты»), `docs/HARNESS.md` (раздел «Ретро»), `README.md`, `CHANGELOG.md` 0.6.0.
- **Вне задачи:**
- сторона Gitea в хвостах закрытия: открытые issue после слитого PR, пропущенные follow-up;
- срезы по нескольким проектам сразу;
- автоматические находки по порогам;
- workspace awp-ca как полигон — первое ретро по нему делается на той машине.

View File

@ -0,0 +1,207 @@
# 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: Период среза
Сессия SHALL входить в срез, если её первая метка времени не раньше `--since`. Без `--since` начало периода MUST браться из даты в имени последнего `docs/retro/ГГГГ-ММ-ДД*.md` репозитория обвязки. Если сессий нет, `retro_metrics.py` MUST завершаться с кодом 1 и сообщением, какой slug и период искались.
#### Scenario: Период по умолчанию
- **WHEN** последний ретро-документ — `docs/retro/2026-10-11.md` и `--since` не указан
- **THEN** в срез входят сессии с первой меткой времени 2026-10-11 и позже
#### 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`, не начинающееся с `<`. Прерывания `[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` чтением кода не считается
### Requirement: Память проекта
Метрика памяти SHALL выводить размер `~/.claude/projects/<slug проекта>/memory/MEMORY.md` в байтах, число и общий объём файлов в этом каталоге. Отсутствующий каталог MUST давать нули, а не ошибку. Отдельно SHALL считаться промпты «перечитай память/контекст».
#### Scenario: Памяти нет
- **WHEN** каталога памяти проекта нет
- **THEN** метрики памяти равны нулю, срез завершается с кодом 0
### Requirement: Кандидаты в поправки владельца
`retro_metrics.py --corrections` SHALL выгружать кандидатов в поправки владельца по процессу. Кандидат — это:
- промпт владельца со словом-признаком: «нет», «не так», «почему», «зачем», «опять», «снова», «я же», «стоп», «подожди», «не надо», «сначала», «верни», «откати»;
- прерывание;
- отказ владельца в разрешении.
Отбор настоящих поправок делает агент, не скрипт.
#### 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 ставиться «—». Метрика, отмеченная в прошлом файле как больше не считаемая, MUST выводиться с этой пометкой.
#### Scenario: Новая метрика
- **WHEN** в прошлом файле нет метрики `corrections`
- **THEN** строка поправок владельца показывает текущее значение и «—» в колонках прошлого и разницы
### Requirement: Секреты не попадают в черновик
Всё, что скрипты ретро выводят из транскриптов (промпты, команды, сообщения), SHALL проходить замену похожих на секреты подстрок на `<redacted>` — той же функцией, что в `hook_blocks_export.py`.
#### 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/*`;
- неархивный `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: Чистый репозиторий
- **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`

View File

@ -0,0 +1,33 @@
# Spec Delta: hook-guards
## MODIFIED Requirements
### Requirement: Регрессионный корпус хуков
Пакет SHALL содержать корпус вызовов хуков из реальных транскриптов и живых случаев. Каждая запись MUST иметь идентификатор, ожидаемый вердикт `allow|block` и код класса.
Корпус MUST состоять из сегментов: строка-шапка `{home, layout}`, за ней записи этого сегмента. Для каждого сегмента строится своя песочница с раскладкой из его шапки. Корпус MUST прогоняться через хуки тестами пакета по источникам и `doctor.sh` по установленным копиям.
Запись с полем `known: "<маркер находки>"` — известная ошибка текущих хуков:
- её расхождение с ожидаемым вердиктом MUST NOT давать FAIL;
- совпадение вердикта при наличии `known` MUST давать FAIL `KNOWN-FIXED`, чтобы пометку сняли.
#### Scenario: Расхождение в тестах пакета
- **WHEN** хук из `hooks/` выносит по записи корпуса без `known` вердикт, отличный от ожидаемого
- **THEN** `bash tests/run.sh` печатает FAIL с идентификатором и классом записи и завершается с ненулевым кодом
#### Scenario: Отчёт doctor
- **WHEN** запущен `doctor.sh` при установленных хуках
- **THEN** в разделе самотеста есть строка `корпус хуков: <совпало>/<всего>`, расхождения перечислены с идентификаторами
- **AND** при наличии записей `known` строка продолжается `, известных ошибок <K>`
#### Scenario: Сегменты с разной раскладкой
- **WHEN** корпус содержит сегмент пилота 12.10 и сегмент ретро с другим домашним каталогом и другими репозиториями
- **THEN** каждая запись прогоняется в песочнице своего сегмента, и вердикты совпадают с ожидаемыми
#### Scenario: Известная ошибка
- **WHEN** запись имеет `expected: allow`, `known: "RT-21"`, а текущий guard-main её блокирует
- **THEN** тест корпуса не падает, в выводе запись отмечена как известная ошибка
#### Scenario: Известная ошибка исправлена
- **WHEN** запись имеет `known: "RT-21"`, а текущие хуки выносят ожидаемый вердикт
- **THEN** тест корпуса печатает FAIL `KNOWN-FIXED` с идентификатором записи