From 6b7a073f6f822308bbde56b9926c67f46571f706 Mon Sep 17 00:00:00 2001 From: "andrey.epifantsev" Date: Fri, 16 Oct 2026 10:16:06 +0400 Subject: [PATCH] =?UTF-8?q?chore(openspec):=20=D0=B0=D1=80=D1=85=D0=B8?= =?UTF-8?q?=D0=B2=20hooks-write-target,=20=D1=81=D0=BF=D0=B5=D0=BA=D0=B0?= =?UTF-8?q?=20hook-guards=20=D0=B2=20openspec/specs=20(#6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit openspec archive hooks-write-target: change перенесён в openspec/changes/archive/2026-10-16-hooks-write-target/, дельта применена — openspec/specs/hook-guards/spec.md (20 требований), validate --strict проходит. Ссылки на путь спеки в harness_target.py, hook_cases_build.py и CHANGELOG переведены на openspec/specs/hook-guards/spec.md; в CLAUDE.md «Текущая работа» B1+B2 переведены в «Сделано» (PR #12, follow-ups — #13). Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- CLAUDE.md | 2 +- docs/retro/scripts/hook_cases_build.py | 2 +- hooks/harness_target.py | 2 +- .../.openspec.yaml | 0 .../2026-10-16-hooks-write-target}/design.md | 0 .../proposal.md | 0 .../specs/hook-guards/spec.md | 0 openspec/specs/hook-guards/spec.md | 325 ++++++++++++++++++ 9 files changed, 329 insertions(+), 4 deletions(-) rename openspec/changes/{hooks-write-target => archive/2026-10-16-hooks-write-target}/.openspec.yaml (100%) rename openspec/changes/{hooks-write-target => archive/2026-10-16-hooks-write-target}/design.md (100%) rename openspec/changes/{hooks-write-target => archive/2026-10-16-hooks-write-target}/proposal.md (100%) rename openspec/changes/{hooks-write-target => archive/2026-10-16-hooks-write-target}/specs/hook-guards/spec.md (100%) create mode 100644 openspec/specs/hook-guards/spec.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d81cd5..5bd95a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changelog ## 0.4.0 — 2026-10-16 -- guard-main решает по цели записи, а не по cwd и тексту команды (#6, B1; RT-1, RT-2, RT-7). Цели — файл Edit/Write, в Bash — редиректы (кроме `/dev/null` и `N>&M`), команды записи с учётом `cd`, `git -C`, `sh -c`, git-мутации. Цель вне репозитория или в worktree на ветке — пропуск; неразрешимый путь — пропуск, git-мутация с неопределённым репозиторием — блок. `git push` блокируется, только если пушит в main; `git switch -c` в той же команде переводит последующие коммиты на новую ветку. `pip install` больше не блокируется. Спека — `openspec/changes/hooks-write-target/`. +- guard-main решает по цели записи, а не по cwd и тексту команды (#6, B1; RT-1, RT-2, RT-7). Цели — файл Edit/Write, в Bash — редиректы (кроме `/dev/null` и `N>&M`), команды записи с учётом `cd`, `git -C`, `sh -c`, git-мутации. Цель вне репозитория или в worktree на ветке — пропуск; неразрешимый путь — пропуск, git-мутация с неопределённым репозиторием — блок. `git push` блокируется, только если пушит в main; `git switch -c` в той же команде переводит последующие коммиты на новую ветку. `pip install` больше не блокируется. Спека — `openspec/specs/hook-guards/spec.md`, change в архиве — `openspec/changes/archive/2026-10-16-hooks-write-target/`. - **Несовместимо:** исключение для openspec-only коммитов и `openspec archive` на main удалено — архив всегда отдельным PR (решение владельца 1, 11.10.2026). - require-plan берёт корень, ветку, `.claude/code-paths`, `.bounded` и планы из репозитория правимого файла. - Общее ядро `hooks/harness_target.py` (python3 stdlib); `guard-main.sh` и `require-plan.sh` — обёртки, регистрация в `settings.json` прежняя. Режим `targets` показывает, какие цели видит хук. diff --git a/CLAUDE.md b/CLAUDE.md index 99b814a..f08209f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,7 +40,7 @@ ## Текущая работа Бэклог обвязки — `docs/retro/2026-10-11.md`, раздел 4 (B1–B12), решения владельца — раздел 5. Находки ретро маркируются `RT-n`. - Сделано: B5+B6 часть 1 — слой ролей (PR #4, 0.3.0). Часть 2 (awp-ca) — issue #2, отложенные находки — issue #5. -- В работе: B1+B2 и отмена исключения openspec-only для guard-main — issue #6, ветка `feat/6-guard-main-target`. +- Сделано: B1+B2 и отмена исключения openspec-only для guard-main — хуки по цели записи и корпус хуков (issue #6, PR #12, 0.4.0), спека — `openspec/specs/hook-guards/`. Отложенные находки — issue #13. - Дальше: B3/B4/B10, B12. ## OpenSpec и superpowers diff --git a/docs/retro/scripts/hook_cases_build.py b/docs/retro/scripts/hook_cases_build.py index 23ff2d8..392ad2c 100644 --- a/docs/retro/scripts/hook_cases_build.py +++ b/docs/retro/scripts/hook_cases_build.py @@ -6,7 +6,7 @@ Вход: docs/retro/fixtures/hook-blocks.jsonl (hook_blocks_export.py, 12.10.2026, 126 записей). Выход: первая строка — {"home": …, "layout": […]} (раскладка песочницы), дальше по записи на случай: поля выгрузки + id, expected (allow|block), why (класс). Вердикт размечен вручную по спеке -openspec/changes/hooks-write-target/specs/hook-guards/spec.md, а не прогоном хука. +openspec/specs/hook-guards/spec.md, а не прогоном хука. """ import json import os diff --git a/hooks/harness_target.py b/hooks/harness_target.py index 0c7d6c2..063486b 100644 --- a/hooks/harness_target.py +++ b/hooks/harness_target.py @@ -6,7 +6,7 @@ Отладка разбора без файловой системы: python3 harness_target.py targets '<команда>' [cwd] → по строке на цель: «kind path op» Любой сбой разбора или вызова git — пропуск (exit 0). Только stdlib, Python 3.8+. -Спека: openspec/changes/hooks-write-target/specs/hook-guards/spec.md. +Спека: openspec/specs/hook-guards/spec.md. """ import glob import json diff --git a/openspec/changes/hooks-write-target/.openspec.yaml b/openspec/changes/archive/2026-10-16-hooks-write-target/.openspec.yaml similarity index 100% rename from openspec/changes/hooks-write-target/.openspec.yaml rename to openspec/changes/archive/2026-10-16-hooks-write-target/.openspec.yaml diff --git a/openspec/changes/hooks-write-target/design.md b/openspec/changes/archive/2026-10-16-hooks-write-target/design.md similarity index 100% rename from openspec/changes/hooks-write-target/design.md rename to openspec/changes/archive/2026-10-16-hooks-write-target/design.md diff --git a/openspec/changes/hooks-write-target/proposal.md b/openspec/changes/archive/2026-10-16-hooks-write-target/proposal.md similarity index 100% rename from openspec/changes/hooks-write-target/proposal.md rename to openspec/changes/archive/2026-10-16-hooks-write-target/proposal.md diff --git a/openspec/changes/hooks-write-target/specs/hook-guards/spec.md b/openspec/changes/archive/2026-10-16-hooks-write-target/specs/hook-guards/spec.md similarity index 100% rename from openspec/changes/hooks-write-target/specs/hook-guards/spec.md rename to openspec/changes/archive/2026-10-16-hooks-write-target/specs/hook-guards/spec.md diff --git a/openspec/specs/hook-guards/spec.md b/openspec/specs/hook-guards/spec.md new file mode 100644 index 0000000..8e3251b --- /dev/null +++ b/openspec/specs/hook-guards/spec.md @@ -0,0 +1,325 @@ +# hook-guards Specification + +## Purpose +Блокирующие хуки обвязки Claude Code: guard-main не даёт изменить main, master и detached HEAD в обход PR, require-plan +не даёт править код без плана или маркера `.bounded`. Хуки решают по цели записи и проверяются регрессионным корпусом +реальных команд. + +## Requirements + +### Requirement: Решение по цели записи, а не по cwd +guard-main SHALL блокировать вызов только тогда, когда хотя бы одна цель записи лежит в git-репозитории, текущая ветка +которого защищена. cwd сессии MUST использоваться только как база для относительных путей и как начальный каталог +разбора команды. Вызов без целей записи MUST пропускаться при любом cwd. + +#### Scenario: Команда только чтения при cwd на main +- **WHEN** cwd — репозиторий на main, команда `grep -rn "npm i\|npm install" docs 2>/dev/null | head` +- **THEN** guard-main завершается с кодом 0 + +#### Scenario: Редирект в /dev/null и дублирование дескриптора +- **WHEN** cwd — репозиторий на main, команда `git fetch origin --prune 2>&1 | tail -3` или `ls x 2>/dev/null` +- **THEN** guard-main завершается с кодом 0 + +#### Scenario: Edit файла вне любого репозитория +- **WHEN** cwd — репозиторий на main, Write в файл `~/.claude/projects//memory/x.md` +- **THEN** guard-main завершается с кодом 0 + +#### Scenario: Edit файла в worktree на ветке при cwd на main +- **WHEN** cwd — основной checkout на main, Edit файла внутри worktree на ветке `feat/6-x` +- **THEN** guard-main завершается с кодом 0 + +#### Scenario: Edit файла в репозитории на main +- **WHEN** Edit файла внутри репозитория, текущая ветка которого main, при любом cwd +- **THEN** guard-main завершается с кодом 2 + +#### Scenario: Запись через симлинк, указывающий в main +- **WHEN** Write в симлинк вне репозитория, цель которого — ещё не созданный файл в репозитории на main +- **THEN** guard-main завершается с кодом 2 + +### Requirement: Защищённые состояния ветки +Защищёнными SHALL считаться ветки `main` и `master` и состояние detached HEAD. Любая другая ветка MUST считаться +рабочей. + +#### Scenario: Запись в репозитории на master +- **WHEN** Write в файл репозитория на `master` +- **THEN** guard-main завершается с кодом 2 + +#### Scenario: Запись при detached HEAD +- **WHEN** Write в файл репозитория в состоянии detached HEAD +- **THEN** guard-main завершается с кодом 2 + +### Requirement: Редирект как файловая цель +guard-main SHALL считать файловой целью файл после операторов перенаправления вывода (`>`, `>>`, `>|`, `&>`, `N>`, +`N>>`). `/dev/null`, `/dev/stdout`, `/dev/stderr`, `/dev/tty` и дублирование дескриптора `N>&M` MUST NOT считаться +целями. + +#### Scenario: Запись редиректом в main +- **WHEN** cwd — репозиторий на main, команда `echo x > notes.md` +- **THEN** guard-main завершается с кодом 2 + +#### Scenario: stderr в файл в main +- **WHEN** cwd — репозиторий на main, команда `make 2>build.log` +- **THEN** guard-main завершается с кодом 2 + +### Requirement: Команды записи как файловые цели +guard-main SHALL считать файловыми целями пути-аргументы известных команд записи: `tee`, `touch`, `mkdir`, `rm`, +`rmdir`, `truncate`, `chmod`, `unlink`, все пути `mv`, назначение `cp`/`install`/`ln`/`rsync`, файлы `sed -i` и +`perl -i`, `dd of=`, каталог `tar -x`/`unzip`/`patch`, текущий каталог для `npm`/`yarn`/`pnpm` +`install|i|ci|add|init`. Аргументы прочих команд MUST NOT считаться целями. + +#### Scenario: sed -i по файлу в main +- **WHEN** команда `sed -i 's/a/b/' <репозиторий на main>/docs/HARNESS.md` +- **THEN** guard-main завершается с кодом 2 + +#### Scenario: BSD sed -i '' по файлу памяти +- **WHEN** cwd — репозиторий на main, команда `sed -i '' 's/a/b/' ~/.claude/projects//memory/x.md` +- **THEN** guard-main завершается с кодом 0 + +#### Scenario: Запись в контейнере не считается локальной +- **WHEN** cwd — репозиторий на main, команда `docker compose exec -T db sh -c 'psql -c "select 1" > /tmp/x'` +- **THEN** guard-main завершается с кодом 0 + +#### Scenario: Установка пакетов Python +- **WHEN** cwd — репозиторий на main, команда `pip install requests` +- **THEN** guard-main завершается с кодом 0 + +#### Scenario: Установка пакетов Node в main +- **WHEN** cwd — репозиторий на main, команда `npm install` +- **THEN** guard-main завершается с кодом 2 + +### Requirement: Каталог внутри команды +guard-main SHALL учитывать `cd`, `pushd` и `popd` внутри команды: они меняют каталог для последующих команд списка, +соединённых `;`, `&&`, `||`. Изменение каталога MUST NOT распространяться через `|` и за пределы `( … )`. Команды +`sh -c`, `bash -c` и `eval` SHALL разбираться рекурсивно, когда они стоят головой команды. + +#### Scenario: cd в worktree и запись +- **WHEN** cwd — основной checkout на main, команда `cd && echo x > f` +- **THEN** guard-main завершается с кодом 0 + +#### Scenario: cd в main и запись +- **WHEN** cwd — worktree на ветке, команда `cd <репозиторий на main> && touch f` +- **THEN** guard-main завершается с кодом 2 + +#### Scenario: Каталог для worktree в не-git workspace +- **WHEN** cwd — каталог workspace вне git, команда `mkdir -p .wt/6-x` +- **THEN** guard-main завершается с кодом 0 + +#### Scenario: Запись внутри bash -c +- **WHEN** cwd — репозиторий на main, команда `bash -c 'echo x > f'` +- **THEN** guard-main завершается с кодом 2 + +### Requirement: Неразрешимая файловая цель пропускается +Если путь файловой цели нельзя определить без выполнения команды (переменная кроме `HOME`, подстановка команды, +`cd -`), guard-main SHALL пропустить такую цель. `~`, `$HOME` и `${HOME}` MUST раскрываться. + +#### Scenario: Цель в переменной +- **WHEN** cwd — репозиторий на main, команда `echo x > "$OUT"` +- **THEN** guard-main завершается с кодом 0 + +#### Scenario: Цель через HOME +- **WHEN** команда `echo x > $HOME/<путь в репозитории на main>/f` +- **THEN** guard-main завершается с кодом 2 + +### Requirement: Git-мутации на защищённой ветке +guard-main SHALL блокировать git-мутации — `commit`, `merge`, `rebase`, `cherry-pick`, `revert`, `reset`, `am`, +`apply` (кроме `--check`/`--stat`), `add`, `rm`, `mv`, `restore`, `stash` (кроме `list`/`show`), `checkout -- <пути>` — в репозитории на +защищённой ветке. Репозиторий MUST определяться по `git -C `, иначе по текущему каталогу разбора. Прочие +подкоманды, включая `worktree`, `switch`, `checkout -b`, `branch`, `fetch`, `pull`, MUST NOT считаться мутациями. + +#### Scenario: Коммит в main +- **WHEN** cwd — репозиторий на main, команда `git add -A && git commit -m x` +- **THEN** guard-main завершается с кодом 2 + +#### Scenario: Коммит в worktree через -C при cwd на main +- **WHEN** cwd — основной checkout на main, команда `git -C commit -m x` +- **THEN** guard-main завершается с кодом 0 + +#### Scenario: Создание worktree через -C +- **WHEN** cwd — workspace вне git, команда `git -C <репозиторий на main> worktree add ../.wt/x -b feat/x` +- **THEN** guard-main завершается с кодом 0 + +#### Scenario: Обновление main после merge +- **WHEN** cwd — репозиторий на main, команда `git pull --ff-only` +- **THEN** guard-main завершается с кодом 0 + +#### Scenario: Список stash на main +- **WHEN** cwd — репозиторий на main, команда `git stash list` +- **THEN** guard-main завершается с кодом 0 + +### Requirement: Смена ветки внутри команды +`git switch -c|-C <ветка>`, `git switch <ветка>`, `git checkout -b|-B <ветка>` и `git checkout main|master` SHALL +менять ветку репозитория для последующих целей той же команды. Если новую ветку нельзя определить (`-`, переменная), +последующие git-мутации в этом репозитории MUST блокироваться, а файловые цели — пропускаться. + +#### Scenario: Архивная ветка и коммит в одной команде +- **WHEN** cwd — репозиторий на main, команда `git switch -q -c chore/archive-x && git add openspec && git commit -m a` +- **THEN** guard-main завершается с кодом 0 + +#### Scenario: Смена ветки и запись +- **WHEN** cwd — репозиторий на main, команда `git switch -q -c feat/16 && touch .bounded` +- **THEN** guard-main завершается с кодом 0 + +#### Scenario: Ветка в переменной и коммит +- **WHEN** cwd — репозиторий на main, команда `git switch "$B" && git commit -m x` +- **THEN** guard-main завершается с кодом 2 + +#### Scenario: checkout main и merge из worktree +- **WHEN** cwd — worktree на ветке, команда `git checkout main && git pull && git merge --no-ff feat/x && git push` +- **THEN** guard-main завершается с кодом 2 + +#### Scenario: Переключение на предыдущую ветку +- **WHEN** cwd — worktree на ветке, команда `git switch - && git merge x` +- **THEN** guard-main завершается с кодом 2 + +### Requirement: Префиксы команды +Голова команды SHALL определяться после присваиваний `VAR=x` и префиксов `sudo`, `env`, `nice`, `timeout`, `ionice`, +`stdbuf`, `xargs`, `nohup`, `command`, `exec`, `time`, включая значения их опций (`nice -n 5`, `sudo -u u`, +`timeout -s KILL 5`). `env -C <каталог>` MUST менять каталог только для этой команды. + +#### Scenario: timeout перед push в main +- **WHEN** cwd — worktree на ветке, команда `timeout 60 git push origin main` +- **THEN** guard-main завершается с кодом 2 + +#### Scenario: nice -n перед коммитом в main +- **WHEN** cwd — репозиторий на main, команда `nice -n 5 git commit -m x` +- **THEN** guard-main завершается с кодом 2 + +### Requirement: Команды над самой ссылкой +Для `rm`, `rmdir`, `unlink`, `ln` и `mv` местом записи SHALL считаться сама ссылка (каталог, где она лежит), а не её +цель; для остальных целей ссылка MUST раскрываться. + +#### Scenario: Удаление ссылки в workspace на main-репозиторий +- **WHEN** cwd — каталог workspace вне git, команда `rm /link`, где `link` указывает на репозиторий на main +- **THEN** guard-main завершается с кодом 0 + +#### Scenario: Удаление ссылки внутри main +- **WHEN** cwd — репозиторий на main, команда `rm LINK.md`, где `LINK.md` — ссылка наружу +- **THEN** guard-main завершается с кодом 2 + +### Requirement: Git-мутация с неопределённым репозиторием блокируется +Если для git-мутации нельзя определить репозиторий (`-C` с переменной, `--git-dir`, `--work-tree`, неразрешимый +текущий каталог), guard-main SHALL заблокировать вызов и в сообщении предложить `git -C <абсолютный путь>`. + +#### Scenario: Репозиторий в переменной +- **WHEN** команда `git -C "$REPO" commit -m x` +- **THEN** guard-main завершается с кодом 2, сообщение содержит `git -C` + +### Requirement: push проверяется по целевой ветке +`git push` SHALL считаться мутацией защищённой ветки, только если он обновляет её на удалённом репозитории: push без +refspec с текущей защищённой ветки, refspec `HEAD`, `main`, `master` или `<что угодно>:main|master`, флаги `--all` и +`--mirror`. Push и удаление других веток MUST пропускаться при любом cwd. + +#### Scenario: push с main +- **WHEN** cwd — репозиторий на main, команда `git push` +- **THEN** guard-main завершается с кодом 2 + +#### Scenario: Удаление ветки фичи из main-checkout +- **WHEN** cwd — репозиторий на main, команда `git push origin --delete feat/6-x` +- **THEN** guard-main завершается с кодом 0 + +#### Scenario: push ветки фичи из main-checkout +- **WHEN** cwd — репозиторий на main, команда `git push origin feat/6-x` +- **THEN** guard-main завершается с кодом 0 + +#### Scenario: push в main из worktree +- **WHEN** cwd — worktree на ветке, команда `git push origin HEAD:main` +- **THEN** guard-main завершается с кодом 2 + +### Requirement: Текущая ветка и теги в push +Refspec-подстановка текущей ветки (`$(git branch --show-current)`, `$(git rev-parse --abbrev-ref HEAD)`, +`$(git symbolic-ref --short HEAD)`) SHALL считаться push текущей ветки. `git push --tags` без refspec MUST NOT +считаться push ветки. + +#### Scenario: push текущей ветки через подстановку +- **WHEN** cwd — worktree на ветке, команда `git push -u origin "$(git branch --show-current)"` +- **THEN** guard-main завершается с кодом 0 + +#### Scenario: push тегов с main +- **WHEN** cwd — репозиторий на main, команда `git push --tags` +- **THEN** guard-main завершается с кодом 0 + +### Requirement: Без исключения для OpenSpec +guard-main SHALL применять одни и те же правила к изменениям в `openspec/`, включая `openspec archive`: архив OpenSpec +делается отдельным PR (решение владельца 11.10.2026). + +#### Scenario: Коммит только openspec/ в main +- **WHEN** в репозитории на main в индексе только файлы `openspec/`, команда `git commit -m archive` +- **THEN** guard-main завершается с кодом 2 + +#### Scenario: openspec archive в main +- **WHEN** cwd — репозиторий на main, команда `openspec archive x` +- **THEN** guard-main завершается с кодом 2 + +### Requirement: Сбой разбора не блокирует +Любой сбой — невалидный JSON на входе, отсутствие `python3`, внутренняя ошибка разбора — SHALL приводить к пропуску +вызова (код 0) обоими хуками. + +#### Scenario: Невалидный вход +- **WHEN** на stdin хука подан текст `not json` +- **THEN** хук завершается с кодом 0 + +#### Scenario: Нет python3 +- **WHEN** `python3` отсутствует в PATH +- **THEN** хук завершается с кодом 0 + +### Requirement: Сообщение блокировки +При блокировке хук SHALL писать в stderr одну строку, которая начинается с `BLOCKED [guard-main]:` или +`BLOCKED [require-plan]:` и называет цель (путь или git-команду), корень репозитория и действие для выхода. + +#### Scenario: Сообщение guard-main +- **WHEN** guard-main блокирует Write в `docs/HARNESS.md` репозитория на main +- **THEN** stderr начинается с `BLOCKED [guard-main]:` и содержит `docs/HARNESS.md`, путь корня и `worktree` + +### Requirement: require-plan по репозиторию правимого файла +require-plan SHALL определять корень, ветку, `.claude/code-paths`, `.bounded` и каталог планов +`docs/superpowers/plans/` по git-репозиторию правимого файла, а не по cwd. Файл вне репозитория MUST пропускаться. + +#### Scenario: Файл в другом репозитории без плана +- **WHEN** cwd — репозиторий A с `.bounded`, Edit файла `src/a.py` в репозитории B без плана и без `.bounded` +- **THEN** require-plan завершается с кодом 2 + +#### Scenario: Файл в worktree с .bounded при cwd на main +- **WHEN** cwd — основной checkout, Edit файла `src/a.py` в worktree, в корне которого есть `.bounded` +- **THEN** require-plan завершается с кодом 0 + +#### Scenario: Имя файла со скобками — литерал +- **WHEN** Edit файла `src/[slug].ts` в репозитории без плана и без `.bounded` +- **THEN** require-plan завершается с кодом 2 + +#### Scenario: code-paths из репозитория файла +- **WHEN** в репозитории файла `.claude/code-paths` содержит `hooks/`, Edit файла `hooks/x.sh` без плана +- **THEN** require-plan завершается с кодом 2 + +### Requirement: Правила require-plan, которые не меняются +require-plan SHALL защищать по умолчанию любой каталог `src/` на любой глубине, а при наличии `.claude/code-paths` — +только перечисленные префиксы. Правка разрешается при `.bounded` в корне, при плане, где упомянута ветка, при +единственном плане в каталоге планов и при detached HEAD, если план есть. + +#### Scenario: src на глубине без плана +- **WHEN** Edit файла `api/src/a.py` в репозитории без планов и без `.bounded` +- **THEN** require-plan завершается с кодом 2 + +#### Scenario: План упоминает ветку +- **WHEN** в каталоге планов два плана, один из них содержит имя текущей ветки +- **THEN** require-plan завершается с кодом 0 + +### Requirement: Регрессионный корпус хуков +Пакет SHALL содержать корпус вызовов хуков из реальных транскриптов и живых случаев; каждая запись MUST иметь +идентификатор, ожидаемый вердикт `allow|block` и код класса. Корпус MUST прогоняться через хуки в песочнице с раскладкой +репозиториев из корпуса: тестами пакета по источникам и `doctor.sh` по установленным копиям. + +#### Scenario: Расхождение в тестах пакета +- **WHEN** хук из `hooks/` выносит по записи корпуса вердикт, отличный от ожидаемого +- **THEN** `bash tests/run.sh` печатает FAIL с идентификатором и классом записи и завершается с ненулевым кодом + +#### Scenario: Отчёт doctor +- **WHEN** запущен `doctor.sh` при установленных хуках +- **THEN** в разделе самотеста есть строка `корпус хуков: <совпало>/<всего>`, расхождения перечислены с идентификаторами + +### Requirement: Выгрузка только настоящих блокировок +Экстрактор блокировок из транскриптов SHALL выгружать только результаты инструмента, которые являются ошибкой хука, и +MUST NOT выгружать обычный вывод команды, в котором встречается слово `BLOCKED`. + +#### Scenario: cat исходника хука +- **WHEN** в транскрипте результат `cat hooks/require-plan.sh` содержит строку `BLOCKED: editing` +- **THEN** экстрактор эту запись не выгружает