diff --git a/CHANGELOG.md b/CHANGELOG.md index 95503c3..8d81cd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # 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/`. +- **Несовместимо:** исключение для 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` показывает, какие цели видит хук. +- Сообщения блокировки: `BLOCKED [guard-main]:` / `BLOCKED [require-plan]:`, цель, корень репозитория, выход. +- Регрессионный корпус хуков (B2, RT-6): 126 записей из транскриптов пилота + 3 живых случая, размечены по спеке (`docs/retro/scripts/hook_cases_build.py`), прогон в песочнице — `tests/test-hook-corpus.sh` по источникам и строка «корпус хуков» в `doctor.sh` по установленным копиям. В песочнице старые хуки дают на корпусе 41 блок (37 ложных), новые — 4 (архивные коммиты в main). +- `hook_blocks_export.py` и `retro_blocks.py` считают только ошибки хуков: из 126 выгруженных записей 49 оказались выводом команд со словом BLOCKED (O-1), счёт RT-1 в ретро завышен. +- Тесты: `test-hook-parse`, `test-hook-guard-main`, `test-hook-require-plan`, `test-hook-corpus`, `test-hook-export`. + ## 0.3.0 — 2026-10-11 - Роли сессии `dev|admin|analyst|qa`: общий слой `roles/.md` (`~/.claude/roles/`), проектный слой `/.claude/roles/.md`; формат из четырёх разделов (спека `docs/superpowers/specs/2026-10-11-roles-and-memory-design.md`). - Skill `/role `: читает оба слоя, раздел памяти роли, «Читать на старте»; строки `brief:` — фоновому агенту `briefing` (sonnet, только чтение). diff --git a/CLAUDE.md b/CLAUDE.md index b93eee1..99b814a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,17 +4,17 @@ Пакет обвязки для разработки с Claude Code: глобальный `CLAUDE.md` с процессом, три блокирующих хука и неблокирующий `role-hint`, скрипты подключения проекта и skill диагностики. Для одного разработчика, который ведёт фичи через superpowers, хранит задачи в Gitea и требования в OpenSpec. Главный сценарий: `bash install.sh` ставит глобальную часть в `~/.claude`, `adopt-project.sh` подключает существующий проект, `new-project.sh` создаёт новый; дальше проект живёт по `docs/HARNESS.md`. Репозиторий — источник; установленные копии в `~/.claude` по месту не правятся. ## Стек и ограничения -- Bash (`set -euo pipefail`) и markdown. JSON из stdin хуков разбирается встроенным `python3`; других зависимостей времени выполнения нет. Внешние CLI (`tea`, `openspec`, `codegraph`) проверяются, но не требуются для установки. +- Bash (`set -euo pipefail`), markdown и `python3` stdlib (3.8+). Логика guard-main и require-plan — `hooks/harness_target.py`, их `.sh` — обёртки; в остальных хуках и скриптах JSON разбирается встроенным `python3`. Других зависимостей времени выполнения нет. Внешние CLI (`tea`, `openspec`, `codegraph`) проверяются, но не требуются для установки. - Хук блокирует вызов только через `exit 2` с сообщением в stderr; `exit 0` — пропустить. Любой сбой разбора входа должен приводить к пропуску, не к блокировке. - `install.sh` идемпотентен: повторный запуск не ломает `settings.json` и делает бэкапы `CLAUDE.md` и `settings.json` с меткой времени. - Хуки работают из `~/.claude/hooks/`, пути в `settings.hooks.json` через `$HOME`; `CLAUDE_CONFIG_DIR` переопределяет `~/.claude`. - macOS и Linux; `python3` должен быть в PATH сессии Claude Code. ## Архитектура -- `hooks/` — поведение обвязки, единственный «код» с логикой. `guard-main.sh` (PreToolUse на Edit/Write/MultiEdit/Bash): запрет записи на main/master/detached HEAD, исключение для openspec-only коммитов. `require-plan.sh` (PreToolUse на Edit/Write/MultiEdit): правки в каталогах кода только при плане для ветки или файле `.bounded`. `harness-check.sh` (SessionStart): проверка, что проект подключён. `role-hint.sh` (SessionStart, все источники): подсказка роли по `session_title`. `settings.hooks.json` — блок, который `install.sh` сливает в `~/.claude/settings.json`. +- `hooks/` — поведение обвязки, единственный «код» с логикой. `guard-main.sh` (PreToolUse на Edit/Write/MultiEdit/Bash): запрет записи в репозиторий на main/master/detached HEAD по цели записи (файл, цели команды Bash, git-мутации), без исключений. `require-plan.sh` (PreToolUse на Edit/Write/MultiEdit): правки в каталогах кода репозитория файла только при плане для ветки или файле `.bounded`. Обе — обёртки над `harness_target.py`: `parse` (команда → цели, без ФС) → `resolve` (путь → корень и ветка) → `decide`; режим `targets` — отладка разбора. `harness-check.sh` (SessionStart): проверка, что проект подключён. `role-hint.sh` (SessionStart, все источники): подсказка роли по `session_title`. `settings.hooks.json` — блок, который `install.sh` сливает в `~/.claude/settings.json`. - `install.sh`, `adopt-project.sh`, `new-project.sh` — установка и бутстрап. `install.sh` копирует `global/`, `hooks/`, `project-template/`, `adopt-project.sh`, `new-project.sh` и `skills/harness-doctor` в `~/.claude/{CLAUDE.md,hooks,harness,skills}`. `adopt-project.sh` работает на ветке `chore/harness-init` и вливает её ff в текущую ветку; это единственная разрешённая запись на main вне PR. - `global/CLAUDE.md` — глобальные правила процесса, ставятся в `~/.claude/CLAUDE.md`. `project-template/` — `CLAUDE.md`, `CLAUDE.harness-section.md` (секция «OpenSpec и superpowers», по ней `harness-check` узнаёт подключённый проект), `ARCHITECTURE.md`, `gitignore`. -- `skills/harness-doctor/` — skill диагностики: `scripts/doctor.sh` снимает состояние машины и проекта и прогоняет самотест хуков; `SKILL.md` — чеклист прочтения отчёта. +- `skills/harness-doctor/` — skill диагностики: `scripts/doctor.sh` снимает состояние машины и проекта и прогоняет самотест хуков; `SKILL.md` — чеклист прочтения отчёта. `fixtures/hook-cases.jsonl` + `scripts/hook_corpus.py` — регрессионный корпус хуков (реальные команды с ожидаемым вердиктом, прогон в песочнице); лежит в skill, чтобы `doctor.sh` гонял его по установленным копиям. - `roles/` — общий слой ролей (формат: четыре раздела, см. спеку ролей), `skills/role/` — skill `/role`, `agents/briefing.md` — агент выжимки, `link-roles.sh` — симлинки workspace → репозитории направлений. Ставятся `install.sh` в `~/.claude/{roles,skills/role,agents,harness}`. - `tests/` — тесты пакета на bash: `run.sh` + `test-*.sh`, работают с источниками, `install.sh` ставят во временный `CLAUDE_CONFIG_DIR`. - `caveman/config.json` — выключенный по умолчанию caveman. `examples/` — реальный `CLAUDE.md` проекта-пилота. @@ -28,15 +28,20 @@ - `bash skills/harness-doctor/scripts/doctor.sh [проект]` — отчёт и самотест хуков. Только чтение. - `bash adopt-project.sh <папка> [--no-merge] [--push]` — подключить существующий проект. Из терминала, не из Claude Code. - `bash new-project.sh <папка> [ssh-url]` — каркас нового проекта. -- `python3 docs/retro/scripts/retro_stats.py <каталог транскриптов>` и `retro_blocks.py` — статистика сессий и блокировок хуков для ретро. +- `python3 docs/retro/scripts/retro_stats.py <каталог транскриптов>` и `retro_blocks.py` — статистика сессий и блокировок хуков для ретро; `hook_blocks_export.py <каталог> [...]` — выгрузка блокировок в JSONL (сырьё для тестов хуков); `hook_cases_build.py > skills/harness-doctor/fixtures/hook-cases.jsonl` — сборка размеченного корпуса из выгрузки. +- `python3 skills/harness-doctor/scripts/hook_corpus.py hooks [--summary]` — корпус хуков по источникам. `python3 hooks/harness_target.py targets '<команда>' [cwd]` — какие цели записи видит хук. ## Тесты - `tests/run.sh` — автоматические тесты источников (хуки через фиктивный JSON, скрипты во временных каталогах, размеры). Самотест хуков в `doctor.sh` проверяет установленные копии; перед ним `bash install.sh`. - Ручная проверка хука: на main в любом репозитории попросить агента создать файл, ожидается `BLOCKED`. -- `tests/` покрывает `role-hint` и скрипты `install.sh`, `doctor.sh`, `link-roles.sh`; регрессионные тесты `guard-main` и `require-plan` на реальных командах из транскриптов (бэклог ретро B2) пока не сделаны. +- Хуки: `test-hook-parse` — разбор команды на цели; `test-hook-guard-main`, `test-hook-require-plan` — сценарии спеки `hook-guards` в песочнице из настоящих репозиториев; `test-hook-corpus` — корпус реальных команд (assert на запись, корпус должен совпадать со сборкой `hook_cases_build.py`); `test-hook-export` — экстрактор. Плюс `role-hint` и скрипты `install.sh`, `doctor.sh`, `link-roles.sh`. +- Правишь разбор или вердикт — сначала случай в `test-hook-parse`/`test-hook-guard-main`; меняется ожидаемый вердикт реальной команды — разметка в `hook_cases_build.py` и пересборка корпуса. ## Текущая работа -Бэклог обвязки — `docs/retro/2026-10-11.md`, раздел 4 (B1–B12), решения владельца — раздел 5. Порядок: B1+B2 (guard-main по цели записи, тесты хуков), затем B5+B6 (роли и память, спека `docs/superpowers/specs/2026-10-11-roles-and-memory-design.md`), затем B3/B4/B10. Находки ретро маркируются `RT-n`. +Бэклог обвязки — `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`. +- Дальше: B3/B4/B10, B12. ## OpenSpec и superpowers - Вход в любую задачу — superpowers:brainstorming. Skills openspec-propose и openspec-explore сами по себе не запускать. diff --git a/README.md b/README.md index a405a70..75989c3 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,9 @@ ``` global/CLAUDE.md глобальные правила процесса → ~/.claude/CLAUDE.md -hooks/guard-main.sh запрет изменений на main/detached HEAD (Edit/Write/Bash); исключение — коммиты только в openspec/ -hooks/require-plan.sh правки в src/ только при наличии плана или файла .bounded +hooks/guard-main.sh запрет изменений на main/master/detached HEAD (Edit/Write/Bash) по цели записи, без исключений +hooks/require-plan.sh правки в src/ (или .claude/code-paths) только при плане для ветки или файле .bounded +hooks/harness_target.py ядро обоих хуков: цели записи команды → репозиторий и ветка каждой цели hooks/settings.hooks.json блок PreToolUse для ~/.claude/settings.json caveman/config.json defaultMode: off → ~/.config/caveman/config.json project-template/ CLAUDE.md, ARCHITECTURE.md, gitignore для нового проекта diff --git a/docs/HARNESS.md b/docs/HARNESS.md index 5ea7820..e04c412 100644 --- a/docs/HARNESS.md +++ b/docs/HARNESS.md @@ -69,13 +69,15 @@ Fix loop: раунды 1–3 продолжают того же implementer, 4 | Правило | Файл | Механизм | |---|---|---| -| Никаких изменений на main / detached HEAD | `guard-main.sh`, PreToolUse на Edit/Write/MultiEdit/Bash | проверяет ветку; для Bash блокирует команды, похожие на запись (`>`, `tee`, `sed -i`, `mv`, `rm`, `git commit/push`…), пропускает `git worktree add`; коммит/push в main разрешены только если изменены исключительно файлы в `openspec/` (архивация) | -| Правки в коде только при плане или `.bounded` | `require-plan.sh`, PreToolUse на Edit/Write/MultiEdit | защищает любой `src/` на любой глубине (`api/src/`, `packages/*/src/`) или каталоги из `.claude/code-paths`; ищет план в `docs/superpowers/plans/` для текущей ветки или файл `.bounded` | +| Никаких изменений на main / master / detached HEAD | `guard-main.sh` → `harness_target.py`, PreToolUse на Edit/Write/MultiEdit/Bash | решает по цели записи, а не по cwd. Цели: файл Edit/Write; в Bash — редиректы (кроме `/dev/null` и `N>&M`), `tee`, `rm`, `mv`, `cp`, `sed -i`, `npm install`, `openspec archive`… с учётом `cd` и `git -C`; git-мутации (`commit`, `add`, `merge`, `reset`…; `push` — только если пушит в main; `switch -c` в той же команде меняет ветку). Цель вне репозитория или в worktree на ветке — пропуск; неразрешимый путь (`$VAR`) — пропуск; git-мутация с неопределённым репозиторием — блок. Исключений нет: архив OpenSpec — отдельным PR | +| Правки в коде только при плане или `.bounded` | `require-plan.sh` → `harness_target.py`, PreToolUse на Edit/Write/MultiEdit | по репозиторию правимого файла, а не по cwd: защищает любой `src/` на любой глубине (`api/src/`, `packages/*/src/`) или каталоги из `.claude/code-paths`; ищет план в `docs/superpowers/plans/` для ветки этого репозитория или файл `.bounded` в его корне | | Обвязка подключена к проекту | `harness-check.sh`, SessionStart | при старте сессии проверяет `openspec/`, `.codegraph/`, секцию в CLAUDE.md; если чего-то нет — пишет агенту инструкцию запустить `~/.claude/harness/adopt-project.sh` (спросив пользователя; `HARNESS_AUTO_ADOPT=1` — без вопроса) | | Подсказка роли по заголовку сессии | `role-hint.sh`, SessionStart (все источники) | читает `session_title`; если начинается с «dev, admin, analyst или qa» — одна строка-подсказка вызвать `/role`; ничего не блокирует | | Тесты зелёные перед коммитом | git pre-commit в проекте (не входит в пакет) | обычный git-хук с командой тестов | -Exit 2 блокирует вызов и показывает агенту stderr, поэтому агент сам делает то, что просит сообщение. Оба обхода, замеченные на пилоте (запись через `echo >` вместо Write; push через detached HEAD), закрыты. +Exit 2 блокирует вызов и показывает агенту stderr, поэтому агент сам делает то, что просит сообщение. Сообщение начинается с `BLOCKED [guard-main]:` или `BLOCKED [require-plan]:` и называет цель, репозиторий и выход. Оба обхода, замеченные на пилоте (запись через `echo >` вместо Write; push через detached HEAD), закрыты. + +Регрессия хуков — корпус `skills/harness-doctor/fixtures/hook-cases.jsonl`: реальные команды из транскриптов пилота с ожидаемым вердиктом. По источникам — `bash tests/run.sh hook`, по установленным копиям — строка «корпус хуков» в `doctor.sh`. Корпус собирается из сырой выгрузки `docs/retro/scripts/hook_cases_build.py`; разметка — в этом скрипте. Разбор одной команды: `python3 ~/.claude/hooks/harness_target.py targets '<команда>' `. ## Дисциплина между сессиями diff --git a/docs/retro/fixtures/hook-blocks.jsonl b/docs/retro/fixtures/hook-blocks.jsonl new file mode 100644 index 0000000..5175b6d --- /dev/null +++ b/docs/retro/fixtures/hook-blocks.jsonl @@ -0,0 +1,126 @@ +{"session": "0d5648e2", "ts": "2026-10-06T13:14:14.950Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cat /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/jetstream-check-later.md /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "0e94130b", "ts": "2026-10-08T14:17:06.073Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "docker compose -f ../docs/dev/docker-compose.yml exec -T postgres sh -c 'psql -U \"$POSTGRES_USER\" -d \"$POSTGRES_DB\" -tAc \"set search_path=dkb; select (select count(*) from consolidation_version), (select count(*) from stored_file), (select count(*) from company_group), (select count(*) from app_user)\"'; T=$(curl -s -X POST localhost:8000/auth/dev-login -H 'content-type: application/json' -d '{\"email\":\"analyst@armka.local\"}' 2>/dev/null | python3 -c 'import sys,json;print(json.load(sys.stdin).get(\"access_token\",\"\"))' 2>/dev/null); echo \"token: curl -s -o /dev/null -w '%{http_code}\\n' localhost:3000/", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (docker compose -f ../docs/dev/docker-compose.yml exec -T postgres sh -c 'psql -U). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "13081199", "ts": "2026-10-08T09:00:56.719Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca; grep -n \"DEBUG\" arm-core/api/src/core/config.py scan-gateway/api/src/core/config.py tabular-parser/api/src/core/config.py; echo ---; sed -n 36,70p scan-gateway/api/src/app.py; echo; grep -n \"get_settings()\\|def create_app\\|^app = \\|FastAPI(\" scan-gateway/api/src/app.py arm-core/api/src/app.py; grep -rn \"setup_logger\\|diagnose\" tabular-parser/api/tests | head; cat ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "1d7198e6", "ts": "2026-10-09T18:35:23.443Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory && cat arm-web-repo.md arm-web-demo-prototype.md pdfjs-legacy-yandex.md contour-back-dev-login.md snapshot-nested-75.md workspace-hooks-and-worktrees.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "26b1e4e5", "ts": "2026-10-07T18:06:54.767Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core/api", "branch": "HEAD", "tool": "Bash", "command": "git -C .. log --oneline -8 main; ls migrations/versions 2>/dev/null || find . -path ./.venv -prune -o -name \"versions\" -type d -print; ", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (git -C .. log --oneline -8 main; ls migrations/versions 2>/dev/null || find . -p). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "26b1e4e5", "ts": "2026-10-07T18:07:01.108Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core/api", "branch": "HEAD", "tool": "Bash", "command": "grep -rln \"legal_entity\" /Users/andrey.epifancev/projects/awp-ca/arm-core/api/src/db/migrations/versions /Users/andrey.epifancev/projects/awp-ca/arm-core/api/migrations/versions 2>/dev/null | xargs ls -t 2>/dev/null | head -5", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (grep -rln \"legal_entity\" /Users/andrey.epifancev/projects/awp-ca/arm-core/api/sr). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "30bc3fca", "ts": "2026-10-08T18:08:27.859Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca/arm-core/api; grep -rn \"404\" src/api/dependencies src/api/endpoints/me.py src/core/exception_handlers.py | head -20; grep -n \"root_path\\|ROOT_PATH\\|mount\\|web_dir\" src/app.py src/core/config.py | head -20; grep -rn \"ROOT_PATH\\|API_URL\" ../web/rsbuild.config.ts ../web/.env* ../Dockerfile* ../web/Dockerfile* 2>/dev/null | head", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca/arm-core/api; grep -rn \"404\" src/api/). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "30bc3fca", "ts": "2026-10-08T18:14:42.003Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca/arm-core; grep -rhoE \"[\\`'](/(admin|me|directories|documents|files|consolidations|company-groups|legal-entities|dev|users|statements|osv)[^\\`'?]*)\" web/src | sed -E \"s/^[\\`']//\" | sort -u > $CLAUDE_JOB_DIR/tmp/web_paths.txt; cat $CLAUDE_JOB_DIR/tmp/web_paths.txt; echo ----; cd api && .venv/bin/python -c \"\nfrom src.app import create_app\nfrom src.core.config import settings\napp=create_app(settings)\nfor r in app.routes:\n p=getattr(r,'path',None); m=getattr(r,'methods',None)\n if p and p.startswith('/api'): print(sorted(m or []), p)\n\" 2>&1 | sort -k2", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca/arm-core; grep -rhoE \"[\\`'](/(admin|m). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "34b466b5", "ts": "2026-10-09T16:18:39.133Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "ls; grep -rn \"dev/token\\|AUTH_DEV_ISSUER\\|auth_dev_issuer\" --include='*.py' . 2>/dev/null | grep -v \"\\.wt\\|tests/\" | head -15; echo ---GATE; cat web/src/auth/AuthGate.tsx", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (ls; grep -rn \"dev/token\\|AUTH_DEV_ISSUER\\|auth_dev_issuer\" --include='*.py' . 2>). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "34b466b5", "ts": "2026-10-09T16:24:03.128Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cat ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md; cat ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/arm-web-repo.md | head -30", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "433a59c7", "ts": "2026-10-06T12:45:57.070Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cat \"/Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md\"; cd /Users/andrey.epifancev/projects/awp-ca && git -C docs status --short | head && git -C docs branch --show-current && git -C docs worktree list && ls .wt 2>/dev/null", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "4cb93cc7", "ts": "2026-10-09T15:04:10.690Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core/api", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca && grep -rn \"CONTENT_TYPES\" arm-core/api/src --include=*.py | head -3; grep -n \"CONTENT_TYPES\" -A12 arm-core/api/src/domain/file_types.py; echo \"=== usage SourcePreview ===\"; grep -rn \"SourcePreview\\|fileId=\" arm-web/src --include=*.tsx | grep -v \"SourcePreview.tsx\" | head; echo \"=== miniopy pin ===\"; grep -n \"miniopy\\|aiohttp\" arm-core/api/pyproject.toml arm-core/api/requirements*.txt arm-core/api/uv.lock 2>/dev/null | head; echo \"=== S3 settings ===\"; grep -n \"S3\\|MINIO\\|OPENBAO\" arm-core/api/src/core/config.py | head -40; echo \"=== контур docs ===\"; grep -rln \"контур\" docs/dev/*.md | head", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca && grep -rn \"CONTENT_TYPES\" arm-core/). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "4cb93cc7", "ts": "2026-10-09T15:04:23.215Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core/api", "branch": "HEAD", "tool": "Bash", "command": "cat ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/feedback-no-harness-bypass.md ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "5bbf3523", "ts": "2026-10-08T12:51:16.774Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory; cat workspace-hooks-and-worktrees.md contour-transfer-queue.md logger-diagnose-state.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "6cc6d498", "ts": "2026-10-08T19:39:34.332Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca; cat ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/contour-transfer-queue.md; ls .wt; ls docs/scripts; ls docs/tests 2>/dev/null; grep -rn \"prepare-contour\" docs --include=*.md --include=Makefile -l", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "75185712", "ts": "2026-10-11T12:13:03.949Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca; f=~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/75185712-0f28-4c91-8f7c-a8dc174a367b/tool-results/bb63026cq.txt; grep -n -- '---HOOKS' $f; sed -n \"$(grep -n -- '---HOOKS' $f | cut -d: -f1),\\$p\" $f | head -400", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "75185712", "ts": "2026-10-11T12:29:14.057Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd ~/projects/cc-skills/claude-harness; cat README.md | head -80; echo ---INSTALL; cat install.sh; echo ---TREE; find . -type f | grep -v examples | head -40; echo ---GLOBAL; ls global; echo ---CHANGELOG; head -30 CHANGELOG.md", "file_path": null, "hook": "other", "message": "# Ожидается BLOCKED и предложение создать worktree.", "expected": null} +{"session": "75185712", "ts": "2026-10-11T12:46:03.638Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /private/tmp/claude-501/-Users-andrey-epifancev-projects-awp-ca/75185712-0f28-4c91-8f7c-a8dc174a367b/scratchpad && python3 -I retro_stats.py ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/ > retro_stats.out 2>&1; wc -l retro_stats.out; cat retro_stats.out", "file_path": null, "hook": "guard-main(Bash)", "message": "[0d5648e2,13081199] BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main испо", "expected": null} +{"session": "75185712", "ts": "2026-10-11T12:46:25.920Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cat ~/.claude/hooks/guard-main.sh; echo ---WS; cat /Users/andrey.epifancev/projects/awp-ca/.claude/hooks/guard-main.sh; echo ---RP; cat ~/.claude/hooks/require-plan.sh", "file_path": null, "hook": "guard-main(Bash)", "message": " echo \"BLOCKED: shell write on $label ($(printf '%s' \"$cmd\" | head -c 80)). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly.\" >&2", "expected": null} +{"session": "7580b8ad", "ts": "2026-10-10T06:19:51.216Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "ls -la && echo ---WT--- && ls -la .wt 2>/dev/null && echo ---MEM--- && cat /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "776ede53", "ts": "2026-10-11T20:43:57.614Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "ls; ls -d .codegraph; grep -rln \"consolidated\\|сводн\" --include='*.py' . 2>/dev/null | grep -v node_modules | grep -v tests | head -20", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (ls; ls -d .codegraph; grep -rln \"consolidated\\|сводн\" --include='*.py' . 2>). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directl", "expected": null} +{"session": "776ede53", "ts": "2026-10-12T05:30:13.767Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "grep -n \"@router\\.\\(post\\|put\\|delete\\)\" api/src/api/endpoints/directories/related_groups.py api/src/api/endpoints/directories/company_groups.py api/src/api/endpoints/directories/legal_entities.py | head -20; grep -n \"NATS_ENABLED\\|DEBUG\\|AUTH_DEV\\|S3\\|OPENBAO__\\|DB_SCHEMA\" .wt 2>/dev/null; grep -v '^#' /Users/andrey.epifancev/projects/awp-ca/.wt/web-proto/arm-core/api/.env | grep -v '^$' | sed 's/\\(SECRET\\|PASSWORD\\|KEY\\)=.*/\\1=***/'", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (grep -n \"@router\\.\\(post\\|put\\|delete\\)\" api/src/api/endpoints/directories/relat). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "776ede53", "ts": "2026-10-12T05:31:39.187Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/tabular-parser", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca/.wt/web-proto/arm-core/api && (lsof -nP -iTCP:8000 -sTCP:LISTEN || true) && nohup .venv/bin/python run_server.py > /private/tmp/claude-501/-Users-andrey-epifancev-projects-awp-ca/776ede53-781a-412c-a70e-2dce0962b8fe/scratchpad/arm-core.log 2>&1 &\nsleep 8; curl -s -m 5 localhost:8000/healthcheck; echo; curl -s -m 5 localhost:8001/healthcheck", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca/.wt/web-proto/arm-core/api && (lsof -). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "776ede53", "ts": "2026-10-12T05:36:58.887Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Write", "command": null, "file_path": "/private/tmp/claude-501/-Users-andrey-epifancev-projects-awp-ca/776ede53-781a-412c-a70e-2dce0962b8fe/scratchpad/synth/run.py", "hook": "guard-main(Edit/Write)", "message": "PreToolUse:Write hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: you are on main. Create an isolated worktree first (superpowers:using-git-worktrees), never edit main directly.", "expected": null} +{"session": "778bb3cc", "ts": "2026-10-07T14:40:20.704Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/ && cat workspace-hooks-and-worktrees.md frontend-auth-guide-state.md core-42-front-gaps.md contour-transfer-queue.md audit-until-osv.md qa-local-stand.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "778bb3cc", "ts": "2026-10-07T14:41:37.739Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core/api", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/.claude/jobs/0d5648e2/tmp && cat > dump_openapi.py <<'EOF'\nimport json, sys, os\nsys.path.insert(0, \"/Users/andrey.epifancev/projects/awp-ca/arm-core/api\")\nos.chdir(\"/Users/andrey.epifancev/projects/awp-ca/arm-core/api\")\nfrom src import app\nspec = app.openapi()\njson.dump(spec, open(sys.argv[1], \"w\"), ensure_ascii=False, indent=1)\nfor p, ops in spec[\"paths\"].items():\n for m, op in ops.items():\n print(m.upper(), p, \"|\", op.get(\"summary\",\"\"))\nEOF\n/Users/andrey.epifancev/projects/awp-ca/.wt/kpp/arm-core/api/.venv/bin/python -B dump_openapi.py /Users/andrey.epifancev/.claude/jobs/0d5648e2/tmp/openapi.json 2>&1 | tail -120", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/.claude/jobs/0d5648e2/tmp && cat > dump_openapi.py <<). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "8105dccb", "ts": "2026-10-08T07:39:44.661Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "ls dkb_24.09.26 | head -30; grep -rl \"Тип контрагента\" dkb_24.09.26 2>/dev/null | head", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (ls dkb_24.09.26 | head -30; grep -rl \"Тип контрагента\" dkb_24.09.2). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly.", "expected": null} +{"session": "8105dccb", "ts": "2026-10-08T07:52:37.175Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cat /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/arm-web-repo.md; ls -la /Users/andrey.epifancev/projects/awp-ca/.wt/web-proto/", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "8105dccb", "ts": "2026-10-08T08:41:22.920Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca/.wt/ocr-q1/fixtures && python3 -I - README.md <<'PY'\nimport sys\np=sys.argv[1]; s=open(p,encoding='utf-8').read()\na=\"\"\" aep_6.pdf → aep_6.json (баланс + ОФР, успех); aep_5, aep_8 — ответы-ошибки\n\"\"\"\nb=\"\"\" aep_6.pdf → aep_6.json (баланс + ОФР, успех); aep_5, aep_8 — ответы-ошибки\n ББ/ОФР группы РБ за 1 кв. 2025 (ответы настоящей ИС, прогон тестировщика 08.10.2026; PDF —\n копии из statements/, эмулятор ищет пары только в ocr/): «Бухгалтерская отчетность 1 кв.25»\n (АО РБ) и «БО 1 кв 2025» (Счастливочная) — .pdf/.PDF + .json, баланс + ОФР, needs_check;\n ИС склеила строки 1500 и 1700 в одну ячейку — нормализатор их пока пропускает (см. эталоны)\n\"\"\"\nassert s.count(a)==1; s=s.replace(a,b); open(p,'w',encoding='utf-8').write(s)\nPY\ngit add README.md \"ocr/БО 1 кв 2025.PDF\" \"ocr/БО 1 кв 2025.json\" \"ocr/Бухгалтерская отчетность 1 кв.25.pdf\" \"ocr/Бухгалтерская отчетность 1 кв.25.json\" \"ocr/expected_normalize/БО 1 кв 2025.json\" \"ocr/expected_normalize/Бухгалтерская отчетность 1 кв.25.json\" && git commit -q -F - <<'EOF'\nfixtures(ocr): ответы ИС Сканирование по ББ/ОФР РБ и Счастливочной за 1 кв. 2025 (#15)\n\nПары для эмулятора (поиск по sha256 только в ocr/): PDF скопированы из statements/,\nJSON — элемент files[] ответа настоящей ИС (прогон тестировщика 08.10.2026), file_uid =\nимя набора, как в aep_6.json. Эталоны нормализатора — make golden-update в scan-gateway\n(main 2026-10-08): оба набора needs_check, по 2 документа (BALANCE + INCOME); старые\nэталоны не изменились.\n\nИзвестное ограничение, зафиксированное эталоном: ИС склеила строки 1500 и 1700\nв одну ячейку («1500\\n1700», суммы через \\n) — нормализатор пропускает строку как\n«нечисловой код», в документах нет 1500 и 1700.\n\nCo-Authored-By: Claude Opus 5.5 \nEOF\ngit log --oneline -1; git push -q -u origin feat/15-ocr-2025-q1 2>&1 | tail -1", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on detached HEAD (cd /Users/andrey.epifancev/projects/awp-ca/.wt/ocr-q1/fixtures && python3 -I - R). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land o", "expected": null} +{"session": "8105dccb", "ts": "2026-10-08T08:49:24.280Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "git switch -q -c feat/16-split-merged-codes && touch .bounded && grep -n bounded .gitignore; codegraph init 2>&1 | tail -1; git status -sb | head -3", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on detached HEAD (git switch -q -c feat/16-split-merged-codes && touch .bounded && grep -n bounded). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land o", "expected": null} +{"session": "8105dccb", "ts": "2026-10-08T14:10:53.578Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "git add openspec && git status --short | grep -v \"^[RAMD] *openspec/\" ; git commit -q -F - <<'EOF'\ndocs(openspec): архив auto-confirm-clean-documents — спеки osv-result-mapping, primary-files, audit-trail (#64)\n\nCo-Authored-By: Claude Opus 5.5 \nEOF\ngit log --oneline -1 && git push -q origin main 2>&1 | tail -1", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (git add openspec && git status --short | grep -v \"^[RAMD] *openspec/\" ; git comm). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "8105dccb", "ts": "2026-10-08T14:11:08.484Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "cat ~/.claude/hooks/guard-main.sh | head -120", "file_path": null, "hook": "guard-main(Bash)", "message": " echo \"BLOCKED: shell write on $label ($(printf '%s' \"$cmd\" | head -c 80)). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly.\" >&2", "expected": null} +{"session": "8782a981", "ts": "2026-10-09T10:54:58.961Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/scan-gateway/api", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca && grep -rniE \"smb|register_session\" reference-old-arm --include=*.py --include=*.yml --include=*.yaml --include=*.env* 2>/dev/null | grep -v node_modules | head -30; echo ----; grep -rniE \"SMB_LOGIN|OPENBAO__SMB|smb\" docs --include=*.md -l | head; ", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca && grep -rniE \"smb|register_session\" ). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "8782a981", "ts": "2026-10-09T12:18:48.416Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/scan-gateway", "branch": "HEAD", "tool": "Bash", "command": "cat /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/feedback-execution-mode-from-claude-md.md /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/logger-diagnose-state.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "8782a981", "ts": "2026-10-09T14:19:05.748Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/scan-gateway/api", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca && grep -n \"smbprotocol\" scan-gateway/api/requirements.txt; P=$(find / -path \"*/site-packages/smbclient/_pool.py\" 2>/dev/null | head -1); echo \"$P\"; D=$(dirname $(dirname \"$P\")); ls $D | grep -i \"^smbprotocol-\"; grep -rn \"STATUS_NOT_FOUND\" $D/smbclient $D/smbprotocol | head -20", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca && grep -n \"smbprotocol\" scan-gateway). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "9057cf5f", "ts": "2026-10-07T20:00:38.444Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "ls; cat ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/frontend-api-guide-state.md; tea repos ls --login home 2>/dev/null | head -20", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "9057cf5f", "ts": "2026-10-07T20:00:49.740Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-web", "branch": "HEAD", "tool": "Bash", "command": "git log --stat --format='--- %h %s (%ad)' --date=iso | head -120; cat package.json rsbuild.config.ts; ls src/conf; cat src/conf/* 2>/dev/null | head -40; find src -type f | xargs wc -l | sort -n | tail -70", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (git log --stat --format='--- %h %s (%ad)' --date=iso | head -120; cat package.js). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "925f14ee", "ts": "2026-10-09T15:58:02.871Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/scan-gateway", "branch": "HEAD", "tool": "Bash", "command": "grep -rn -B2 -A8 \"D-6\\|D-7\" openspec/specs/*/spec.md openspec/specs/*.md 2>/dev/null | head -80; echo ----RULES; grep -n -A12 \"^labels\" api/rules/*.yaml 2>/dev/null || grep -rn -A10 \"labels:\" api/src/normalizer/rules/*.yaml api/rules 2>/dev/null | head -40; echo ----RAW; find /Users/andrey.epifancev/projects/awp-ca -path '*f37d677c*' 2>/dev/null | head", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (grep -rn -B2 -A8 \"D-6\\|D-7\" openspec/specs/*/spec.md openspec/specs/*.md 2>/dev/). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "925f14ee", "ts": "2026-10-09T16:02:27.157Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/scan-gateway", "branch": "HEAD", "tool": "Bash", "command": "cat /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md; echo ----; cd /Users/andrey.epifancev/projects/awp-ca/scan-gateway && git worktree list && git status --short | head && git log --oneline -3", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "925f14ee", "ts": "2026-10-09T16:02:31.733Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/scan-gateway", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca && git -C scan-gateway fetch -q origin && git -C scan-gateway status -sb | head -2 && mkdir -p .wt/type-by-codes && git -C scan-gateway worktree add .wt/../.wt/type-by-codes/scan-gateway -b tmp/type-by-codes origin/main 2>&1 | tail -2; ls .wt/type-by-codes", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca && git -C scan-gateway fetch -q origi). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "a0a8c17e", "ts": "2026-10-09T08:54:06.683Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/.wt/stmt-template/arm-web", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/ && cat auto-confirm-64.md jetstream-check-later.md workspace-hooks-and-worktrees.md logger-diagnose-state.md | grep -v '^\\s*$'", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "a22dee1e", "ts": "2026-10-08T07:39:44.661Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/docs/mockup", "branch": "HEAD", "tool": "Bash", "command": "ls dkb_24.09.26 | head -30; grep -rl \"Тип контрагента\" dkb_24.09.26 2>/dev/null | head", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (ls dkb_24.09.26 | head -30; grep -rl \"Тип контрагента\" dkb_24.09.2). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly.", "expected": null} +{"session": "a22dee1e", "ts": "2026-10-08T07:52:37.175Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/.wt/web-proto/arm-web", "branch": "HEAD", "tool": "Bash", "command": "cat /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/arm-web-repo.md; ls -la /Users/andrey.epifancev/projects/awp-ca/.wt/web-proto/", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "a22dee1e", "ts": "2026-10-08T08:41:22.920Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/.wt/ocr-q1/scan-gateway/api", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca/.wt/ocr-q1/fixtures && python3 -I - README.md <<'PY'\nimport sys\np=sys.argv[1]; s=open(p,encoding='utf-8').read()\na=\"\"\" aep_6.pdf → aep_6.json (баланс + ОФР, успех); aep_5, aep_8 — ответы-ошибки\n\"\"\"\nb=\"\"\" aep_6.pdf → aep_6.json (баланс + ОФР, успех); aep_5, aep_8 — ответы-ошибки\n ББ/ОФР группы РБ за 1 кв. 2025 (ответы настоящей ИС, прогон тестировщика 08.10.2026; PDF —\n копии из statements/, эмулятор ищет пары только в ocr/): «Бухгалтерская отчетность 1 кв.25»\n (АО РБ) и «БО 1 кв 2025» (Счастливочная) — .pdf/.PDF + .json, баланс + ОФР, needs_check;\n ИС склеила строки 1500 и 1700 в одну ячейку — нормализатор их пока пропускает (см. эталоны)\n\"\"\"\nassert s.count(a)==1; s=s.replace(a,b); open(p,'w',encoding='utf-8').write(s)\nPY\ngit add README.md \"ocr/БО 1 кв 2025.PDF\" \"ocr/БО 1 кв 2025.json\" \"ocr/Бухгалтерская отчетность 1 кв.25.pdf\" \"ocr/Бухгалтерская отчетность 1 кв.25.json\" \"ocr/expected_normalize/БО 1 кв 2025.json\" \"ocr/expected_normalize/Бухгалтерская отчетность 1 кв.25.json\" && git commit -q -F - <<'EOF'\nfixtures(ocr): ответы ИС Сканирование по ББ/ОФР РБ и Счастливочной за 1 кв. 2025 (#15)\n\nПары для эмулятора (поиск по sha256 только в ocr/): PDF скопированы из statements/,\nJSON — элемент files[] ответа настоящей ИС (прогон тестировщика 08.10.2026), file_uid =\nимя набора, как в aep_6.json. Эталоны нормализатора — make golden-update в scan-gateway\n(main 2026-10-08): оба набора needs_check, по 2 документа (BALANCE + INCOME); старые\nэталоны не изменились.\n\nИзвестное ограничение, зафиксированное эталоном: ИС склеила строки 1500 и 1700\nв одну ячейку («1500\\n1700», суммы через \\n) — нормализатор пропускает строку как\n«нечисловой код», в документах нет 1500 и 1700.\n\nCo-Authored-By: Claude Opus 5.5 \nEOF\ngit log --oneline -1; git push -q -u origin feat/15-ocr-2025-q1 2>&1 | tail -1", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on detached HEAD (cd /Users/andrey.epifancev/projects/awp-ca/.wt/ocr-q1/fixtures && python3 -I - R). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land o", "expected": null} +{"session": "a22dee1e", "ts": "2026-10-08T08:49:24.280Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/.wt/ocr-q1/scan-gateway", "branch": "HEAD", "tool": "Bash", "command": "git switch -q -c feat/16-split-merged-codes && touch .bounded && grep -n bounded .gitignore; codegraph init 2>&1 | tail -1; git status -sb | head -3", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on detached HEAD (git switch -q -c feat/16-split-merged-codes && touch .bounded && grep -n bounded). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land o", "expected": null} +{"session": "adba5129", "ts": "2026-10-11T16:41:25.025Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "ls arm-core/openspec/changes/archive | tail -3; D=$(ls -d arm-core/openspec/changes/archive/* | tail -1); ls -R \"$D\"; echo ----; head -40 \"$D/proposal.md\"; echo ----; head -60 \"$D\"/specs/*/spec.md | head -80; echo ----; grep -n \"^## \" \"$D/design.md\"; echo ----; cat .wt 2>/dev/null; grep -rn \"wt\" jira-hub/.gitignore; cat ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md | head -40", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "b02ed030", "ts": "2026-10-08T19:24:09.355Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/reference-old-arm", "branch": "HEAD", "tool": "Bash", "command": "grep -rnE \"img_uploaded|img_parsed|tabular_uploaded|tabular_parsed|subject\" . 2>/dev/null | grep -v -E \"node_modules|\\.git/\" | cut -c1-250 | head -30", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (grep -rnE \"img_uploaded|img_parsed|tabular_uploaded|tabular_parsed|subject\" . 2>). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "b02ed030", "ts": "2026-10-08T19:28:34.941Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory && cat workspace-hooks-and-worktrees.md logger-diagnose-state.md feedback-no-harness-bypass.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "bce8483e", "ts": "2026-10-10T14:44:11.172Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/docs/context", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca && echo \"=== arm-web branches/log ===\"; git -C arm-web branch -a | head; git -C arm-web log --oneline -15 --all --date=short --format=\"%ad %s\" | head -20; echo \"=== arm-web routes ===\"; grep -rhoE \"path: *['\\\"][^'\\\"]+['\\\"]|]*path=['\\\"][^'\\\"]+['\\\"]\" arm-web/src 2>/dev/null | sort -u | head -40; echo \"=== arm-web pages ===\"; ls arm-web/src/pages 2>/dev/null; ls arm-web/src/app 2>/dev/null | head; echo \"=== memory arm-web ===\"; cat ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/arm-web-repo.md ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/arm-web-demo-prototype.md 2>/dev/null | grep -v \"^---\" | head -80", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca && echo \"=== arm-web branches/log ===). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "bce8483e", "ts": "2026-10-10T14:44:19.830Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/docs/context", "branch": "HEAD", "tool": "Bash", "command": "ls /Users/andrey.epifancev/projects/awp-ca/arm-web/src/pages /Users/andrey.epifancev/projects/awp-ca/arm-web/src 2>/dev/null; grep -rhoE \"path=['\\\"][^'\\\"]+['\\\"]\" /Users/andrey.epifancev/projects/awp-ca/arm-web/src 2>/dev/null | sort -u | head -40", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (ls /Users/andrey.epifancev/projects/awp-ca/arm-web/src/pages /Users/andrey.epifa). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "bce8483e", "ts": "2026-10-10T20:57:19.615Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-web", "branch": "HEAD", "tool": "Bash", "command": "echo \"--- чек-лист/замена/удаление/ЕЦП/история/состав где-либо\"; grep -rln -i \"чек-лист\\|checklist\" src | grep -v test; grep -rn -i \"заменить\\|удалить\\|replace\\|ЕЦП\\|История изменений\\|состав\" src --include=*.tsx 2>/dev/null | grep -v test | cut -c1-140 | head -12; echo \"--- CompanyGroupsTable / ConsoleLegalEntities\"; ls src/widgets/CompanyGroupsTable src/widgets/ConsoleLegalEntities; grep -rn -i \"Modal\\|Drawer\\|Tabs\\|onRow\\|navigate\" src/widgets/CompanyGroupsTable/*.tsx src/widgets/ConsoleLegalEntities/*.tsx | cut -c1-140 | head -10", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (echo \"--- чек-лист/замена/удаление/ЕЦП/история/с). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly.", "expected": null} +{"session": "bce8483e", "ts": "2026-10-10T20:57:22.616Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-web", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca && grep -rn -i \"reprocess\\|повторн\\|retry\\|attempt\" arm-core/api/src/api --include=*.py 2>/dev/null | grep -v test | head -5; grep -rln -i \"reprocess\\|повторная обработка\" arm-core/api/src | head -3; echo \"--- gitea open issues arm-web\"; tea issues list --repo RSHB/arm-web --state open --fields index,title 2>&1 | head -15; echo \"--- arm-core open\"; tea issues list --repo RSHB/arm-core --state open --fields index,title 2>&1 | head -15", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca && grep -rn -i \"reprocess\\|повто). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directl", "expected": null} +{"session": "bce8483e", "ts": "2026-10-10T20:57:48.631Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-web", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca/arm-web && grep -rn \"path=\" src/App.tsx src/app/*.tsx src/routes*.tsx 2>/dev/null | cut -c1-140 | head -20; ls src; ls src/pages 2>/dev/null || ls src/app 2>/dev/null", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca/arm-web && grep -rn \"path=\" src/App.t). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "bfc350d7", "ts": "2026-10-08T06:54:33.003Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/ && cat qa-local-stand.md arm-web-repo.md arm-web-demo-prototype.md workspace-hooks-and-worktrees.md feedback-no-harness-bypass.md feedback-compare-with-mockup.md frontend-api-guide-state.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "bfc350d7", "ts": "2026-10-08T07:14:14.008Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/fixtures/osv", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca/.wt/qa-guide-web/docs && cat > /Users/andrey.epifancev/.claude/jobs/0d5648e2/tmp/sec8.md <<'EOF'\n## 8. Сквозной сценарий в UI (arm-web)\n\nОсновной путь аналитика по макету: группа компаний → новая консолидация → файлы ЮЛ → проверка ОСВ и форм ББ/ОФР →\nсводная ОСВ. Номера в квадратных скобках — экраны макета `docs/mockup/screens/-*.png` (открывать по одному через\n`docs/mockup/INDEX.md`): если не ясно, «так ли должно выглядеть», сравнивайте с ними при окне 1600×1000. Права, коды\nошибок и граничные случаи, которых нет в UI, — в разделах 6–7 (там же в каждом разделе указано, какая часть доступна в UI).\n\n**Предусловия.** Стенд по разделу 4: ядро с `NATS_ENABLED=true`, tabular-parser на `:8001`, `make seed-dev` (ГК «Русский\nБисквит» с ГСК из РБ и Счастливочной), фронт — `make start`. Для 8.5 (PDF) — ещё шлюз и эмулятор ИС Сканирование:\n\n```bash\ndocker compose -f ../docs/dev/docker-compose.yml --profile services up -d --build --no-deps scan-gateway scan-emulator\n```\n\nПользователь — `ka_go@armka.local`. Начинайте со свежего стенда (раздел 10): тогда цифры совпадут с 7.2 без поправок.\nПодписи кнопок, колонок и сообщений ниже — дословно как в UI.\n\n### 8.1 Навигация и группы компаний [03]\n\n| № | Шаги | Ожидание |\n|---|---|---|\n| W1 | Войти `ka_go`, посмотреть боковое меню | логотип «АРМ КА», кнопка «+ Новая консолидация»; разделы «Отчётность» (Группы компаний, Монозаемщики), «Справочники» (ГК и организации, ЮЛ вне ГК), «Документы» (Единое хранилище), «Система» (Журнал действий, Пользователи и роли); кнопка «‹» сворачивает меню до иконок. Всё, кроме «Группы компаний» и «Пользователи и роли», — заглушки |\n| W2 | «Группы компаний» | крошки «Отчётность › Группы компаний», подзаголовок «4 групп компаний» (после `make seed-dev`: Русский Бисквит, Унагранде, Новороссийский прокатный завод, Агрофирма Межениновская); колонки «Группа компаний», «CRM ID», «Основной РФ», «Организаций» |\n| W3 | Поиск «бисквит»; затем фильтр в заголовке «Группа компаний» (сортировка, отметки, «OK») | остаётся «Русский Бисквит»; появляется «Сбросить фильтры»; «Настройка таблицы» скрывает и показывает колонки |\n| W4 | В строке «Русский Бисквит» — кнопка «→ Консолидации» | страница группы [34]: «Русский Бисквит», «20 организаций», блок «Консолидации 0 шт.»; блок «Организации» пуст, пока версия не выбрана; «← Назад» возвращает к списку |\n\n### 8.2 Новая консолидация [42]\n\n| № | Шаги | Ожидание |\n|---|---|---|\n| W5 | «+ Новая консолидация» в меню | модалка «Новая консолидация»: «Группа компаний», «ГСК» (неактивна до выбора ГК), «Год» (текущий и три предыдущих), «Квартал»; «Создать» неактивна |\n| W6 | ГК «Русский Бисквит», ГСК «ГСК Русский Бисквит», 2024, «3 квартал» | блок «ЮЛ консолидации»: все открытые ЮЛ ГК, РБ (3528052718) и Счастливочная (3528104476) отмечены и стоят первыми, счётчик «Выбрано: 2 / 20»; поиск «Поиск ЮЛ...» фильтрует список |\n| W7 | «Создать» | уведомление «Создана консолидация 2024, 3 квартал»; открывается страница группы с выбранной версией (`?version=` в адресе): строка «2024 · К3 · 1 · Инициация», «Дата создания» — сегодня по Москве, «ГСК» и «Дата верификации» — «—» (так в прототипе); в «Организации» — 2 строки, «Бух.баланс» и «ОФР» — «Отсутствует», «Ответственный: Dev: Аналитик ГО» |\n| W8 | Повторить W5–W7 с тем же периодом | уведомление «Консолидация ГК за этот период уже создана» (409 `CONSOLIDATION_EXISTS`), модалка остаётся открытой |\n| W9 | В модалке оставить отмеченным одно ЮЛ; снять все | одно ЮЛ — уведомление с текстом сервера (422 `PERIMETER_TOO_SMALL`, K2); ни одного — «Создать» неактивна |\n\n### 8.3 Организации, чек-лист, загрузка файлов [34], [39], [40]\n\n| № | Шаги | Ожидание |\n|---|---|---|\n| W10 | Кликнуть строку версии в «Консолидации» | строка подсвечена зелёным, адрес `?version=`; блок «Организации»: колонки «Наименование», «Документы», «ОФР», «Бух.баланс», «ОСВ», «ИНН», «Тип»; меню «⋮» строки версии: «Добавить версию» → «Будет позже», «История изменений» → заглушка журнала |\n| W11 | «Скачать чек-лист» | файл `checklist-Русский Бисквит-2024-Q3.csv`; содержимое — как в F17 (BOM, разделитель `;`, открывается в Excel) |\n| W12 | Клик по строке «ПЕКАРНЯ СЧАСТЛИВОЧНАЯ, ООО» (или кнопке «Документы») | справа панель «ПЕРВИЧНЫЕ ДОКУМЕНТЫ» [39]: имя ЮЛ, ИНН 3528104476, кнопка «Загрузить документы», «Документов нет»; крестик справа закрывает панель |\n| W13 | «Загрузить документы» → «Выбрать документы» → пять файлов `../fixtures/osv/schastl/2024-Q3/*` → «Загрузить» | модалка «Загрузка документов» [40]: зелёный блок «Принято: 5» с именами, кнопка «Загрузить» сменилась на «Готово». В панели файлы сначала «Обработка», через 5–15 с у каждого документ: «ОСВ 60 · XLS · <дата загрузки>», период «2024 К3», проверка «Период —», «Контрагент ✓», статус «Требуется проверка». Панель опрашивает сервер раз в 5 с, пока есть файлы в обработке |\n| W14 | Загрузить ещё раз любой из этих файлов | красный блок «Отклонено: 1» со строкой «<имя> — файл «<имя>» уже загружен для этого ЮЛ» (`DUPLICATE_FILE`), уведомление «Ни один файл не принят» |\n| W15 | Закрыть панель, обновить страницу (F5) | статус версии в таблице — «Обработка документов» (K8) |\n\n### 8.4 Просмотр ОСВ-документа\n\n| № | Шаги | Ожидание |\n|---|---|---|\n| W16 | В панели Счастливочной клик по «ОСВ 60» | экран «Просмотр ОСВ»: крошки «Отчётность › ПЕКАРНЯ СЧАСТЛИВОЧНАЯ, ООО › Проверка документа», подзаголовок «<имя файла> · XLS · <дата>», справа статус «Требуется проверка»; надпись «Суммы в рублях; строки ОСВ только для чтения»; дерево счёт → субсчёт → контрагент, колонки «Счёт / контрагент», «ИНН» и шесть сумм. Корневая строка `60`: «—», «27 923 002,09», «107 554 149,33», «99 188 100,32», «—», «19 556 953,08» (сальдо нач. Кт + обороты Кт − обороты Дт = сальдо кон. Кт) |\n| W17 | «✓ Подтвердить» | уведомление «Документ подтверждён», статус «Загружен», кнопка неактивна; в панели документ — «Загружен» |\n| W18 | Остальные файлы РБ и Счастливочной 2024 Q1–Q3 (22, список — 7.1) — через «Загрузить документы» каждому ЮЛ и подтверждение каждого ОСВ, либо скриптом из `arm-web`: `scripts/demo-prepare.sh ` (id — из адреса `?version=`) | скрипт печатает по строке на файл (пять файлов из W13 — «уже загружен», это нормально), ждёт распознавания (до 5 мин), подтверждает все ОСВ и в конце печатает `строк: 1385 документов: 22 несопоставленных: 1381 пробелов: 7`. После F5 статус версии — «Коррекция данных» (K9), в «Организации» в колонке «ОСВ» — счета ЮЛ |\n\n`demo-prepare.sh` берёт пользователя вторым аргументом (по умолчанию `ka_go@armka.local`), адрес ядра — из `API`\n(по умолчанию `localhost:8000/api/v1`), каталог фикстур — из `FIXTURES_DIR` (по умолчанию `../fixtures` рядом с `arm-web`).\n\n### 8.5 Проверка формы ББ/ОФР из PDF [41], [41-02]\n\nPDF из `fixtures/ocr` — отчётность других компаний (`aep_6.pdf` — ООО «Дорстроймонтаж», на 30.06.2025), поэтому ячейки\nББ/ОФР чек-листа версии 2024 Q3 они не заполняют; для проверки экрана подойдёт любое ЮЛ версии. Распознаются только PDF,\nдля которых в эмуляторе есть ответ (раздел 6.8).\n\n| № | Шаги | Ожидание |\n|---|---|---|\n| W19 | В панели Счастливочной загрузить `../fixtures/ocr/aep_6.pdf` | через 10–60 с два документа: «Бух. баланс · PDF» и «ОФР · PDF», статус «Требуется проверка», «Контрагент ✗» (в скане другая организация) |\n| W20 | Открыть «Бух. баланс» | экран «Проверка распознавания»: слева «ИСХОДНЫЙ ДОКУМЕНТ» — страницы PDF «Страница 1…8», масштаб «−», «100%», «+» и сброс; справа «РЕЗУЛЬТАТ РАСПОЗНАВАНИЯ»: шапка «ИНН —», «Дата отчёта —» (оранжевый фон — не распознано), «Единица измерения тыс. руб.»; заголовок «Бухгалтерский баланс»; колонки «На 30 июня 2025 г», «На 31 декабря 2024 г.», «На 31 декабря 2023 г.»; разделы «АКТИВ», «I. Внеоборотные активы» … «ПАССИВ»; итоги разделов жирным; суммы без копеек, пустые — «-»; строка 1600 «БАЛАНС» — 293 709 / 179 791 / 189 847. Ячейки с уверенностью распознавания < 100 % — оранжевые, подсказка «Уверенность: N%» |\n| W21 | «✎ Редактировать» → в строке 1600 первой колонки прибавить 100 → «✓ Сохранить» | уведомление «Изменения сохранены»; ячейка голубая, подсказка «Исходное: 293 709»; блок «Нарушения (N)» со строками «Колонка N: <соотношение> — ожидалось X, получено Y» (F3) |\n| W22 | «✓ Подтвердить данные» | красный блок в правой части с текстом сервера и списком нарушений (409 `CHECKS_NOT_PASSED`, F4); документ не подтверждён |\n| W23 | «Принять с комментарием» → «Принять нарушения»: пустой комментарий, затем «округление в форме клиента» → «Принять» | с пустым «Принять» неактивна; после — синий блок «Нарушения приняты» с датой и комментарием (F5) |\n| W24 | «✎ Редактировать», ввести в ячейку `abc` или `1.234` → «✓ Сохранить» | ячейка в красной рамке, подсказка «Неверный формат суммы»; уведомление «Исправьте суммы с неверным форматом», запрос не уходит. «Отменить» — правки сброшены |\n| W25 | Вернуть в 1600 исходную сумму, сохранить | нарушений нет («Проверки пройдены»), принятие сброшено правкой (F6) |\n| W26 | В шапке «Дата отчёта» ввести `2025-09-15` → «✓ Сохранить» → «✓ Подтвердить данные» | красный блок «<текст сервера>. Исправьте дату отчёта в шапке и сохраните.» (409 `PERIOD_REQUIRED`: дата не на конец квартала, F9). Дата вводится строго `ГГГГ-ММ-ДД` |\n| W27 | Дата `2025-06-30` → «✓ Сохранить» → «✓ Подтвердить данные» | уведомление «Документ подтверждён», статус «Загружен», «Редактировать» и «Подтвердить данные» неактивны |\n| W28 | Открыть «ОФР» того же файла [41-02] | заголовок «Отчёт о финансовых результатах», колонки периодов ОФР; остальное — как W20 |\n\nФормы из Excel (`fixtures/statements-xlsx`, раздел 6.9) открываются тем же экраном, но слева вместо скана — «Превью\nнедоступно для этого формата» и кнопка «Скачать исходник». Документы «Прочий документ» (расшифровки) в панели не\nоткрываются.\n\n### 8.6 Сводная ОСВ [30]\n\nПредусловие — W18: все 22 ОСВ загружены и подтверждены.\n\n| № | Шаги | Ожидание |\n|---|---|---|\n| W29 | В строке версии «К отчётности →» | экран «Русский Бисквит — 2024 К3», крошки «Отчётность › Русский Бисквит › 2024 · К3», справа «Статус отчёта» «Коррекция данных»; активна вкладка «ОСВ», «Шахматка», «Таблица корректировки», «Гудвилл», «КФО», «КФО Общий», «Подтверждение КФО клиента» неактивны (подсказка «Будет в следующих срезах») |\n| W30 | Сводка | под таблицей «Строк: 1385» (= `rows_total`, 7.2); жёлтый баннер «Требуется указать соответствия … Осталось: 1381.» (= `unmapped_total`); свёрнутый блок «Пробелы: 7 — по этим счетам нет ОСВ за часть кварталов, их строк в своде нет», раскрыть — 7 строк «<ЮЛ> — счёт <код> — нет кварталов: Q1, Q2»: РБ 01, 58.03, 66, 67, 76; Счастливочная 01, 02 |\n| W31 | Колонка «Период» | у всех строк «9 мес. 2024 г.»; суммы с запятой и пробелами, ноль — «—»; таблица по 500 строк на страницу |\n| W32 | Фильтр «Компания ГК» (воронка в заголовке) = «ПЕКАРНЯ СЧАСТЛИВОЧНАЯ, ООО» → «Применить»; фильтр «Контрагенты» = `бисквит` → «Применить» | «Строк: 6» — `60.01`, `60.02`, `62.01`, `62.02`, `76.06`, `76.АВ`, контрагент «РУССКИЙ БИСКВИТ, АО», ИНН 3528052718. Суммы — как в 7.2, на экране с запятой: `60.01` — «—», «4 454 151,93», «128 058 143,65», «125 950 450,77», «—», «2 346 459,05»; `60.02` — «—» везде, кроме оборотов «9 745 019,62» / «9 745 019,62»; `62.01` — «40 211 938,00», «—», «398 018 012,46», «330 517 504,00», «107 712 446,46», «—»; `62.02` — «—», «24 100 000,00», «9 900 000,00», «—», «—», «14 200 000,00». Иконка активного фильтра зелёная |\n| W33 | Сбросить фильтры; «Счёт» = `62` | «Строк: 387» на свежем стенде; `60.01` — 0 строк (фильтр принимает только корень счёта, ограничение прототипа) |\n| W34 | «Показать ЮЛ без ИНН» | только несопоставленные строки, «Строк» = «Осталось»; кнопка сменилась на «Показать все строки» |\n| W35 | «Экспорт» (с фильтрами и без) | файл `svod-osv_Русский Бисквит_2024-Q3_v1.xlsx` по текущим фильтрам; содержимое — по 7.3 |\n\nЕсли на стенде раньше делали ручное сопоставление контрагентов (K14), «Осталось» меньше 1381: алиасы переживают версии.\nОстальные цифры от этого не меняются.\n\n### 8.7 Что в UI не считать багом (прототип)\n\n- Нет «Взять в работу», смены периметра, ручных переходов статусов, сопоставления контрагентов, экрана чек-листа,\n комментариев, удаления файлов, «Добавить версию» — проверяйте через API (разделы 6.6, 6.9, 7).\n- В своде не показываются «документов: 22», колонки «Признак контрагента» и «Способ сопоставления» (признак есть только\n внутри фильтра «Контрагенты»: ГСК / ГК / Внешний); фильтр «Счёт» принимает только корень (`60`, не `60.01`).\n- Таблица консолидаций и статус версии обновляются после F5: после создания консолидации на странице той же ГК новой\n строки может не быть до обновления; панель «Организации» перечитывается при закрытии панели документов.\n- «Распознан» и «Требуется проверка» в панели документов выглядят одинаково — «Требуется проверка».\n- «ГСК» и «Дата верификации» в таблице консолидаций — всегда «—»; «Утверждён в РФ», «Верифицирован в ГО» — «—».\n- Панель документов показывает первые 100 файлов ЮЛ; загрузка — только из панели ЮЛ внутри версии.\n- Шапку формы (ИНН, дату, единицу) нельзя очистить — только заменить; время принятия нарушений — в часовом поясе\n браузера, остальные даты — по Москве.\n- Ссылка `?version=` с несуществующим id «выбирает» пустую версию; при 404 на своде может прийти до трёх сообщений;\n после истечения токена кроме «Сессия истекла» бывают лишние уведомления.\n- Предупреждения antd в консоли браузера («Static function can not consume context…», deprecated) — не влияют.\n- Уведомления в `/admin` не появляются — дефект arm-web#7 (раздел 6.1).\n\nБаг UI — issue в `RSHB/arm-web` (раздел 11): номер сценария W, скриншот, вкладка Network с запросом и ответом.\n\nEOF\npython3 - <<'EOF'\np='dev/qa-guide.md'; s=open(p).read()\nsec=open('/Users/andrey.epifancev/.claude/jobs/0d5648e2/tmp/sec8.md').read()\nfor a,b in [(\"## 11. Типичные проблемы\",\"## 12. Типичные проблемы\"),(\"## 10. Как заводить баги\",\"## 11. Как заводить баги\"),(\"## 9. Сброс стенда\",\"## 10. Сброс стенда\"),(\"## 8. Известные ограничения\",\"## 9. Известные ограничения\")]:\n assert s.count(a)==1; s=s.replace(a,b)\na=\"## 9. Известные ограничения\"\ns=s.replace(a, sec+a)\nopen(p,'w').write(s)\nEOF\ngrep -n \"^## \" dev/qa-guide.md", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca/.wt/qa-guide-web/docs && cat > /Users). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "c15a6f0c", "ts": "2026-10-06T13:14:14.950Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cat /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/jetstream-check-later.md /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "c222e9bb", "ts": "2026-10-06T09:44:42.939Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core/api", "branch": "HEAD", "tool": "Bash", "command": "echo \"=== enums with perm\"; grep -n -i \"permission\\|role\" src/models/enums.py | head -40; echo \"=== seed perms\"; ls seed; grep -rn -i \"permission\" seed/*.yaml seed/*.json seed/*.csv 2>/dev/null | head -5; echo \"=== seed files content heads\"; for f in seed/*; do echo \"--- $f\"; head -60 \"$f\"; done 2>/dev/null | head -250", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (echo \"=== enums with perm\"; grep -n -i \"permission\\|role\" src/models/enums.py | ). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "c222e9bb", "ts": "2026-10-06T09:45:06.568Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core/api", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca/arm-core/web && ls src src/auth src/api; cat src/api/client.ts; cat src/auth/*.ts src/auth/*.tsx 2>/dev/null", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca/arm-core/web && ls src src/auth src/a). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "c222e9bb", "ts": "2026-10-06T09:49:37.296Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core/web", "branch": "HEAD", "tool": "Write", "command": null, "file_path": "/private/tmp/claude-501/-Users-andrey-epifancev-projects-awp-ca/c222e9bb-473a-4a4b-be9b-5dc8cdb15b48/scratchpad/frontend-auth-guide.md", "hook": "guard-main(Edit/Write)", "message": "PreToolUse:Write hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: you are on main. Create an isolated worktree first (superpowers:using-git-worktrees), never edit main directly.", "expected": null} +{"session": "c222e9bb", "ts": "2026-10-06T09:52:47.722Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cat /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md; git -C /Users/andrey.epifancev/projects/awp-ca/docs worktree list; git -C /Users/andrey.epifancev/projects/awp-ca/docs remote -v | head -2; cat /Users/andrey.epifancev/projects/awp-ca/docs/.gitignore", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "c3f1f3d1", "ts": "2026-10-02T21:55:49.335Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "echo \"=====openspec tree=====\" && find openspec -type f | sort && for f in $(find openspec -maxdepth 1 -type f) openspec/config.yaml openspec/AGENTS.md; do [ -f \"$f\" ] && { echo \"--- $f\"; cat \"$f\"; }; done 2>/dev/null; echo \"=====openspec specs/changes=====\" && find openspec/specs openspec/changes -type f 2>/dev/null | head -50; for f in $(find openspec/specs openspec/changes -type f 2>/dev/null | head -20); do echo \"--- $f\"; cat \"$f\"; done; echo \"=====setup=====\" && find setup -type f | sort && for f in $(find setup -type f | sort); do echo \"--- $f\"; cat \"$f\"; done", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (echo \"=====openspec tree=====\" && find openspec -type f | sort && for f in $(fin). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "c3f1f3d1", "ts": "2026-10-02T21:56:21.933Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "cat \"$HOME/.claude/hooks/guard-main.sh\"; echo \"=====settings hooks=====\"; python3 -c \"import json;d=json.load(open('$HOME/.claude/settings.json'));print(json.dumps(d.get('hooks',{}),ensure_ascii=False,indent=1))\"; ls \"$HOME/.claude/hooks/\" \"$HOME/.claude/harness/\" 2>&1 | head -40", "file_path": null, "hook": "guard-main(Bash)", "message": " echo \"BLOCKED: shell write on $label ($(printf '%s' \"$cmd\" | head -c 80)). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly.\" >&2", "expected": null} +{"session": "c3f1f3d1", "ts": "2026-10-02T21:57:47.007Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cat \"$HOME/.claude/hooks/require-plan.sh\"; echo \"=====harness-check=====\"; cat \"$HOME/.claude/hooks/harness-check.sh\"; echo \"=====adopt-project=====\"; cat \"$HOME/.claude/harness/adopt-project.sh\"; echo \"=====project-template=====\"; find \"$HOME/.claude/harness/project-template\" -type f | head -30", "file_path": null, "hook": "require-plan", "message": "echo \"BLOCKED: editing $fpath without a plan. Run superpowers:brainstorming; for a bounded change create '$root/.bounded' first.\" >&2", "expected": null} +{"session": "c3f1f3d1", "ts": "2026-10-02T22:00:57.967Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca && WT=/Users/andrey.epifancev/projects/awp-ca/.wt/slice-0 && mkdir -p \"$WT\" && for r in docs arm-core tabular-parser scan-gateway; do git -C \"$r\" worktree add \"$WT/$r\" -b tmp/slice-0-skeleton 2>&1 | tail -1; done && ln -sfn ../../fixtures \"$WT/fixtures\" && ln -sfn ../../reference-old-arm \"$WT/reference-old-arm\" && ln -sfn ../../CLAUDE.md \"$WT/CLAUDE.md\" && ln -sfn ../../.claude \"$WT/.claude\" && ls -la \"$WT\" && for r in docs arm-core tabular-parser scan-gateway; do echo \"--- $r: $(git -C \"$WT/$r\" branch --show-current) @ $(git -C \"$WT/$r\" rev-parse --short HEAD)\"; done && git -C docs worktree list", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca && WT=/Users/andrey.epifancev/project). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "c48e5bdb", "ts": "2026-10-09T12:34:44.922Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory && cat workspace-hooks-and-worktrees.md qa-local-stand.md qa-guide-web-state.md arm-web-demo-prototype.md feedback-compare-with-mockup.md arm-web-repo.md feedback-no-harness-bypass.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "c58a72aa", "ts": "2026-10-08T13:28:11.693Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/; cat arm-web-repo.md arm-web-demo-prototype.md workspace-hooks-and-worktrees.md feedback-compare-with-mockup.md feedback-no-harness-bypass.md; sed -n 1,200p qa-local-stand.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "cedd7915", "ts": "2026-10-06T08:41:54.622Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory && cat slice-4-state.md dkb-package-state.md workspace-hooks-and-worktrees.md tech-debt-slice-1.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "cedd7915", "ts": "2026-10-06T08:42:35.390Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "grep -rn \"Claude-Session\\|bounded\" ~/.claude/hooks/*.sh .claude/hooks/*.sh 2>/dev/null | head -20; ls ~/.claude/hooks .claude/hooks; git -C arm-core log -5 --format='%s%n%(trailers:key=Claude-Session)' | head -20; echo \"=== fixtures osv-dkb README\"; cat fixtures/osv-dkb/README.md | head -150", "file_path": null, "hook": "require-plan", "message": "/Users/andrey.epifancev/.claude/hooks/require-plan.sh:32:echo \"BLOCKED: editing $fpath without a plan. Run superpowers:brainstorming; for a bounded change create '$root/.bounded' first.\" >&2", "expected": null} +{"session": "cedd7915", "ts": "2026-10-06T18:39:59.376Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/tabular-parser", "branch": "HEAD", "tool": "Bash", "command": "openspec validate --specs --strict --no-interactive 2>&1 | tail -2; git add openspec && git commit -q -m \"$(cat <<'EOF'\nopenspec: архив change slice-2b-statement-parser (2026-10-06), спека statement-parsing, дельта tabular-recognition-service\n\nRefs #14\n\nCo-Authored-By: Claude Opus 5.5 \nClaude-Session: https://claude.ai/code/session_01EAKRDNzXtRrwmWz9z6PZu3\nEOF\n)\" && git push -q origin main 2>&1 | grep -v \"^remote:\"; git log --oneline -1 | cat; git status -sb | head -1", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (openspec validate --specs --strict --no-interactive 2>&1 | tail -2; git add open). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "cedd7915", "ts": "2026-10-06T18:40:06.664Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/tabular-parser", "branch": "HEAD", "tool": "Bash", "command": "git add openspec && git commit -q -m \"openspec: архив change slice-2b-statement-parser (2026-10-06), спека statement-parsing, дельта tabular-recognition-service\n\nRefs #14\n\nCo-Authored-By: Claude Opus 5.5 \nClaude-Session: https://claude.ai/code/session_01EAKRDNzXtRrwmWz9z6PZu3\" && git push -q origin main && git log --oneline -1 | cat", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (git add openspec && git commit -q -m \"openspec: архив change slice-2b-state). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directl", "expected": null} +{"session": "cedd7915", "ts": "2026-10-06T18:40:09.776Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/tabular-parser", "branch": "HEAD", "tool": "Bash", "command": "grep -n -i \"archive\" ~/.claude/hooks/guard-main.sh", "file_path": null, "hook": "guard-main(Bash)", "message": "37: echo \"BLOCKED: shell write on $label ($(printf '%s' \"$cmd\" | head -c 80)). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly.\" >&2", "expected": null} +{"session": "cedd7915", "ts": "2026-10-06T18:40:32.919Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/tabular-parser", "branch": "HEAD", "tool": "Bash", "command": "git switch -q -c chore/archive-slice-2b-statement-parser && git add openspec && git commit -q -m \"openspec: архив change slice-2b-statement-parser (2026-10-06), спека statement-parsing, дельта tabular-recognition-service\n\nRefs #14\n\nCo-Authored-By: Claude Opus 5.5 \nClaude-Session: https://claude.ai/code/session_01EAKRDNzXtRrwmWz9z6PZu3\" && git push -q -u origin chore/archive-slice-2b-statement-parser && git log --oneline -1 | cat", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (git switch -q -c chore/archive-slice-2b-statement-parser && git add openspec && ). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "cedd7915", "ts": "2026-10-06T19:00:15.482Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/tabular-parser/api", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca && W=$PWD; mkdir -p .wt/f20 && git -C tabular-parser worktree add -q -b fix/20-continuation-tolerance $W/.wt/f20/tabular-parser origin/main && git -C fixtures worktree add -q -b fix/20-golden $W/.wt/f20/fixtures origin/main && for l in docs .claude CLAUDE.md; do ln -s ../../$l $W/.wt/f20/$l; done; touch $W/.wt/f20/tabular-parser/.bounded; make -C $W/.wt/f20/tabular-parser venv 2>&1 | tail -1; T=$W/.wt/f20/tabular-parser/api; wc -l $T/src/parser/statements/*.py $T/src/parser/book.py; cat $T/rules/statement_checks.yaml | head -20", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca && W=$PWD; mkdir -p .wt/f20 && git -C). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "d31ab7be", "ts": "2026-10-07T12:01:20.115Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca; M=~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory; cat $M/slice-3b-state.md $M/slice-3a-state.md $M/core-33-golden-tests.md $M/slice-4-state.md $M/workspace-hooks-and-worktrees.md; ls; ls .wt 2>/dev/null", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "d60ad490", "ts": "2026-10-07T10:13:06.399Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "grep -rn -iE \"version_table_schema|search_path|schema *=|create schema|\\\"armka\\\"|'armka'|_SCHEMA\" --include='*.py' --include='*.ini' --include='*.toml' --include='*.yml' --include='*.yaml' --include='.env*' . 2>/dev/null | grep -v node_modules | head -40", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (grep -rn -iE \"version_table_schema|search_path|schema *=|create schema|\\\"armka\\\"). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "d60ad490", "ts": "2026-10-07T10:13:36.529Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca/arm-core; ls api/alembic/versions; sed -n 1,40p api/src/db/db.py; sed -n 1,20p api/src/models/base.py; grep -rn \"DATABASE\" raisa_config.yml api/src/core/*.py 2>/dev/null | head", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca/arm-core; ls api/alembic/versions; se). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "d60ad490", "ts": "2026-10-07T10:13:38.071Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca; git -C .wt/slice-3b/arm-core log --oneline -1 2>/dev/null || ls .wt/slice-3b; ls .wt/slice-3b/arm-core/api/alembic/versions 2>/dev/null || find .wt/slice-3b -path '*alembic/versions' -maxdepth 5", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca; git -C .wt/slice-3b/arm-core log --o). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "d60ad490", "ts": "2026-10-07T10:14:06.972Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "cat >> /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/slice-3b-state.md <<'EOF'\n\n**После слияния 3b:** взять arm-core #38 (схема БД `dkb` зашита в models/base.py, db/db.py, alembic/env.py, миграциях 0001–0004 и conftest → сделать настройкой). Bounded. Отложено 07.10.2026, чтобы не конфликтовать с SDD-агентом 3b.\nEOF", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cat >> /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "d60ad490", "ts": "2026-10-07T10:14:16.116Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Edit", "command": null, "file_path": "/Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/slice-3b-state.md", "hook": "guard-main(Edit/Write)", "message": "PreToolUse:Edit hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: you are on main. Create an isolated worktree first (superpowers:using-git-worktrees), never edit main directly.", "expected": null} +{"session": "d60ad490", "ts": "2026-10-07T14:07:41.379Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core/web", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca/arm-core && tea issues create --login home --repo RSHB/arm-core --title \"Зависимости под контур: PyJWT[crypto]==2.8.0, cryptography==43.0.3 (и правка JwksVerifier)\" --description \"$(cat <<'EOF'\n## Откуда\n\nЗапрос разработчика, который переносит сервисы в контур РАИСА: в arm-core должны стоять `pyjwt[crypto]==2.8.0` и `cryptography==43.0.3`. Сейчас `api/requirements.txt`: `PyJWT[crypto]==2.15.1`, `cryptography==50.0.2`; `api/requirements-dev.txt`: `PyJWT[crypto]>=2.9`.\n\n## Что проверено (07.10.2026, отдельный venv)\n\n- Вся `requirements.txt` с этими версиями разрешается без конфликтов (`uv pip compile`, кроме `raisa_*`).\n- Dev-аутентификация (`DevHs256Verifier`, `POST /api/v1/dev/token`) на 2.8.0 работает: encode/decode HS256, `require: [exp, sub]`, чужая подпись → `InvalidSignatureError`.\n- Параметры `PyJWKClient(cache_keys, cache_jwk_set, lifespan, timeout)` на 2.8.0 принимаются.\n- **Ломается `JwksVerifier`**: `src/core/auth.py`, `_signing_key` отдаёт в `jwt.decode` сам объект `PyJWK` — это поддерживается только с PyJWT 2.10. На 2.8.0 `TypeError: Expecting a PEM-formatted key` → перехватывается → 401 «недействительный токен» на **любой** токен из JWKS. Статический `AUTH_PUBLIC_KEY` работает.\n\n## Что сделать\n\n1. `api/requirements.txt`: `PyJWT[crypto]==2.8.0`, `cryptography==43.0.3`; `api/requirements-dev.txt`: снять `>=2.9` (согласовать с 2.8.0).\n2. `JwksVerifier._signing_key`: в `jwt.decode` передавать `signing_key.key`, а допустимый алгоритм выводить из типа ключа самим (RSA → RS256, EC P-256 → ES256 — как уже сделано для `AUTH_PUBLIC_KEY` в `_load_static_key`), чтобы сохранить поведение «ключ одного типа не принимает токен другого алгоритма» (Срез 0, решение #72).\n3. Тесты `api/tests/test_auth_core.py` (блок JwksVerifier, `_FakeJwkClient`) — зелёные на 2.8.0; добавить случай RSA-ключ + ES256-токен → 401 (а не 500). Проверить, что `PyJWKClientConnectionError` есть в 2.8.0 (импортируется в тестах).\n4. `make check-full` зелёный на новых версиях.\n\n## Контекст\n\nВ контуре пока разворачиваем **без платформенной аутентификации**, только dev-заглушка (`DEBUG=true`, `AUTH_DEV_ISSUER=true`, `AUTH_DEV_SECRET`) — она с 2.8.0 работает и без этой задачи. JWKS-путь понадобится, когда подключим SSO РАИСА (OQ-54: параметры Keycloak, `iss`/`aud`, claim с email; не выяснено, используется ли на платформе `raisa_fastapi_protected_api` — в коде он импортируется, но не подключён). Возможно, версии 2.8.0/43.0.3 — требование самого `raisa_fastapi_protected_api`; уточнить у разработчика.\n\nBounded-задача. Если `requirements.txt` трогает Срез 3b (#36) — начинать после его слияния.\nEOF\n)\" | tail -2", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca/arm-core && tea issues create --login). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "d60ad490", "ts": "2026-10-07T14:07:55.976Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core/web", "branch": "HEAD", "tool": "Bash", "command": "tea issues create --login home --repo RSHB/arm-core --title \"Зависимости под контур: PyJWT[crypto]==2.8.0, cryptography==43.0.3 (и правка JwksVerifier)\" --description \"$(cat <<'EOF'\n## Откуда\n\nЗапрос разработчика, который переносит сервисы в контур РАИСА: в arm-core должны стоять `pyjwt[crypto]==2.8.0` и `cryptography==43.0.3`. Сейчас `api/requirements.txt`: `PyJWT[crypto]==2.15.1`, `cryptography==50.0.2`; `api/requirements-dev.txt`: `PyJWT[crypto]>=2.9`.\n\n## Что проверено (07.10.2026, отдельный venv)\n\n- Вся `requirements.txt` с этими версиями разрешается без конфликтов (`uv pip compile`, кроме `raisa_*`).\n- Dev-аутентификация (`DevHs256Verifier`, `POST /api/v1/dev/token`) на 2.8.0 работает: encode/decode HS256, `require: [exp, sub]`, чужая подпись → `InvalidSignatureError`.\n- Параметры `PyJWKClient(cache_keys, cache_jwk_set, lifespan, timeout)` на 2.8.0 принимаются.\n- **Ломается `JwksVerifier`**: `src/core/auth.py`, `_signing_key` отдаёт в `jwt.decode` сам объект `PyJWK` — это поддерживается только с PyJWT 2.10. На 2.8.0 `TypeError: Expecting a PEM-formatted key` → перехватывается → 401 «недействительный токен» на **любой** токен из JWKS. Статический `AUTH_PUBLIC_KEY` работает.\n\n## Что сделать\n\n1. `api/requirements.txt`: `PyJWT[crypto]==2.8.0`, `cryptography==43.0.3`; `api/requirements-dev.txt`: снять `>=2.9` (согласовать с 2.8.0).\n2. `JwksVerifier._signing_key`: в `jwt.decode` передавать `signing_key.key`, а допустимый алгоритм выводить из типа ключа самим (RSA → RS256, EC P-256 → ES256 — как уже сделано для `AUTH_PUBLIC_KEY` в `_load_static_key`), чтобы сохранить поведение «ключ одного типа не принимает токен другого алгоритма» (Срез 0, решение #72).\n3. Тесты `api/tests/test_auth_core.py` (блок JwksVerifier, `_FakeJwkClient`) — зелёные на 2.8.0; добавить случай RSA-ключ + ES256-токен → 401 (а не 500). Проверить, что `PyJWKClientConnectionError` есть в 2.8.0 (импортируется в тестах).\n4. `make check-full` зелёный на новых версиях.\n\n## Контекст\n\nВ контуре пока разворачиваем **без платформенной аутентификации**, только dev-заглушка (`DEBUG=true`, `AUTH_DEV_ISSUER=true`, `AUTH_DEV_SECRET`) — она с 2.8.0 работает и без этой задачи. JWKS-путь понадобится, когда подключим SSO РАИСА (OQ-54: параметры Keycloak, `iss`/`aud`, claim с email; не выяснено, используется ли на платформе `raisa_fastapi_protected_api` — в коде он импортируется, но не подключён). Возможно, версии 2.8.0/43.0.3 — требование самого `raisa_fastapi_protected_api`; уточнить у разработчика.\n\nBounded-задача. Если `requirements.txt` трогает Срез 3b (#36) — начинать после его слияния.\nEOF\n)\"", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (tea issues create --login home --repo RSHB/arm-core --title \"Зависимос�). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly.", "expected": null} +{"session": "d60ad490", "ts": "2026-10-07T14:08:08.999Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core/web", "branch": "HEAD", "tool": "Write", "command": null, "file_path": "/private/tmp/claude-501/-Users-andrey-epifancev-projects-awp-ca/d60ad490-3778-4c02-b9ba-0689e706df8f/scratchpad/issue-jwt.md", "hook": "guard-main(Edit/Write)", "message": "PreToolUse:Write hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: you are on main. Create an isolated worktree first (superpowers:using-git-worktrees), never edit main directly.", "expected": null} +{"session": "d60ad490", "ts": "2026-10-07T14:15:11.785Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core/api", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca && sed -i '' 's/делать \\*\\*после завершения агента Среза 3b\\*\\* (arm-core #36)/делать **после завершения агента arm-core #42** (front-api-gaps, worktree .wt\\/front-gaps; Срез 3b уже слит и заархивирован 07.10.2026)/' /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/contour-transfer-queue.md && grep -n \"после завершения\" /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/contour-transfer-queue.md", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca && sed -i '' 's/делать \\*\\*по). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly.", "expected": null} +{"session": "d60ad490", "ts": "2026-10-07T14:15:16.765Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core/api", "branch": "HEAD", "tool": "Edit", "command": null, "file_path": "/Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/contour-transfer-queue.md", "hook": "guard-main(Edit/Write)", "message": "PreToolUse:Edit hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: you are on main. Create an isolated worktree first (superpowers:using-git-worktrees), never edit main directly.", "expected": null} +{"session": "d60ad490", "ts": "2026-10-07T14:20:39.845Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core/api/src/schemas", "branch": "HEAD", "tool": "Edit", "command": null, "file_path": "/Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/contour-transfer-queue.md", "hook": "guard-main(Edit/Write)", "message": "PreToolUse:Edit hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: you are on main. Create an isolated worktree first (superpowers:using-git-worktrees), never edit main directly.", "expected": null} +{"session": "d60ad490", "ts": "2026-10-07T14:23:25.033Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cat /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md; ls /Users/andrey.epifancev/projects/awp-ca/.wt", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "d60ad490", "ts": "2026-10-07T14:45:48.564Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/.wt/verify-main/arm-core/api", "branch": "HEAD", "tool": "Bash", "command": "P=$(ls -d .venv venv 2>/dev/null | head -1)/bin/python; echo $P; $P -c \"import jwt,cryptography;print(jwt.__version__,cryptography.__version__)\"; $P -m pytest -q -rs 2>&1 | grep SKIPPED | sed -E 's/.*SKIPPED \\[[0-9]+\\] //' | cut -c1-150 | sort | uniq -c | head", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on detached HEAD (P=$(ls -d .venv venv 2>/dev/null | head -1)/bin/python; echo $P; $P -c \"import j). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land o", "expected": null} +{"session": "dc1c37f7", "ts": "2026-10-09T18:32:56.639Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cat ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md; echo \"#### docs status\"; git -C docs status --short | head; git -C docs branch --show-current; ls .wt 2>/dev/null; echo \"#### openspec in docs\"; ls docs/openspec docs/openspec/changes 2>/dev/null | head -20", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "e08f3f0b", "ts": "2026-10-09T06:07:31.177Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca && grep -rln \"1110\" tabular-parser/src tabular-parser/rules arm-core/api/rules scan-gateway/src docs/context 2>/dev/null | head; grep -n \"^## \\|^### \" docs/context/05-domain-rules.md | sed -n 1,200p | grep -n \"12\\|7\\.\\|11\" | head -30", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca && grep -rln \"1110\" tabular-parser/sr). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "e08f3f0b", "ts": "2026-10-09T06:13:46.634Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-web", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/ && cat workspace-hooks-and-worktrees.md feedback-no-harness-bypass.md; ls /Users/andrey.epifancev/projects/awp-ca/.wt/web-proto /Users/andrey.epifancev/projects/awp-ca/.wt/logger-diagnose", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "e3c5dd5e", "ts": "2026-10-08T15:22:43.958Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/docs/mockup/dkb_24.09.26", "branch": "HEAD", "tool": "Bash", "command": "python3 - <<'EOF'\ns=open(\"dkb_24.09.26.html\",encoding=\"utf-8\").read()\ni=3350471\nseg=s[i-40000:i]\nimport re\nfor pat in [r'.{0,200}\\bN\\s*=\\s*(?:\\(|[A-Za-z_$]+=>|useCallback|function).{0,300}', r'.{0,300}Показать ЮЛ без ИНН', r'.{0,200}missingInn.{0,200}', r'.{0,200}counterpartyInn\\|\\|.{0,200}']:\n for m in re.finditer(pat,seg): print(\">>\",m.group(0)[:600]); print()\nEOF", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (python3 - <<'EOF'", "expected": null} +{"session": "e3c5dd5e", "ts": "2026-10-08T15:22:52.982Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/docs/mockup/dkb_24.09.26", "branch": "HEAD", "tool": "Write", "command": null, "file_path": "/Users/andrey.epifancev/.claude/jobs/8105dccb/tmp/mock_n.py", "hook": "guard-main(Edit/Write)", "message": "PreToolUse:Write hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: you are on main. Create an isolated worktree first (superpowers:using-git-worktrees), never edit main directly.", "expected": null} +{"session": "e3c5dd5e", "ts": "2026-10-08T15:22:58.746Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/docs/mockup/dkb_24.09.26", "branch": "HEAD", "tool": "Bash", "command": "grep -o -E '.{0,150}Показать ЮЛ без ИНН' /Users/andrey.epifancev/projects/awp-ca/docs/mockup/dkb_24.09.26/dkb_24.09.26.html | head -3; grep -o -E '.{0,10}[^A-Za-z_$]N=[A-Za-z_$]+=>.{0,250}' /Users/andrey.epifancev/projects/awp-ca/docs/mockup/dkb_24.09.26/dkb_24.09.26.html | grep -i inn | head", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (grep -o -E '.{0,150}Показать ЮЛ без ИНН' /Users/andrey.epifancev). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly.", "expected": null} +{"session": "e3c5dd5e", "ts": "2026-10-08T15:26:58.431Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/docs/mockup/dkb_24.09.26", "branch": "HEAD", "tool": "Write", "command": null, "file_path": "/Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/osv-inn-mapping-rework.md", "hook": "guard-main(Edit/Write)", "message": "PreToolUse:Write hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: you are on main. Create an isolated worktree first (superpowers:using-git-worktrees), never edit main directly.", "expected": null} +{"session": "e3c5dd5e", "ts": "2026-10-08T15:29:21.316Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/docs/mockup/dkb_24.09.26", "branch": "HEAD", "tool": "Bash", "command": "cat /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/feedback-no-harness-bypass.md /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/le-group-name-state.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "e44c4b65", "ts": "2026-10-02T21:55:49.335Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "echo \"=====openspec tree=====\" && find openspec -type f | sort && for f in $(find openspec -maxdepth 1 -type f) openspec/config.yaml openspec/AGENTS.md; do [ -f \"$f\" ] && { echo \"--- $f\"; cat \"$f\"; }; done 2>/dev/null; echo \"=====openspec specs/changes=====\" && find openspec/specs openspec/changes -type f 2>/dev/null | head -50; for f in $(find openspec/specs openspec/changes -type f 2>/dev/null | head -20); do echo \"--- $f\"; cat \"$f\"; done; echo \"=====setup=====\" && find setup -type f | sort && for f in $(find setup -type f | sort); do echo \"--- $f\"; cat \"$f\"; done", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (echo \"=====openspec tree=====\" && find openspec -type f | sort && for f in $(fin). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "e44c4b65", "ts": "2026-10-02T21:56:21.933Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cat \"$HOME/.claude/hooks/guard-main.sh\"; echo \"=====settings hooks=====\"; python3 -c \"import json;d=json.load(open('$HOME/.claude/settings.json'));print(json.dumps(d.get('hooks',{}),ensure_ascii=False,indent=1))\"; ls \"$HOME/.claude/hooks/\" \"$HOME/.claude/harness/\" 2>&1 | head -40", "file_path": null, "hook": "guard-main(Bash)", "message": " echo \"BLOCKED: shell write on $label ($(printf '%s' \"$cmd\" | head -c 80)). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly.\" >&2", "expected": null} +{"session": "e44c4b65", "ts": "2026-10-02T21:57:47.007Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cat \"$HOME/.claude/hooks/require-plan.sh\"; echo \"=====harness-check=====\"; cat \"$HOME/.claude/hooks/harness-check.sh\"; echo \"=====adopt-project=====\"; cat \"$HOME/.claude/harness/adopt-project.sh\"; echo \"=====project-template=====\"; find \"$HOME/.claude/harness/project-template\" -type f | head -30", "file_path": null, "hook": "require-plan", "message": "echo \"BLOCKED: editing $fpath without a plan. Run superpowers:brainstorming; for a bounded change create '$root/.bounded' first.\" >&2", "expected": null} +{"session": "e44c4b65", "ts": "2026-10-02T22:00:57.967Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca && WT=/Users/andrey.epifancev/projects/awp-ca/.wt/slice-0 && mkdir -p \"$WT\" && for r in docs arm-core tabular-parser scan-gateway; do git -C \"$r\" worktree add \"$WT/$r\" -b tmp/slice-0-skeleton 2>&1 | tail -1; done && ln -sfn ../../fixtures \"$WT/fixtures\" && ln -sfn ../../reference-old-arm \"$WT/reference-old-arm\" && ln -sfn ../../CLAUDE.md \"$WT/CLAUDE.md\" && ln -sfn ../../.claude \"$WT/.claude\" && ls -la \"$WT\" && for r in docs arm-core tabular-parser scan-gateway; do echo \"--- $r: $(git -C \"$WT/$r\" branch --show-current) @ $(git -C \"$WT/$r\" rev-parse --short HEAD)\"; done && git -C docs worktree list", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca && WT=/Users/andrey.epifancev/project). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "e44c4b65", "ts": "2026-10-04T09:29:49.924Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/fixtures/ocr", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca && cat fixtures/.gitignore; git -C fixtures log --oneline -3; git -C fixtures worktree add .wt/fixtures-ocr -b feat/1-ocr-fixtures-aep3-aep6 2>&1 | tail -1 && for f in aep_3.json aep_3.pdf aep_3_ofr.json aep_6.json; do cp \"fixtures/ocr/$f\" \".wt/fixtures-ocr/ocr/$f\" && cmp \"fixtures/ocr/$f\" \".wt/fixtures-ocr/ocr/$f\" && echo \"ok $f\"; done; python3 -c \"\nimport json\nfor f in ['aep_3.json','aep_3_ofr.json','aep_6.json']:\n d=json.load(open('.wt/fixtures-ocr/ocr/'+f)); print(f,'valid json, error=',d['error'])\"", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca && cat fixtures/.gitignore; git -C fi). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "e44c4b65", "ts": "2026-10-04T09:31:33.866Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/reference-old-arm/api/src", "branch": "HEAD", "tool": "Write", "command": null, "file_path": "/Users/andrey.epifancev/projects/awp-ca/.wt/slice-0/scan-gateway/docs/superpowers/plans/2026-10-04-multi-file-response.md", "hook": "guard-main(Edit/Write)", "message": "PreToolUse:Write hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: you are on main. Create an isolated worktree first (superpowers:using-git-worktrees), never edit main directly.", "expected": null} +{"session": "e44c4b65", "ts": "2026-10-04T09:31:36.996Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/reference-old-arm/api/src", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca/.wt/slice-0/scan-gateway && git add docs/superpowers/plans/2026-10-04-multi-file-response.md && git commit -q -F - <<'EOF'\nchore: план доработки scan-gateway по реальным ответам ИС (несколько json на задание, служебный каталог)\n\nРешения человека 04.10.2026 (OQ-76/77/78/79) и факты о формате ответа по новым фикстурам aep_3 и aep_6:\ndata_ocr — объект, успех = error.code == \"\", таблиц в одном json несколько.\n\nCo-Authored-By: Claude Sonnet 5.5 \nClaude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2\nEOF\ngit log --oneline -1; git status --short | wc -l | tr -d ' '", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca/.wt/slice-0/scan-gateway && git add d). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "e44c4b65", "ts": "2026-10-04T15:43:22.935Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/docs", "branch": "HEAD", "tool": "Bash", "command": "git add -A openspec && git diff --cached --name-only | grep -v '^openspec/' | wc -l | tr -d ' ' && git commit -q -F - <<'EOF'\nchore(openspec): архив change slice-0-skeleton (docs), спеки recognition-contract, dev-environment, quality-checks\n\nЗадачи 3.x, 5.1, 5.2 отмечены выполненными: исполнены в дочерних change'ах arm-core, tabular-parser, scan-gateway (PR #3 каждого).\n\nCo-Authored-By: Claude Sonnet 5.5 \nClaude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2\nEOF\ngit push -q origin main 2>&1 | tail -1; git status -sb | head -1; git log --oneline -1 | cut -c1-90", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (git add -A openspec && git diff --cached --name-only | grep -v '^openspec/' | wc). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "e44c4b65", "ts": "2026-10-04T15:44:07.480Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/docs", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca && for r in docs arm-core tabular-parser scan-gateway; do echo \"$r: dirty=$(git -C $r status --porcelain | wc -l | tr -d ' ') branch=$(git -C $r branch --show-current)\"; done; mkdir -p .wt/archive && for r in docs arm-core tabular-parser scan-gateway; do git -C $r worktree add ../.wt/archive/$r -b chore/archive-slice-0-skeleton 2>&1 | tail -1; done", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca && for r in docs arm-core tabular-par). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "e44c4b65", "ts": "2026-10-05T07:18:56.578Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/docs", "branch": "HEAD", "tool": "Bash", "command": "awk '/^## §3\\b|^## §3\\. /,0' context/06-test-data.md | head -120; echo '-----'; ls -la ../fixtures/osv ../fixtures/statements-xlsx ../fixtures/golden 2>/dev/null | head -60", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (awk '/^## §3\\b|^## §3\\. /,0' context/06-test-data.md | head -120; echo '-----'). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main dire", "expected": null} +{"session": "e44c4b65", "ts": "2026-10-06T08:16:10.000Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/tabular-parser/api", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca && git -C fixtures fetch -q origin && git -C fixtures worktree add -q /Users/andrey.epifancev/projects/awp-ca/.wt/dkb/fixtures -b wip/dkb origin/main && git -C tabular-parser fetch -q origin && git -C tabular-parser worktree add -q /Users/andrey.epifancev/projects/awp-ca/.wt/dkb/tabular-parser -b wip/dkb-golden origin/main && cd /Users/andrey.epifancev/projects/awp-ca/.wt/dkb/fixtures && ID=$(tea issues create --login home --repo RSHB/arm-ka-fixtures --title \"Пакет ДКБ 06.10.2026: ОСВ шести новых организаций, ББ/ОФР в Excel, план счетов 1С\" --description \"Разложить присланный пакет «Документы по дкб в формате xls» + ББ/ОФР/план счетов: osv//<период>/ для АО Цимлянский, ООО НКЗ, ООО Новоросметалл, ООО НПЗ, ООО Приманыческий, АО Агрокомплекс им. Ткачева, Счастливочная 1 кв 2025 (6 недостающих счетов); отчёты «Анализ счета» отдельно; statements-xlsx/ (Унагранде Ко, Унагранде Групп, НПЗ, Агрофирма Межениновская, шаблоны Минфина); directories/ план счетов 1С (488 строк, OQ-113); golden-снимки парсера; README. Дубли РБ 3 кв 2025 и Счастливочной 01/60 1 кв 2025 не добавлять.\" 2>&1 | grep -oE \"issues/[0-9]+\" | grep -oE \"[0-9]+\") && git branch -m wip/dkb feat/$ID-dkb-package && echo \"fixtures issue $ID; branch $(git branch --show-current)\"; ls /Users/andrey.epifancev/projects/awp-ca/.wt/dkb/", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca && git -C fixtures fetch -q origin &&). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "f44aee1b", "ts": "2026-10-07T19:04:37.820Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cat /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md | head -40; for r in arm-core arm-ka-docs arm-tabular-parser arm-scan-gateway arm-ka-fixtures; do echo \"=== $r\"; tea issues list --login home --repo RSHB/$r --state open --output simple 2>&1 | head -30; done", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "f66d8575", "ts": "2026-10-09T09:27:13.268Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca; cat ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/le-group-name-state.md ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md; git -C arm-core status -sb | head; git -C arm-core worktree list; grep -rn \"alias\" arm-core/src --include=*.py -il | head -30", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": null} +{"session": "259e4a45", "ts": "2026-10-11T17:05:38.793Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness", "branch": "main", "tool": "Bash", "command": "cat README.md ARCHITECTURE.md CHANGELOG.md docs/HARNESS.md .gitignore; echo ===HOOKS; cat hooks/settings.hooks.json hooks/guard-main.sh hooks/require-plan.sh hooks/harness-check.sh; echo ===SCRIPTS; cat install.sh adopt-project.sh new-project.sh; echo ===TEMPLATE; cat project-template/CLAUDE.md project-template/CLAUDE.harness-section.md project-template/ARCHITECTURE.md project-template/gitignore; echo ===DOCTOR; cat skills/harness-doctor/SKILL.md skills/harness-doctor/scripts/doctor.sh; echo ===OPENSPEC; cat openspec/config.yaml; echo ===CAVEMAN; cat caveman/config.json; echo ===GLOBAL; head -5 global/CLAUDE.md; diff global/CLAUDE.md ~/.claude/CLAUDE.md && echo SAME_AS_HOME", "file_path": null, "hook": "other", "message": "# Ожидается BLOCKED и предложение создать worktree.", "expected": null} +{"session": "259e4a45", "ts": "2026-10-11T17:05:41.652Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness", "branch": "main", "tool": "Bash", "command": "cat hooks/settings.hooks.json hooks/guard-main.sh hooks/require-plan.sh hooks/harness-check.sh", "file_path": null, "hook": "guard-main(Bash)", "message": " echo \"BLOCKED: shell write on $label ($(printf '%s' \"$cmd\" | head -c 80)). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly.\" >&2", "expected": null} +{"session": "259e4a45", "ts": "2026-10-11T17:05:46.774Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness", "branch": "main", "tool": "Bash", "command": "cat install.sh adopt-project.sh new-project.sh; echo ===TEMPLATE; cat project-template/CLAUDE.harness-section.md project-template/gitignore; echo ===DOCTOR-SKILL; cat skills/harness-doctor/SKILL.md; echo ===DOCTOR-SH-HEAD; head -40 skills/harness-doctor/scripts/doctor.sh; echo ===OPENSPEC; cat openspec/config.yaml; echo ===CAVEMAN; cat caveman/config.json; echo ===CLAUDE-DIR; ls .claude; cat .claude/settings*.json 2>/dev/null", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cat install.sh adopt-project.sh new-project.sh; echo ===TEMPLATE; cat project-te). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "259e4a45", "ts": "2026-10-11T17:06:14.575Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness/.wt/retro", "branch": "main", "tool": "Bash", "command": "cat docs/retro/2026-10-11.md", "file_path": null, "hook": "guard-main(Bash)", "message": "контроллер и всех его implementer-ов в состояние `BLOCKED: shell write on main`, и они останавливаются. Правило «не cd, пока", "expected": null} +{"session": "259e4a45", "ts": "2026-10-11T17:14:09.509Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness/.wt/retro", "branch": "retro/2026-10-11", "tool": "Bash", "command": "cat > CLAUDE.md <<'EOF'\n# claude-harness\n\n## Что это\nПакет обвязки для разработки с Claude Code: глобальный `CLAUDE.md` с процессом, три блокирующих хука, скрипты подключения проекта и skill диагностики. Для одного разработчика, который ведёт фичи через superpowers, хранит задачи в Gitea и требования в OpenSpec. Главный сценарий: `bash install.sh` ставит глобальную часть в `~/.claude`, `adopt-project.sh` подключает существующий проект, `new-project.sh` создаёт новый; дальше проект живёт по `docs/HARNESS.md`. Репозиторий — источник; установленные копии в `~/.claude` по месту не правятся.\n\n## Стек и ограничения\n- Bash (`set -euo pipefail`) и markdown. JSON из stdin хуков разбирается встроенным `python3`; других зависимостей времени выполнения нет. Внешние CLI (`tea`, `openspec`, `codegraph`) проверяются, но не требуются для установки.\n- Хук блокирует вызов только через `exit 2` с сообщением в stderr; `exit 0` — пропустить. Любой сбой разбора входа должен приводить к пропуску, не к блокировке.\n- `install.sh` идемпотентен: повторный запуск не ломает `settings.json` и делает бэкапы `CLAUDE.md` и `settings.json` с меткой времени.\n- Хуки работают из `~/.claude/hooks/`, пути в `settings.hooks.json` через `$HOME`; `CLAUDE_CONFIG_DIR` переопределяет `~/.claude`.\n- macOS и Linux; `python3` должен быть в PATH сессии Claude Code.\n\n## Архитектура\n- `hooks/` — поведение обвязки, единственный «код» с логикой. `guard-main.sh` (PreToolUse на Edit/Write/MultiEdit/Bash): запрет записи на main/master/detached HEAD, исключение для openspec-only коммитов. `require-plan.sh` (PreToolUse на Edit/Write/MultiEdit): правки в каталогах кода только при плане для ветки или файле `.bounded`. `harness-check.sh` (SessionStart): проверка, что проект подключён. `settings.hooks.json` — блок, который `install.sh` сливает в `~/.claude/settings.json`.\n- `install.sh`, `adopt-project.sh`, `new-project.sh` — установка и бутстрап. `install.sh` копирует `global/`, `hooks/`, `project-template/`, `adopt-project.sh`, `new-project.sh` и `skills/harness-doctor` в `~/.claude/{CLAUDE.md,hooks,harness,skills}`. `adopt-project.sh` работает на ветке `chore/harness-init` и вливает её ff в текущую ветку; это единственная разрешённая запись на main вне PR.\n- `global/CLAUDE.md` — глобальные правила процесса, ставятся в `~/.claude/CLAUDE.md`. `project-template/` — `CLAUDE.md`, `CLAUDE.harness-section.md` (секция «OpenSpec и superpowers», по ней `harness-check` узнаёт подключённый проект), `ARCHITECTURE.md`, `gitignore`.\n- `skills/harness-doctor/` — skill диагностики: `scripts/doctor.sh` снимает состояние машины и проекта и прогоняет самотест хуков; `SKILL.md` — чеклист прочтения отчёта.\n- `caveman/config.json` — выключенный по умолчанию caveman. `examples/` — реальный `CLAUDE.md` проекта-пилота.\n- `docs/HARNESS.md` — как устроен цикл фичи, SDD, маркировка, закрытие. `docs/PILOT-2026-09-27.md` — пилот. `docs/retro/` — ретроспектива 11.10.2026 и скрипты подсчёта по транскриптам. `docs/superpowers/specs/` — спеки фич обвязки, `docs/superpowers/plans/` — планы.\n- Связь слоёв: хуки читают JSON вызова из stdin и состояние git проекта; скрипты бутстрапа читают `project-template/`; `doctor.sh` читает установленные копии в `~/.claude`, не репозиторий.\n\n## Команды\n- `bash install.sh --check` — проверка окружения: бинарники, плагины, конфликты.\n- `bash install.sh` — установка глобальной части, идемпотентно.\n- `bash skills/harness-doctor/scripts/doctor.sh [проект]` — отчёт и самотест хуков. Только чтение.\n- `bash adopt-project.sh <папка> [--no-merge] [--push]` — подключить существующий проект. Из терминала, не из Claude Code.\n- `bash new-project.sh <папка> [ssh-url]` — каркас нового проекта.\n- `python3 docs/retro/scripts/retro_stats.py <каталог транскриптов>` и `retro_blocks.py` — статистика сессий и блокировок хуков для ретро.\n\n## Тесты\n- Отдельного набора тестов нет. Единственная автоматическая проверка — самотест хуков в `doctor.sh`: фиктивные вызовы Write/Bash на main и на ветке, `src/` с планом и без, `.bounded`. Прогоняется по установленным копиям, поэтому перед проверкой изменений в `hooks/` сначала `bash install.sh`.\n- Ручная проверка хука: на main в любом репозитории попросить агента создать файл, ожидается `BLOCKED`.\n- Регрессионные тесты хуков на реальных командах из транскриптов — бэклог ретро B2, пока не сделаны.\n\n## Текущая работа\nБэклог обвязки — `docs/retro/2026-10-11.md`, раздел 4 (B1–B12), решения владельца — раздел 5. Порядок: B1+B2 (guard-main по цели записи, тесты хуков), затем B5+B6 (роли и память, спека `docs/superpowers/specs/2026-10-11-roles-and-memory-design.md`), затем B3/B4/B10. Находки ретро маркируются `RT-n`.\n\n## OpenSpec и superpowers\n- Вход в любую задачу — superpowers:brainstorming. Skills openspec-propose и openspec-explore сами по себе не запускать.\n- Когда brainstorming классифицировал задачу как architectural, структуру change создать через `openspec new change ` (или `/opsx:propose `), а содержимое proposal.md, specs/ и design.md написать по итогам brainstorming. tasks.md из OpenSpec не заполнять — задачи живут в плане superpowers.\n- `/opsx:apply` не использовать: исполнение идёт через writing-plans и SDD/executing-plans.\n- `openspec archive ` — после merge PR, как часть чеклиста закрытия.\n- Хук require-plan защищает любой каталог `src/` на любой глубине. Если код лежит иначе — перечислить каталоги (префиксы от корня, по одному в строке) в `.claude/code-paths`. В этом репозитории код — `hooks/` и `skills/`, они перечислены в `.claude/code-paths`.\nEOF\nprintf 'hooks/\\nskills/\\n' > .claude/code-paths\necho '--- checks'; grep -c '{{' CLAUDE.md; grep -q '^## OpenSpec и superpowers' CLAUDE.md && echo section-ok; bash ~/.claude/skills/harness-doctor/scripts/doctor.sh . 2>&1 | sed -n '/## Проект/,$p'; echo '--- require-plan on hooks/ with .bounded'; printf '{\"cwd\":\"%s\",\"tool_input\":{\"file_path\":\"%s/hooks/x.sh\"}}' \"$PWD\" \"$PWD\" | bash ~/.claude/hooks/require-plan.sh; echo \"exit=$?\"; mv .bounded .bounded.off; printf '{\"cwd\":\"%s\",\"tool_input\":{\"file_path\":\"%s/hooks/x.sh\"}}' \"$PWD\" \"$PWD\" | bash ~/.claude/hooks/require-plan.sh; echo \"exit=$? (ожидалось 2)\"; mv .bounded.off .bounded; git status --short", "file_path": null, "hook": "require-plan", "message": "BLOCKED: editing /Users/andrey.epifancev/projects/cc-skills/claude-harness/.wt/retro/hooks/x.sh without a plan. Run superpowers:brainstorming; for a bounded change create '/Users/andrey.epifancev/projects/cc-skills/claude-harness/.wt/retro/.bounded' first.", "expected": null} +{"session": "daff409e", "ts": "2026-10-12T10:37:01.326Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness", "branch": "main", "tool": "Bash", "command": "cd ~ ; echo \"--- which -a\"; which -a claude; echo \"--- realpath\"; realpath \"$(which claude)\" 2>/dev/null; echo \"--- native\"; ls -la ~/.local/bin/claude 2>&1; echo \"--- npm prefix\"; P=$(npm -g config get prefix 2>/dev/null); echo \"$P\"; ls -d \"$P/lib/node_modules/@anthropic-ai/claude-code\" 2>&1; echo \"--- local\"; ls -d ~/.claude/local 2>&1; echo \"--- PATH has .local/bin?\"; echo \"$PATH\" | tr ':' '\\n' | grep -n \"\\.local/bin\"; echo \"--- version\"; claude --version; echo \"--- installMethod/autoUpdates\"; jq '{installMethod, autoUpdates, numStartups}' ~/.claude.json; echo \"--- env flags\"; env | grep -E '^(CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC|DISABLE_AUTOUPDATER|DISABLE_UPDATES)=' | cut -d= -f1", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd ~ ; echo \"--- which -a\"; which -a claude; echo \"--- realpath\"; realpath \"$(wh). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "daff409e", "ts": "2026-10-12T10:37:13.083Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness", "branch": "main", "tool": "Bash", "command": "grep -nE 'npm|realpath|cd |write|BLOCKED|pattern|case' ~/.claude/hooks/guard-main.sh | head -40", "file_path": null, "hook": "guard-main(Bash)", "message": "37: echo \"BLOCKED: shell write on $label ($(printf '%s' \"$cmd\" | head -c 80)). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly.\" >&2", "expected": null} +{"session": "daff409e", "ts": "2026-10-12T10:37:30.378Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness", "branch": "main", "tool": "Bash", "command": "echo \"--- user skills\"; ls ~/.claude/skills 2>&1; echo \"--- project skills\"; ls .claude/skills 2>&1; echo \"--- agents\"; ls ~/.claude/agents .claude/agents 2>&1; echo \"--- plugins\"; jq -r '.plugins | keys[]' ~/.claude/plugins/installed_plugins.json 2>/dev/null; echo \"--- CLAUDE.md files\"; ls -la ~/.claude/CLAUDE.md CLAUDE.md CLAUDE.local.md .claude/CLAUDE.md 2>&1; ls .claude/rules 2>&1; find . -name CLAUDE.md -not -path './node_modules/*' -not -path './.git/*'; wc -c ~/.claude/CLAUDE.md CLAUDE.md; ls", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (echo \"--- user skills\"; ls ~/.claude/skills 2>&1; echo \"--- project skills\"; ls ). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "daff409e", "ts": "2026-10-12T10:37:54.874Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness", "branch": "main", "tool": "Bash", "command": "ls ~/.claude/skills/synced | head; ls .claude/commands; ls .claude/commands/* | head; echo \"--- global drift\"; diff -q global/CLAUDE.md ~/.claude/CLAUDE.md; git check-ignore -v .wt; for d in ~/.claude/skills/*/ .claude/skills/*/; do f=\"$d/SKILL.md\"; [ -f \"$f\" ] || continue; awk 'NR==1&&$0!=\"---\"{print FILENAME\": no frontmatter\";exit} NR>1&&$0==\"---\"{exit} NR>1' \"$f\" > /dev/null; done", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (ls ~/.claude/skills/synced | head; ls .claude/commands; ls .claude/commands/* | ). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "daff409e", "ts": "2026-10-12T10:38:16.273Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness", "branch": "main", "tool": "Write", "command": null, "file_path": "/private/tmp/claude-501/-Users-andrey-epifancev-projects-cc-skills-claude-harness/daff409e-f6ff-4ead-b651-64cb360470eb/scratchpad/scan.py", "hook": "guard-main(Edit/Write)", "message": "PreToolUse:Write hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: you are on main. Create an isolated worktree first (superpowers:using-git-worktrees), never edit main directly.", "expected": null} +{"session": "daff409e", "ts": "2026-10-12T10:38:39.879Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness", "branch": "main", "tool": "Bash", "command": "python3 -I - <<'EOF'\nimport json, os, glob, re, collections, statistics, time\nhome = os.path.expanduser(\"~\")\ndef fm_check(path):\n lines = open(path, encoding=\"utf-8\").read().split(\"\\n\")\n if not lines or lines[0].strip() != \"---\":\n return \"no frontmatter\"\n try:\n end = lines[1:].index(\"---\") + 1\n except ValueError:\n return \"unterminated\"\n block = \"\\n\".join(lines[1:end])\n try:\n import yaml\n except ImportError:\n bad = [l for l in block.split(\"\\n\") if l and not l.startswith((\" \", \"\\t\", \"-\", \"#\")) and \":\" not in l]\n return \"no-yaml-lib; suspicious lines=%d\" % len(bad)\n try:\n d = yaml.safe_load(block)\n if not isinstance(d, dict): return \"not a mapping\"\n miss = [k for k in (\"name\",\"description\") if k not in d]\n return \"ok\" + (\" missing \" + \",\".join(miss) if miss else \"\") + \" desc=%d\" % len(str(d.get(\"description\",\"\")))\n except Exception as e:\n return \"YAML ERROR: \" + str(e).split(\"\\n\")[0]\nprint(\"== skill frontmatter\")\nfor p in glob.glob(home + \"/.claude/skills/*/SKILL.md\") + glob.glob(\".claude/skills/*/SKILL.md\"):\n print(\" \", p.replace(home, \"~\"), \"=\", fm_check(p))\nfiles = sorted(glob.glob(home + \"/.claude/projects/*/*.jsonl\"), key=os.path.getmtime, reverse=True)[:50]\nmt = [os.path.getmtime(f) for f in files]\nprint(\"\\n== window: %d sessions, %.1f days (%s .. %s)\" % (len(files), (max(mt)-min(mt))/86400, time.strftime(\"%Y-%m-%d\", time.localtime(min(mt))), time.strftime(\"%Y-%m-%d\", time.localtime(max(mt)))))\nskills=collections.Counter(); slash=collections.Counter(); mcp=collections.Counter()\nhooks=collections.defaultdict(list); hto=collections.Counter()\nden=collections.Counter(); dk=collections.defaultdict(collections.Counter)\ncre=re.compile(r\"/?([^<]+)\")\nfor fp in files:\n tu={}; pend=[]\n for line in open(fp, encoding=\"utf-8\", errors=\"replace\"):\n try: o=json.loads(line)\n except Exception: continue\n t=o.get(\"type\"); msg=o.get(\"message\") or {}; c0=msg.get(\"content\") if isinstance(msg, dict) else None\n if t==\"assistant\" and isinstance(c0,list):\n for c in c0:\n if isinstance(c,dict) and c.get(\"type\")==\"tool_use\":\n n=c.get(\"name\",\"\"); inp=c.get(\"input\") or {}; tu[c.get(\"id\")]=(n,inp)\n if n==\"Skill\": skills[str(inp.get(\"skill\"))]+=1\n elif n.startswith(\"mcp__\"): mcp[n.split(\"__\")[1]]+=1\n elif t==\"user\":\n s=c0 if isinstance(c0,str) else json.dumps(c0) if c0 else \"\"\n for m in cre.findall(s): slash[m.strip()]+=1\n kind=o.get(\"toolDenialKind\")\n if isinstance(c0,list):\n for c in c0:\n if isinstance(c,dict) and c.get(\"type\")==\"tool_result\":\n if kind and kind not in (\"interrupted\",\"cancelled\"): pend.append((c.get(\"tool_use_id\"),kind,True))\n elif not kind and c.get(\"is_error\"):\n x=json.dumps(c.get(\"content\"))\n if \"want to proceed\" in x or \"Permission to use\" in x or \"Permission for this\" in x: pend.append((c.get(\"tool_use_id\"),\"fallback\",False))\n elif t==\"attachment\":\n a=o.get(\"attachment\") or {}\n if str(a.get(\"type\",\"\")).startswith(\"hook_\"):\n k=(a.get(\"hookEvent\"), (a.get(\"command\") or a.get(\"hookName\") or \"\")[-45:])\n if a.get(\"durationMs\") is not None: hooks[k].append(a[\"durationMs\"])\n if a.get(\"type\")==\"hook_cancelled\" and a.get(\"timedOut\"): hto[k]+=1\n for tid,kind,st in pend:\n n,inp=tu.get(tid,(\"?\",{}))\n if n.startswith(\"mcp__\") and not st: continue\n k=(\"Bash: \"+\" \".join(str(inp.get(\"command\",\"\")).split()[:2])[:50]) if n==\"Bash\" else n\n den[k]+=1; dk[k][kind]+=1\nprint(\"== Skill calls:\", dict(skills.most_common()))\nprint(\"== slash:\", dict(slash.most_common(40)))\nprint(\"== MCP by server:\", dict(mcp.most_common()))\nprint(\"\\n== hooks: n median max\")\nfor k,v in sorted(hooks.items(), key=lambda kv: -statistics.median(kv[1])): print(\" \",k,len(v),int(statistics.median(v)),max(v))\nprint(\"== hook timeouts:\", dict(hto))\nprint(\"\\n== denials\")\nfor k,n in den.most_common(25): print(\" \",k,n,dict(dk[k]))\nEOF", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (python3 -I - <<'EOF'", "expected": null} +{"session": "e9960c90", "ts": "2026-10-12T10:59:13.940Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness", "branch": "main", "tool": "Bash", "command": "git fetch origin --prune 2>&1 | tail -3; git log --oneline -6 origin/main; echo ---; git branch -r; echo ---; git merge-base --is-ancestor origin/retro/2026-10-11 origin/main 2>/dev/null && echo \"retro is ancestor of main\" || echo \"retro NOT ancestor (squash/rebase?)\"; tea pulls edit --help 2>&1 | head -30", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (git fetch origin --prune 2>&1 | tail -3; git log --oneline -6 origin/main; echo ). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "e9960c90", "ts": "2026-10-12T11:02:22.931Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness/.wt/roles", "branch": "main", "tool": "Bash", "command": "sed -n 1,45p ~/.claude/hooks/guard-main.sh", "file_path": null, "hook": "guard-main(Bash)", "message": " echo \"BLOCKED: shell write on $label ($(printf '%s' \"$cmd\" | head -c 80)). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly.\" >&2", "expected": null} +{"session": "e9960c90", "ts": "2026-10-12T11:03:38.383Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness", "branch": "main", "tool": "Write", "command": null, "file_path": "/Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-cc-skills-claude-harness/memory/roles-and-memory-prs.md", "hook": "guard-main(Edit/Write)", "message": "PreToolUse:Write hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: you are on main. Create an isolated worktree first (superpowers:using-git-worktrees), never edit main directly.", "expected": null} +{"session": "e9960c90", "ts": "2026-10-12T11:09:44.026Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness", "branch": "main", "tool": "Bash", "command": "grep -rniE 'guard-main' docs/retro/2026-10-11.md docs/superpowers/specs/*.md CHANGELOG.md 2>/dev/null | head -20; echo ----; tea issues list --state open 2>&1 | head -8; echo ----; ls hooks; grep -n 'file_path' hooks/guard-main.sh", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (grep -rniE 'guard-main' docs/retro/2026-10-11.md docs/superpowers/specs/*.md CHA). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": null} +{"session": "e9960c90", "ts": "2026-10-12T11:25:08.186Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness/.wt/guard", "branch": "tmp/guard-main", "tool": "Bash", "command": "sed -n 38,75p docs/retro/2026-10-11.md; grep -nE '^\\*\\*RT-(6|7)' -A6 docs/retro/2026-10-11.md", "file_path": null, "hook": "guard-main(Bash)", "message": "контроллер и всех его implementer-ов в состояние `BLOCKED: shell write on main`, и они останавливаются. Правило «не cd, пока", "expected": null} +{"session": "e9960c90", "ts": "2026-10-12T11:25:14.192Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness/.wt/guard", "branch": "tmp/guard-main", "tool": "Bash", "command": "ls docs/retro/scripts; sed -n 1,200p docs/retro/scripts/retro_blocks.py", "file_path": null, "hook": "guard-main(Bash)", "message": " if \"BLOCKED\" in txt and (\"hook\" in txt.lower() or \"BLOCKED:\" in txt[:400]):", "expected": null} +{"session": "e9960c90", "ts": "2026-10-12T11:26:42.248Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness/.wt/guard", "branch": "feat/6-guard-main-target", "tool": "Bash", "command": "python3 docs/retro/scripts/hook_blocks_export.py /private/tmp/claude-501/-Users-andrey-epifancev-projects-cc-skills-claude-harness/e9960c90-6445-4fa5-b76f-0b371dfa85b2/scratchpad/fake-tr", "file_path": null, "hook": "guard-main(Bash)", "message": "{\"session\": \"s1.jsonl\", \"ts\": \"t1\", \"cwd\": \"/r\", \"branch\": \"main\", \"tool\": \"Bash\", \"command\": \"grep x 2>/dev/null; tea login add --token \"file_path\": null, \"hook\": \"guard-main(Bash)\", \"message\": \"PreToolUse:Bash hook error: BLOCKED: shell write on main (grep)\", \"expected\": null}", "expected": null} diff --git a/docs/retro/scripts/hook_blocks_export.py b/docs/retro/scripts/hook_blocks_export.py new file mode 100644 index 0000000..96b47ef --- /dev/null +++ b/docs/retro/scripts/hook_blocks_export.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Выгрузка блокировок хуков из транскриптов Claude Code в JSONL — сырьё для регрессионных тестов хуков (B2). + +Запуск: python3 hook_blocks_export.py <каталог-транскриптов> [<каталог> ...] > blocks.jsonl +Одна строка — одна блокировка: сессия, время, cwd и ветка на момент вызова, инструмент, команда или путь файла, +первая строка ответа хука. Поле expected не заполняется: вердикт (block/allow) проставляется при разметке. +Выгружаются только ошибки хуков (результат начинается с `PreToolUse:<инструмент> hook error`); вывод команд, +в котором встретилось слово BLOCKED (файлы памяти, исходники хуков), не попадает. +Похожие на секреты подстроки заменяются на . +""" +import json, sys, glob, os, re + +HOOK_ERROR = re.compile(r"\s*PreToolUse:\S+ hook error") + +RE_SECRET = re.compile( + r'(?i)gh[pousr]_[A-Za-z0-9]{20,}|sk-[A-Za-z0-9_-]{20,}|\b[0-9a-f]{40}\b' + r'|(?P(?:token|password|passwd|secret|api[_-]?key)\s*[=:]\s*)\S+' + r'|(?P--(?:token|password)\s+)\S+|(?PAuthorization:\s*\S+\s+)\S+' +) + + +def redact(s): + def sub(m): + keep = m.group("kv") or m.group("flag") or m.group("auth") or "" + return keep + "" + return RE_SECRET.sub(sub, s) + + +def hook_of(txt, tool): + if "[guard-main]" in txt: + return "guard-main(Bash)" if tool == "Bash" else "guard-main(Edit/Write)" + if "[require-plan]" in txt: + return "require-plan" + if "shell write" in txt: # формулировки до 0.4.0 + return "guard-main(Bash)" + if "you are on" in txt: + return "guard-main(Edit/Write)" + if "without a plan" in txt: + return "require-plan" + if "commit/push в main" in txt or "ветке main" in txt or "reference-old-arm" in txt or "docs/source" in txt: + return "workspace-guard" + return "other" + + +for d in sys.argv[1:]: + for f in sorted(glob.glob(os.path.join(os.path.expanduser(d), "*.jsonl"))): + sid = os.path.basename(f)[:8] + pending = {} # tool_use_id -> (name, input, cwd, branch, ts) + for line in open(f, encoding="utf-8", errors="replace"): + try: + j = json.loads(line) + except Exception: + continue + m = j.get("message") or {} + cont = m.get("content") + if j.get("type") == "assistant": + for b in cont or []: + if isinstance(b, dict) and b.get("type") == "tool_use": + pending[b.get("id")] = (b.get("name"), b.get("input") or {}, j.get("cwd", ""), + j.get("gitBranch", ""), j.get("timestamp", "")) + elif j.get("type") == "user" and isinstance(cont, list): + for b in cont: + if not (isinstance(b, dict) and b.get("type") == "tool_result"): + continue + txt = b.get("content", "") + if isinstance(txt, list): + txt = "\n".join(x.get("text", "") for x in txt if isinstance(x, dict)) + if not HOOK_ERROR.match(txt): + continue # обычный вывод команды, даже если в нём есть слово BLOCKED + name, inp, cwd, branch, ts = pending.get(b.get("tool_use_id"), ("?", {}, "", "", "")) + if name not in ("Bash", "Edit", "Write", "MultiEdit"): + continue + msg = next((l for l in txt.splitlines() if "BLOCKED" in l), "") + print(json.dumps({ + "session": sid, "ts": ts, "cwd": cwd, "branch": branch, "tool": name, + "command": redact(inp.get("command", "")) if name == "Bash" else None, + "file_path": inp.get("file_path") if name != "Bash" else None, + "hook": hook_of(txt, name), "message": redact(msg[:300]), "expected": None, + }, ensure_ascii=False)) diff --git a/docs/retro/scripts/hook_cases_build.py b/docs/retro/scripts/hook_cases_build.py new file mode 100644 index 0000000..23ff2d8 --- /dev/null +++ b/docs/retro/scripts/hook_cases_build.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +"""Сборка регрессионного корпуса хуков из сырой выгрузки блокировок (B2, #6). + +Запуск из корня репозитория: + python3 docs/retro/scripts/hook_cases_build.py > skills/harness-doctor/fixtures/hook-cases.jsonl +Вход: 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, а не прогоном хука. +""" +import json +import os +import re +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +RAW = os.path.join(HERE, "..", "fixtures", "hook-blocks.jsonl") +HOME = "/Users/andrey.epifancev" +AW, CH = "projects/awp-ca", "projects/cc-skills/claude-harness" + +LAYOUT = [{"path": AW}, {"path": AW + "/reference-old-arm"}] +LAYOUT += [{"path": "%s/%s" % (AW, r), "branch": "main"} + for r in ("arm-core", "docs", "tabular-parser", "scan-gateway", "arm-web", "fixtures")] +LAYOUT += [{"path": "%s/.wt/%s/%s" % (AW, n, r), "worktree_of": "%s/%s" % (AW, r), "branch": "feat/" + n} + for n, r in (("web-proto", "arm-core"), ("web-proto", "arm-web"), ("ocr-q1", "fixtures"), + ("ocr-q1", "scan-gateway"), ("stmt-template", "arm-web"), ("qa-guide-web", "docs"), + ("slice-0", "scan-gateway"), ("slice-3b", "arm-core"))] +LAYOUT += [{"path": CH, "branch": "main"}] +LAYOUT += [{"path": "%s/.wt/%s" % (CH, n), "worktree_of": CH, "branch": b} + for n, b in (("retro", "retro/2026-10-11"), ("roles", "feat/2-roles-and-memory"), + ("guard", "feat/6-guard-main-target"))] +LAYOUT += [{"path": ".claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory"}, + {"path": ".claude/projects/-Users-andrey-epifancev-projects-cc-skills-claude-harness/memory"}, + {"path": ".claude/hooks"}, {"path": ".claude/jobs"}] + +# Блок по спеке: архивный коммит openspec прямо в main (решение владельца 1 — архив отдельным PR). +BLOCK = {"c031", "c069", "c070", "c103"} +# Класс для записей, где он не выводится общим правилом ниже. +WHY = { + "c002": "unparseable", "c008": "var-target", "c023": "outside-repo", "c026": "outside-repo", + "c029": "worktree", "c030": "outside-repo", "c040": "worktree-setup", "c044": "worktree", "c045": "worktree", + "c055": "outside-repo", "c064": "worktree-setup", "c072": "switch-then-commit", "c073": "worktree-setup", + "c078": "outside-repo", "c080": "heredoc", "c081": "heredoc", "c083": "outside-repo", "c091": "heredoc", + "c099": "worktree-setup", "c100": "worktree-setup", "c101": "worktree", "c102": "worktree", + "c104": "worktree-setup", "c106": "worktree-setup", "c119": "heredoc", +} +# Ложные блоки этой сессии (15.10.2026), до перехода в worktree: cwd — основной checkout на main. +LIVE_RE = ('git show origin/feat/6-guard-main-target:docs/retro/fixtures/hook-blocks.jsonl | python3 -c \'\n' + 'import sys,json,re,collections\n' + 'rs=[json.loads(l) for l in sys.stdin if l.strip()]\n' + 'c=collections.Counter()\n' + 'for r in rs:\n' + ' cmd=r.get("command") or ""\n' + ' if re.search(r"\\d>{1,2}\\s*/dev/null|\\d>&\\d",cmd): c["bash: N>/dev/null или N>&M"]+=1\n' + ' if re.search(r"mkdir\\s+-p\\s+\\S*\\.wt/",cmd): c["bash: mkdir -p .wt/"]+=1\n' + ' if re.search(r"\\bsed\\s+-i",cmd): c["bash: sed -i"]+=1\n' + 'for k,v in c.most_common(): print(f"{v:4} {k}")\'') +LIVE = [ + ("l001", "grep-pattern", 'grep -rn "npm i\\|npm install\\|brew install\\|plugin install\\|go install" ' + '--include="*.md" --include="*.sh" . | grep -v "^./docs/retro/2026" | head -20'), + ("l002", "devnull", 'grep -rn "plugin add\\|marketplace\\|caveman@\\|superpowers@" --include="*.md" ' + '--include="*.sh" README.md docs global install.sh 2>/dev/null | head -20'), + ("l003", "regex-text", LIVE_RE), +] +DEVNULL = re.compile(r"\d>{1,2}\s*/dev/null|\d>&\d") + + +def why_of(cid, r): + if cid in BLOCK: + return "main-git" + if cid in WHY: + return WHY[cid] + if "hook error" not in (r.get("message") or ""): + return "not-a-block" # вывод успешной команды, где встретилось слово BLOCKED + if r["tool"] != "Bash": + return "outside-repo" + return "devnull" if DEVNULL.search(r.get("command") or "") else "read-only" + + +def main(): + print(json.dumps({"home": HOME, "layout": LAYOUT}, ensure_ascii=False)) + for n, line in enumerate(l for l in open(RAW, encoding="utf-8") if l.strip()): + r = json.loads(line) + cid = "c%03d" % (n + 1) + r.update({"id": cid, "expected": "block" if cid in BLOCK else "allow", "why": why_of(cid, r)}) + print(json.dumps(r, ensure_ascii=False)) + for cid, why, cmd in LIVE: + print(json.dumps({"id": cid, "session": "live-1015", "cwd": HOME + "/" + CH, "branch": "main", + "tool": "Bash", "command": cmd, "file_path": None, "hook": "guard-main(Bash)", + "message": "", "expected": "allow", "why": why}, ensure_ascii=False)) + + +if __name__ == "__main__": + main() diff --git a/docs/retro/scripts/retro_blocks.py b/docs/retro/scripts/retro_blocks.py index 21481a8..9c453d7 100644 --- a/docs/retro/scripts/retro_blocks.py +++ b/docs/retro/scripts/retro_blocks.py @@ -8,6 +8,7 @@ files = sorted(glob.glob(os.path.join(d, "*.jsonl"))) RE_REAL = re.compile(r'(^|[^<\d])>{1,2}[^&]|\btee\b|\bsed\s+-i|\bmv\b|\bcp\b|\brm\b|\btouch\b|\bmkdir\b|\bgit\s+(add|commit|push|apply|stash\s+pop)\b|\bnpm\s+(i|install|init)\b|\bpip\s+install\b') RE_STDERR = re.compile(r'\d>{1,2}\s*/dev/null|\d>&\d') RE_HEREDOC_PY = re.compile(r"<<\s*'?(PY|EOF)") +RE_HOOK_ERROR = re.compile(r"\s*PreToolUse:\S+ hook error") # только ошибка хука, не вывод команды со словом BLOCKED blocks = collections.Counter() by_hook = collections.Counter() @@ -63,10 +64,14 @@ for f in files: if isinstance(txt, list): txt = "\n".join(x.get("text", "") for x in txt if isinstance(x, dict)) name, inp = pending.get(b.get("tool_use_id"), ("?", {})) - if "BLOCKED" in txt and ("hook" in txt.lower() or "BLOCKED:" in txt[:400]): + if RE_HOOK_ERROR.match(txt) and "BLOCKED" in txt: if name not in ("Bash", "Edit", "Write", "MultiEdit"): continue - if "shell write" in txt: + if "[guard-main]" in txt: + hook = "guard-main(Bash)" if name == "Bash" else "guard-main(Edit/Write)" + elif "[require-plan]" in txt: + hook = "require-plan" + elif "shell write" in txt: hook = "guard-main(Bash)" elif "you are on" in txt: hook = "guard-main(Edit/Write)" diff --git a/docs/superpowers/plans/2026-10-16-hooks-write-target.md b/docs/superpowers/plans/2026-10-16-hooks-write-target.md new file mode 100644 index 0000000..d8b8603 --- /dev/null +++ b/docs/superpowers/plans/2026-10-16-hooks-write-target.md @@ -0,0 +1,1603 @@ +# Хуки решают по цели записи — план реализации + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** guard-main и require-plan решают по цели записи (файл, цели команды Bash, git-мутации) и ветке её репозитория, а не по cwd; исключение openspec-only удалено; у хуков есть регрессионный корпус реальных команд, который гоняют тесты пакета и `doctor.sh`. + +**Architecture:** Новое ядро `hooks/harness_target.py` (stdlib): `parse` разбирает команду Bash на цели без файловой системы, `resolve` находит корень и ветку репозитория пути, `decide_guard` / `decide_plan` выносят вердикт. `guard-main.sh` и `require-plan.sh` становятся обёртками, регистрация в `settings.json` не меняется. Корпус — `skills/harness-doctor/fixtures/hook-cases.jsonl` (собирается `docs/retro/scripts/hook_cases_build.py` из сырой выгрузки), прогон в песочнице — `skills/harness-doctor/scripts/hook_corpus.py`. + +**Tech Stack:** bash, python3 stdlib (3.8+), git; тесты — `tests/run.sh` (bash, без зависимостей). + +**Spec:** `openspec/changes/hooks-write-target/` — `proposal.md`, `specs/hook-guards/spec.md`, `design.md`. Issue #6, ветка `feat/6-guard-main-target`. + +**Worktree:** `/Users/andrejepifancev/Projects/cc-harness.wt/6-guard-main-target` — все пути ниже от его корня, все команды — из него. + +## Global Constraints + +- Хук блокирует только через `exit 2` с сообщением в stderr; любой сбой (нет `python3`, нет `harness_target.py`, невалидный JSON, исключение разбора, сбой git) — `exit 0`. +- Только stdlib python3, совместимость с Python 3.8 (на машине 3.9.6): без `match`, без `X | Y` в аннотациях, без `str.removeprefix`. +- Защищены ветки `main`, `master` и detached HEAD. +- Сообщения блокировки начинаются с `BLOCKED [guard-main]:` или `BLOCKED [require-plan]:` и называют цель, корень репозитория и выход. +- Точки входа — `hooks/guard-main.sh`, `hooks/require-plan.sh`; `hooks/settings.hooks.json` не меняется. +- Исключений для `openspec/` и `openspec archive` нет (решение владельца 1, 11.10.2026). +- Зелёным считается только весь набор `bash tests/run.sh`. +- Коммиты — в ветке `feat/6-guard-main-target`, trailer `Co-Authored-By: Claude Opus 5.5 `. Push и PR — после задачи 5. +- Тексты документации и сообщений — по-русски. + +## Предусловия окружения + +- Сессия в worktree `/Users/andrejepifancev/Projects/cc-harness.wt/6-guard-main-target` на ветке `feat/6-guard-main-target`, дерево чистое (`git status --short` пуст). +- `python3 --version` ≥ 3.8, `git --version` ≥ 2.22 (`branch --show-current`). +- `bash tests/run.sh` на старте зелёный (157 passed). +- Стенд, порты, ключи API не нужны. + +## Review Focus + +1. Путь с пробелом в кавычках (`touch "a b.md"`) — одна цель с пробелом, а не две. Тест: задача 1 (разбор), задача 2 (блок в main). +2. Запись через симлинк, в том числе висячий, указывающий в main — должна блокироваться по цели ссылки, а не по её каталогу. Тест: задача 2. +3. Пустой `cwd` в JSON хука и относительный `file_path` — пропуск без исключения. Тест: задача 2. +4. `cd ~/…` и относительный `git -C sub` после `cd` — путь считается от нового каталога. Тест: задача 1. +5. Запись внутри `.git/` (`.git/info/exclude`) — это не рабочее дерево ветки, пропуск. Тест: задача 2. + +## Файлы + +| Файл | Задача | Что | +|---|---|---| +| `hooks/harness_target.py` | 1 | создать: лексер, `parse`, `resolve`, `decide_guard`, `decide_plan`, CLI | +| `tests/test-hook-parse.sh` | 1 | создать | +| `hooks/guard-main.sh` | 2 | заменить телом-обёрткой | +| `tests/test-hook-guard-main.sh` | 2 | создать | +| `hooks/require-plan.sh` | 3 | заменить телом-обёрткой | +| `tests/test-hook-require-plan.sh` | 3 | создать | +| `docs/retro/scripts/hook_cases_build.py` | 4 | создать | +| `skills/harness-doctor/fixtures/hook-cases.jsonl` | 4 | сгенерировать | +| `skills/harness-doctor/scripts/hook_corpus.py` | 4 | создать | +| `tests/test-hook-corpus.sh`, `tests/test-hook-export.sh` | 4 | создать | +| `docs/retro/scripts/hook_blocks_export.py`, `docs/retro/scripts/retro_blocks.py` | 4 | фильтр ошибок хука | +| `install.sh`, `skills/harness-doctor/scripts/doctor.sh`, `skills/harness-doctor/SKILL.md` | 5 | ядро и корпус | +| `tests/test-install.sh`, `tests/test-doctor.sh` | 5 | новые проверки | +| `README.md`, `docs/HARNESS.md`, `CLAUDE.md`, `roles/dev.md`, `CHANGELOG.md` | 5 | документация | + +--- + +### Task 1: Ядро `harness_target.py` и разбор команды + +**Files:** +- Create: `hooks/harness_target.py` +- Test: `tests/test-hook-parse.sh` + +**Interfaces:** +- Produces: + - `Target(kind, path, op)` — namedtuple; `kind` ∈ `file|git|switch`; `path` — абсолютный путь или `None` (не разрешился); для `switch` `path` — каталог репозитория, `op` — новая ветка или `None`. + - `parse(cmd: str, cwd: str | None) -> list[Target]` — цели в порядке выполнения; бросает `ValueError` на незакрытой кавычке/подстановке. + - `resolve(path: str) -> (root: str, branch: str, real: str) | None`; `branch == ""` — detached HEAD. + - `decide_guard(tool: str, tool_input: dict, cwd: str) -> str | None` — сообщение блокировки guard-main. + - `decide_plan(file_path: str, cwd: str) -> str | None` — сообщение блокировки require-plan. + - CLI: `python3 hooks/harness_target.py targets '' [cwd]` → строки `kind path op` (`-` для `None`), при ошибке разбора — `error <текст>` и exit 1; `guard-main` / `require-plan` — JSON вызова из stdin → exit 0/2. + +- [ ] **Step 1: Написать тест разбора** + +Создать `tests/test-hook-parse.sh`: + +```bash +# harness_target.py targets: разбор команды Bash на цели записи, без файловой системы (спека hook-guards). +P="$ROOT/hooks/harness_target.py" +assert_file "harness_target.py существует" "$P" +t(){ python3 "$P" targets "$1" /r 2>&1; } +NL=$'\n' +# чтение и дескрипторы +assert_eq "grep с текстом «npm i» — целей нет" "$(t 'grep -rn "npm i\|npm install" docs | head')" "" +assert_eq "2>/dev/null и 2>&1 — не цели" "$(t 'ls x 2>/dev/null; git fetch origin --prune 2>&1 | tail -3')" "" +assert_eq ">&2 и 1>&- — не цели" "$(t 'echo x >&2; echo y 1>&-')" "" +assert_eq "регулярка с > в кавычках" "$(t "python3 -c 're.search(r\"\\d>{1,2}\", s); mkdir'")" "" +assert_eq "комментарий" "$(t 'echo x # > f')" "" +# редиректы +assert_eq "редирект" "$(t 'echo x > notes.md')" "file /r/notes.md >" +assert_eq "дозапись по абсолютному пути" "$(t 'echo x >> /tmp/a.log')" "file /tmp/a.log >>" +assert_eq "stderr в файл" "$(t 'make 2>build.log')" "file /r/build.log 2>" +assert_eq "echo 2 > f — 2 это аргумент" "$(t 'echo 2 > f')" "file /r/f >" +assert_eq "&> в файл" "$(t 'cmd &> out.log')" "file /r/out.log &>" +assert_eq "heredoc: цель — файл, тело не разбирается" "$(t "cat > CLAUDE.md <<'EOF'${NL}a > b${NL}rm -rf x${NL}EOF${NL}echo ok")" "file /r/CLAUDE.md >" +# команды записи +assert_eq "tee -a" "$(t 'echo x | tee -a a.log b.log')" "file /r/a.log tee${NL}file /r/b.log tee" +assert_eq "cp — назначение" "$(t 'cp a b dst/')" "file /r/dst cp" +assert_eq "cp -t" "$(t 'cp -t out a b')" "file /r/out cp" +assert_eq "mv — источник и назначение" "$(t 'mv a b')" "file /r/a mv${NL}file /r/b mv" +assert_eq "rm -rf" "$(t 'rm -rf build')" "file /r/build rm" +assert_eq "chmod — без режима" "$(t 'chmod +x run.sh')" "file /r/run.sh chmod" +assert_eq "mkdir -p .wt/" "$(t 'mkdir -p .wt/6-x')" "file /r/.wt/6-x mkdir" +assert_eq "glob — каталог до метасимвола" "$(t 'rm docs/*.md')" "file /r/docs rm" +assert_eq "sed -i" "$(t "sed -i 's/a/b/' x.md")" "file /r/x.md sed -i" +assert_eq "sed -i '' (BSD)" "$(t "sed -i '' 's/a/b/' x.md")" "file /r/x.md sed -i" +assert_eq "sed -i -e, два файла" "$(t 'sed -i -e s/a/b/ x.md y.md')" "file /r/x.md sed -i${NL}file /r/y.md sed -i" +assert_eq "sed -n — чтение" "$(t 'sed -n 1,5p x.md')" "" +assert_eq "perl -pi -e" "$(t "perl -pi -e 's/a/b/' x")" "file /r/x perl -i" +assert_eq "dd of=" "$(t 'dd if=a of=/r/b.img')" "file /r/b.img dd" +assert_eq "tar -x -C" "$(t 'tar -xzf a.tgz -C out')" "file /r/out tar -x" +assert_eq "tar c — не запись" "$(t 'tar czf a.tgz src')" "" +assert_eq "unzip -d" "$(t 'unzip a.zip -d out')" "file /r/out unzip" +assert_eq "unzip -l — чтение" "$(t 'unzip -l a.zip')" "" +assert_eq "npm install — текущий каталог" "$(t 'npm install')" "file /r npm install" +assert_eq "npm -g install — не репозиторий" "$(t 'npm -g install x')" "" +assert_eq "pip install — не репозиторий" "$(t 'pip install requests')" "" +assert_eq "openspec archive" "$(t 'openspec archive x')" "file /r openspec archive" +assert_eq "openspec validate — чтение" "$(t 'openspec validate x --strict')" "" +assert_eq "docker … sh -c — не локальная запись" "$(t "docker compose exec -T db sh -c 'psql > /tmp/x'")" "" +assert_eq "префиксы sudo/env/присваивания" "$(t 'sudo rm /etc/x; env A=1 touch f; A=1 B=2 touch g')" "file /etc/x rm${NL}file /r/f touch${NL}file /r/g touch" +assert_eq "xargs rm — цели неизвестны" "$(t 'xargs rm < list')" "file - rm" +assert_eq "if/then" "$(t 'if [ -f x ]; then rm x; fi')" "file /r/x rm" +# каталог +assert_eq "cd && запись" "$(t 'cd /w && echo x > f')" "file /w/f >" +assert_eq "cd относительный ;" "$(t 'cd sub; touch f')" "file /r/sub/f touch" +assert_eq "( cd ) не протекает" "$(t '(cd /w; touch a); touch b')" "file /w/a touch${NL}file /r/b touch" +assert_eq "cd в конвейере не протекает" "$(t 'cd /w | cat; touch b')" "file /r/b touch" +assert_eq "pushd/popd" "$(t 'pushd /w && touch a && popd && touch b')" "file /w/a touch${NL}file /r/b touch" +assert_eq "cd - — каталог неизвестен" "$(t 'cd - && touch a')" "file - touch" +assert_eq "bash -c разбирается" "$(t "bash -c 'echo x > f'")" "file /r/f >" +assert_eq "eval разбирается" "$(t 'eval "rm x"')" "file /r/x rm" +# переменные и HOME +assert_eq "цель в переменной" "$(t 'echo x > "$OUT"')" "file - >" +assert_eq "\$HOME раскрывается" "$(t 'echo x > $HOME/f')" "file $HOME/f >" +assert_eq "~ раскрывается" "$(t 'echo x > ~/f')" "file $HOME/f >" +assert_eq "ln в переменную" "$(t 'ln -sfn ../../fixtures "$WT/fixtures"')" "file - ln" +# git +assert_eq "git add && commit" "$(t 'git add -A && git commit -m x')" "git /r add${NL}git /r commit" +assert_eq "git -C" "$(t 'git -C /w commit -m x')" "git /w commit" +assert_eq "git -C в переменной" "$(t 'for r in a b; do git -C "$r" commit -m x; done')" "git - commit" +assert_eq "git --git-dir" "$(t 'git --git-dir=/x/.git commit -m x')" "git - commit" +assert_eq "worktree, pull, fetch, log — не мутации" "$(t 'git worktree add ../w -b f; git pull --ff-only; git fetch; git log')" "" +assert_eq "stash list/show — чтение" "$(t 'git stash list; git stash show')" "" +assert_eq "stash — мутация" "$(t 'git stash')" "git /r stash" +assert_eq "checkout -b — смена ветки" "$(t 'git checkout -b f')" "switch /r f" +assert_eq "checkout -- пути" "$(t 'git checkout -- a.txt')" "git /r checkout --" +assert_eq "checkout ветки — не мутация" "$(t 'git checkout main')" "" +assert_eq "switch -c && commit" "$(t 'git switch -q -c feat/x && git commit -m y')" "switch /r feat/x${NL}git /r commit" +assert_eq "apply --check — чтение" "$(t 'git apply --check p.diff')" "" +assert_eq "apply — мутация" "$(t 'git apply p.diff')" "git /r apply" +assert_eq "commit с heredoc в подстановке" "$(t "git commit -m \"\$(cat <<'EOF'${NL}fix: it's (done)${NL}EOF${NL})\"")" "git /r commit" +# push +assert_eq "push без refspec" "$(t 'git push')" "git /r push" +assert_eq "push origin HEAD" "$(t 'git push -u origin HEAD')" "git /r push" +assert_eq "push ветки фичи" "$(t 'git push -u origin feat/x')" "" +assert_eq "push --delete ветки фичи" "$(t 'git push origin --delete feat/x')" "" +assert_eq "push HEAD:main" "$(t 'git push origin HEAD:main')" "git /r push>main" +assert_eq "push --delete main" "$(t 'git push origin --delete main')" "git /r push>main" +assert_eq "push --all" "$(t 'git push --all')" "git /r push --all" +assert_eq "push в переменную" "$(t 'git push origin "$BR"')" "git /r push?" +assert_eq "push feat/\$ID — префикс фичи" "$(t 'git push origin feat/$ID-x')" "" +# граничные случаи (Review Focus) +assert_eq "путь с пробелом в кавычках" "$(t 'touch "a b.md"')" "file /r/a b.md touch" +assert_eq "арифметика \$((…)) не ломает разбор" "$(t 'echo $((1+2)) > f')" "file /r/f >" +assert_eq "cd ~/x" "$(t 'cd ~/x && touch f')" "file $HOME/x/f touch" +assert_eq "git -C относительный после cd" "$(t 'cd /w && git -C sub commit -m x')" "git /w/sub commit" +assert_eq "перенос строки обратным слешем" "$(t "git add a \\${NL} b && git commit -m x")" "git /r add${NL}git /r commit" +# сбой разбора +assert_grep "незакрытая кавычка — error" '^error' <(t 'echo "x') +``` + +- [ ] **Step 2: Убедиться, что тест падает** + +Run: `bash tests/run.sh hook-parse` +Expected: `FAIL harness_target.py существует` и остальные FAIL (файла нет). + +- [ ] **Step 3: Создать ядро** + +Создать `hooks/harness_target.py`: + +```python +#!/usr/bin/env python3 +"""claude-harness: общее ядро хуков guard-main и require-plan — решение по цели записи, а не по cwd. + +Обёртки hooks/guard-main.sh и hooks/require-plan.sh передают сюда JSON вызова из stdin: + python3 harness_target.py guard-main|require-plan < hook.json → exit 0 (пропустить) | 2 (блок, причина в stderr) +Отладка разбора без файловой системы: + 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. +""" +import glob +import json +import os +import re +import subprocess +import sys +from collections import namedtuple + +# kind: file | git | switch; path: абсолютный путь или None (не разрешился); op: что делает команда. +# Для switch path — каталог репозитория, op — новая ветка (None — не определена). +Target = namedtuple("Target", "kind path op") + +PROTECTED = ("main", "master", "") # "" — detached HEAD +SEPS = (";", "&&", "||", "|", "|&", "&", ";;", "\n", "(", ")") +OPS = sorted(["&&", "||", ";;", ";", "|&", "|", "&>>", "&>", "&", "(", ")", ">>", ">|", ">&", ">", + "<<<", "<<-", "<<", "<&", "<>", "<"], key=len, reverse=True) +OP_CHARS = set(";&|()<>") +WRITE_REDIRS = (">", ">>", ">|", "&>", "&>>") +KEYWORDS = ("if", "then", "else", "elif", "fi", "do", "done", "while", "until", "!", "{", "}") +SKIP_HEADS = ("for", "case", "select", "function") +PREFIXES = ("sudo", "env", "nohup", "command", "exec", "nice", "time", "builtin", "xargs") +ALL_PATHS = ("tee", "touch", "mkdir", "rm", "rmdir", "truncate", "unlink", "mv", "shred") +DEST = ("cp", "install", "ln", "rsync") +GIT_MUT = ("commit", "merge", "rebase", "cherry-pick", "revert", "reset", "am", "add", "rm", "mv", "restore") +ASSIGN = re.compile(r"[A-Za-z_][A-Za-z0-9_]*(\[[^]]*\])?\+?=") +FD = re.compile(r"\d+(?=[<>])") +VAR = re.compile(r"[A-Za-z_][A-Za-z0-9_]*|[0-9?$!#*@-]") + + +def _home(): + return os.environ.get("HOME", "") + + +# ---------- лексер ---------- + +class Lexer: + """Токены команды: ("w", текст, динамическое?) и ("op", оператор). Тело heredoc пропускается. + + Динамическое слово содержит $VAR, $(…) или `…` — в тексте остаётся сырой вид подстановки. + ~, $HOME и ${HOME} раскрываются по переменной окружения HOME. + """ + + def __init__(self, s, i=0): + self.s, self.i, self.heredocs = s, i, [] + + def tokens(self, until_paren=False): + s, out, depth = self.s, [], 0 + while self.i < len(s): + c = s[self.i] + if c in " \t": + self.i += 1 + elif s.startswith("\\\n", self.i): + self.i += 2 + elif c == "\n": + self.i += 1 + out.append(("op", "\n")) + self._skip_heredocs() + elif c == "#": + j = s.find("\n", self.i) + self.i = len(s) if j < 0 else j + elif FD.match(s, self.i): + m = FD.match(s, self.i) + self.i = m.end() + op = self._op() + out.append(("op", m.group() + op)) + elif c in OP_CHARS: + op = self._op() + if until_paren and op == "(": + depth += 1 + elif until_paren and op == ")": + if depth == 0: + return out + depth -= 1 + out.append(("op", op)) + if op in ("<<", "<<-"): + w = self._word() + if w is not None: + self.heredocs.append((w[0], op == "<<-")) + out.append(("w",) + w) + else: + out.append(("w",) + self._word()) + if until_paren: + raise ValueError("unclosed $(") + return out + + def _op(self): + op = next(o for o in OPS if self.s.startswith(o, self.i)) + self.i += len(op) + return op + + def _skip_heredocs(self): + s = self.s + for delim, strip_tabs in self.heredocs: + while self.i < len(s): + j = s.find("\n", self.i) + line = s[self.i:len(s) if j < 0 else j] + self.i = len(s) if j < 0 else j + 1 + if (line.lstrip("\t") if strip_tabs else line) == delim: + break + self.heredocs = [] + + def _word(self): + s, n = self.s, len(self.s) + while self.i < n and s[self.i] in " \t": + self.i += 1 + start, buf, dyn = self.i, [], False + while self.i < n: + c = s[self.i] + if c in " \t\n" or c in OP_CHARS: + break + if c == "\\": + if self.i + 1 < n and s[self.i + 1] != "\n": + buf.append(s[self.i + 1]) + self.i += 2 + elif c == "'": + j = s.find("'", self.i + 1) + if j < 0: + raise ValueError("unclosed '") + buf.append(s[self.i + 1:j]) + self.i = j + 1 + elif c == '"': + self.i += 1 + while True: + if self.i >= n: + raise ValueError('unclosed "') + c = s[self.i] + if c == '"': + self.i += 1 + break + if c == "\\" and self.i + 1 < n and s[self.i + 1] in '"\\$`\n': + if s[self.i + 1] != "\n": + buf.append(s[self.i + 1]) + self.i += 2 + elif c in "$`": + t, d = self._dollar() + buf.append(t) + dyn = dyn or d + else: + buf.append(c) + self.i += 1 + elif c in "$`": + t, d = self._dollar() + buf.append(t) + dyn = dyn or d + elif c == "~" and self.i == start and (self.i + 1 >= n or s[self.i + 1] in "/ \t\n;&|()<>"): + buf.append(_home()) + self.i += 1 + else: + buf.append(c) + self.i += 1 + if self.i == start: + return None + return "".join(buf), dyn + + def _dollar(self): + """$…/`…` с позиции i → (текст, динамическое?).""" + s, i = self.s, self.i + if s[i] == "`": + j = i + 1 + while j < len(s) and s[j] != "`": + j += 2 if s[j] == "\\" else 1 + if j >= len(s): + raise ValueError("unclosed `") + self.i = j + 1 + return s[i:j + 1], True + if s.startswith("$(", i): # подстановка и арифметика $((…)): пропуск до парной скобки + sub = Lexer(s, i + 2) + sub.tokens(until_paren=True) + self.i = sub.i + return s[i:self.i], True + if s.startswith("${", i): + j = s.find("}", i) + if j < 0: + raise ValueError("unclosed ${") + self.i = j + 1 + return (_home(), False) if s[i + 2:j] == "HOME" else (s[i:j + 1], True) + m = VAR.match(s, i + 1) + if not m: + self.i = i + 1 + return "$", False + self.i = m.end() + return (_home(), False) if m.group() == "HOME" else (s[i:m.end()], True) + + +# ---------- разбор ---------- + +def _join(cur, p): + """Абсолютный нормализованный путь; у glob — каталог до первого метасимвола; None, если не определить.""" + if p is None: + return None + m = re.search(r"[*?\[]", p) + if m: + p = os.path.dirname(p[:m.start()]) or "." + if os.path.isabs(p): + return os.path.normpath(p) + if not cur: + return None + return os.path.normpath(os.path.join(cur, p)) + + +def _path(w, cur): + text, dyn = w + return None if dyn else _join(cur, text) + + +def _operands(args, valued=()): + """Аргументы-операнды без опций; опции из valued забирают следующее слово как значение.""" + out, k, opts_done = [], 0, False + while k < len(args): + t = args[k][0] + if not opts_done and t == "--": + opts_done = True + elif not opts_done and t.startswith("-") and t != "-": + if t in valued: + k += 1 + else: + out.append(args[k]) + k += 1 + return out + + +def _branch(w): + """Имя ветки из слова. Для динамического — '<статический префикс>*', если префикс не может стать + защищённой веткой, иначе None (не определена).""" + text, dyn = w + if not dyn: + return text + cut = min([i for i in (text.find("$"), text.find("`")) if i >= 0] or [len(text)]) + prefix = text[:cut] + if prefix and not any(p.startswith(prefix) for p in ("main", "master", "HEAD", "refs/heads/main", + "refs/heads/master")): + return prefix + "*" + return None + + +class _State: + def __init__(self, cwd, out, depth): + self.cur, self.out, self.depth, self.dirs = cwd, out, depth, [] + + +def parse(cmd, cwd, _out=None, _depth=0): + """Цели записи команды Bash в порядке выполнения. Чистая функция: файловую систему не читает. + cwd — каталог, от которого считаются относительные пути (None — неизвестен).""" + out = [] if _out is None else _out + if _depth > 5: + return out + st = _State(cwd or None, out, _depth) + toks = Lexer(cmd).tokens() + stack, words, redirs, prev = [], [], [], None + i = 0 + while i <= len(toks): + t = toks[i] if i < len(toks) else ("op", "\n") + if t[0] == "w": + words.append((t[1], t[2])) + i += 1 + continue + op = t[1] + if op in SEPS: + if words or redirs: + new = _simple(words, redirs, st) + if new is not _SAME and op not in ("|", "|&") and prev not in ("|", "|&"): + st.cur = new + words, redirs = [], [] + if op == "(": + stack.append(st.cur) + elif op == ")" and stack: + st.cur = stack.pop() + prev = op + i += 1 + continue + nxt = toks[i + 1] if i + 1 < len(toks) and toks[i + 1][0] == "w" else None + redirs.append((op, (nxt[1], nxt[2]) if nxt else None)) + i += 2 if nxt else 1 + return out + + +_SAME = object() + + +def _simple(words, redirs, st): + """Одна простая команда: цели в st.out; возвращает новый каталог для cd/pushd/popd или _SAME.""" + cur, out = st.cur, st.out + for op, w in redirs: + o = op.lstrip("0123456789") + if w is None: + continue + if o in WRITE_REDIRS or (o == ">&" and not re.fullmatch(r"\d+-?|-", w[0])): + if not w[1] and w[0].startswith("/dev/"): + continue + out.append(Target("file", _path(w, cur), op)) + ws = list(words) + while ws and ws[0][0] in KEYWORDS and not ws[0][1]: + ws.pop(0) + if not ws or ws[0][0] in SKIP_HEADS: + return _SAME + xargs = False + while ws: + h = ws[0][0] + if ASSIGN.match(h): + ws.pop(0) + elif h in PREFIXES and not ws[0][1]: + xargs = xargs or h == "xargs" + ws.pop(0) + while ws and (ws[0][0].startswith("-") or (h == "env" and ASSIGN.match(ws[0][0]))): + ws.pop(0) + else: + break + if not ws or ws[0][1]: + return _SAME # пусто или голова — переменная: неизвестная команда + head, args = os.path.basename(ws[0][0]), ws[1:] + + def file_(w, op): + out.append(Target("file", None if xargs else _path(w, cur), op)) + + if head in ("cd", "pushd"): + a = _operands(args) + if not a: + new = _home() or None + else: + new = None if a[0][1] or a[0][0] == "-" else _join(cur, a[0][0]) + if head == "pushd": + st.dirs.append(cur) + return new + if head == "popd": + return st.dirs.pop() if st.dirs else None + if head in ("sh", "bash", "zsh", "dash"): + for k, (t, d) in enumerate(args): + if t.startswith("-") and not t.startswith("--") and "c" in t[1:]: + if k + 1 < len(args) and not args[k + 1][1]: + parse(args[k + 1][0], cur, out, st.depth + 1) + break + return _SAME + if head == "eval": + parse(" ".join(t for t, d in args), cur, out, st.depth + 1) + return _SAME + if xargs and head in ALL_PATHS + DEST + ("chmod", "chown", "chgrp", "sed", "perl"): + out.append(Target("file", None, head)) # пути придут из stdin + return _SAME + if head in ALL_PATHS: + for w in _operands(args, ("-s", "--size") if head == "truncate" else ()): + file_(w, head) + elif head in ("chmod", "chown", "chgrp"): + for w in _operands(args)[1:]: + file_(w, head) + elif head in DEST: + tdir = next((args[k + 1] for k, (t, d) in enumerate(args[:-1]) if t == "-t"), None) + tdir = tdir or next(((t.split("=", 1)[1], d) for t, d in args if t.startswith("--target-directory=")), None) + ops = _operands(args, ("-t", "-S", "--suffix")) + if tdir: + dst = tdir + elif len(ops) >= 2: + dst = ops[-1] + elif head == "ln" and ops: + dst = (".", False) # ln -s цель — ссылка в текущем каталоге + else: + dst = None + if dst is not None and not (head == "rsync" and re.match(r"[^/]*:", dst[0])): + file_(dst, head) + elif head == "sed": + inplace = any(t == "-i" or t.startswith("-i") or t.startswith("--in-place") + or re.fullmatch(r"-[A-Za-z]*i[A-Za-z]*", t) for t, d in args) + if inplace: + rest = [w for k, w in enumerate(args) + if not (w[0] == "" and k > 0 and args[k - 1][0] == "-i")] # BSD: sed -i '' … + has_script = any(t in ("-e", "-f", "--expression", "--file") or t.startswith("--expression=") + for t, d in rest) + ops = _operands(rest, ("-e", "-f", "-l", "--expression", "--file")) + for w in (ops if has_script else ops[1:]): + file_(w, "sed -i") + elif head == "perl": + if any(re.fullmatch(r"-[A-Za-z]*i\S*", t) for t, d in args): + has_script = any(re.fullmatch(r"-[A-Za-z]*[eE]", t) for t, d in args) + ops = _operands(args, ("-e", "-E", "-pe", "-ne", "-pie", "-npe")) + for w in (ops if has_script else ops[1:]): + file_(w, "perl -i") + elif head == "dd": + for t, d in args: + if t.startswith("of="): + file_((t[3:], d), "dd") + elif head == "tar": + first = args[0][0] if args else "" + if any(t in ("--extract", "--get") or (re.fullmatch(r"-[A-Za-z]+", t) and "x" in t) for t, d in args) \ + or (re.fullmatch(r"[A-Za-z]+", first) and "x" in first): + dst = next((args[k + 1] for k, (t, d) in enumerate(args[:-1]) if t == "-C"), None) + dst = dst or next(((t.split("=", 1)[1], d) for t, d in args if t.startswith("--directory=")), None) + file_(dst or (".", False), "tar -x") + elif head == "unzip": + if not any(t in ("-l", "-t", "-v", "-Z", "-p") for t, d in args): + dst = next((args[k + 1] for k, (t, d) in enumerate(args[:-1]) if t == "-d"), None) + file_(dst or (".", False), "unzip") + elif head == "patch": + if "--dry-run" not in [t for t, d in args]: + dst = next((args[k + 1] for k, (t, d) in enumerate(args[:-1]) if t == "-d"), None) + dst = dst or next(((t.split("=", 1)[1], d) for t, d in args if t.startswith("--directory=")), None) + file_(dst or (".", False), "patch") + elif head in ("npm", "yarn", "pnpm"): + if not any(t in ("-g", "--global") for t, d in args): + ops = _operands(args) + sub = ops[0][0] if ops else ("install" if head == "yarn" else "") + if sub in ("install", "i", "ci", "add", "init"): + out.append(Target("file", cur, head + " " + sub)) + elif head == "openspec": + ops = _operands(args) + if ops and ops[0][0] in ("archive", "new", "init", "update"): + out.append(Target("file", cur, "openspec " + ops[0][0])) + elif head == "git": + _git(args, st) + return _SAME + + +def _git(args, st): + repo, k, unknown = st.cur, 0, False + while k < len(args): + t, d = args[k] + if t == "-C" and k + 1 < len(args): + v = args[k + 1] + if v[1]: + unknown = True + else: + repo = _join(repo, v[0]) + k += 2 + elif t in ("-c", "--namespace", "--exec-path", "--config-env") and k + 1 < len(args): + k += 2 + elif t.startswith("--git-dir") or t.startswith("--work-tree"): + unknown = True + k += 1 if "=" in t else 2 + elif t.startswith("-"): + k += 1 + else: + break + if k >= len(args): + return + sub, rest = args[k][0], args[k + 1:] + path = None if unknown or repo is None else repo + words = [t for t, d in rest] + add = st.out.append + if sub in GIT_MUT: + add(Target("git", path, sub)) + elif sub == "apply": + if not any(w in ("--check", "--stat", "--numstat", "--summary") for w in words): + add(Target("git", path, "apply")) + elif sub == "stash": + s2 = next((w for w in words if not w.startswith("-")), "push") + if s2 not in ("list", "show"): + add(Target("git", path, "stash")) + elif sub == "checkout": + flag = next((j for j, w in enumerate(words) if w in ("-b", "-B", "--orphan")), None) + if flag is not None and flag + 1 < len(rest): + add(Target("switch", path, _branch(rest[flag + 1]))) + elif "--" in words and words.index("--") < len(words) - 1: + add(Target("git", path, "checkout --")) + elif sub == "switch": + name, found = None, False + for j, w in enumerate(words): + if w in ("-c", "-C", "--create", "--force-create", "--orphan") and j + 1 < len(rest): + name, found = _branch(rest[j + 1]), True + break + if w in ("-d", "--detach"): + name, found = "", True + break + if not found: + ops = _operands(rest) + if ops: + name, found = _branch(ops[0]), True + if found: + add(Target("switch", path, name)) + elif sub == "push": + _push(rest, path, add) + + +def _push(rest, path, add): + delete, everything, pos, k = False, False, [], 0 + while k < len(rest): + t = rest[k][0] + if t in ("--delete", "-d"): + delete = True + elif t in ("--all", "--mirror", "--branches"): + everything = True + elif t in ("--repo", "-o", "--push-option", "--receive-pack", "--exec"): + k += 1 + elif not t.startswith("-"): + pos.append(rest[k]) + k += 1 + if everything: + add(Target("git", path, "push --all")) + return + refs = pos[1:] + if not refs: + if not delete: + add(Target("git", path, "push")) + return + for text, dyn in refs: + src, colon, dst = text.lstrip("+").partition(":") + if delete or not colon: + dst = src + if dst.startswith("refs/heads/"): + dst = dst[len("refs/heads/"):] + if dst == "HEAD" and not delete and not colon: + add(Target("git", path, "push")) + continue + name = _branch((dst, dyn)) + if name is None: + add(Target("git", path, "push?")) + elif name in ("main", "master"): + add(Target("git", path, "push>" + name)) + + +# ---------- файловая система и вердикт ---------- + +_cache = {} + + +def _git_out(d, *args): + env = {k: v for k, v in os.environ.items() if k not in ("GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE")} + r = subprocess.run(["git", "-C", d] + list(args), stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, + universal_newlines=True, timeout=5, env=env) + return r.stdout.strip() if r.returncode == 0 else None + + +def resolve(path): + """(корень, ветка, реальный путь) git-репозитория, в котором лежит path, или None — вне репозитория. + Ветка '' — detached HEAD. Симлинки раскрываются до поиска (в том числе висячие: запись создаст файл у цели + ссылки), затем берётся ближайший существующий предок.""" + real = os.path.realpath(path) + p = real + while not os.path.exists(p): + parent = os.path.dirname(p) + if parent == p: + return None + p = parent + d = p if os.path.isdir(p) else os.path.dirname(p) + if d not in _cache: + root = _git_out(d, "rev-parse", "--show-toplevel") + branch = _git_out(root, "branch", "--show-current") if root else None + _cache[d] = (root, branch or "") if root else None + hit = _cache[d] + if not hit: + return None + return hit[0], hit[1], real + + +def _label(branch): + return "detached HEAD" if branch == "" else branch + + +WT_HINT = "Работай в worktree на ветке (superpowers:using-git-worktrees)." + + +def decide_guard(tool, ti, cwd): + """Сообщение блокировки guard-main или None.""" + cwd = cwd or None + if tool == "Bash": + targets = parse(ti.get("command") or "", cwd) + elif tool in ("Edit", "Write", "MultiEdit"): + fp = ti.get("file_path") or "" + targets = [Target("file", _join(cwd, fp), tool)] if fp else [] + else: + return None + over = {} # корень → ветка после git switch/checkout -b в этой же команде (None — не определена) + for t in targets: + if t.kind == "switch": + r = resolve(t.path) if t.path else None + if r: + over[r[0]] = t.op + continue + if t.path is None: + if t.kind == "git": + if t.op == "push?": + return "BLOCKED [guard-main]: git push — целевая ветка не определена. Укажи ветку явно." + return ("BLOCKED [guard-main]: git %s — репозиторий не определён (переменная в -C, --git-dir или " + "--work-tree). Укажи git -C <абсолютный путь>." % t.op) + continue + r = resolve(t.path) + if not r: + continue + root, branch, real = r + if root in over: + branch = over[root] + if t.kind == "git": + if t.op.startswith("push>") or t.op == "push --all": + return "BLOCKED [guard-main]: git %s — репозиторий %s. В main только через PR." % ( + t.op.replace(">", " в "), root) + if t.op == "push?": + return "BLOCKED [guard-main]: git push — целевая ветка не определена. Укажи ветку явно." + if branch is None or branch in PROTECTED: + return "BLOCKED [guard-main]: git %s — репозиторий %s на %s. В main только через PR. %s" % ( + t.op, root, "неизвестной ветке" if branch is None else _label(branch), WT_HINT) + elif branch in PROTECTED: + return "BLOCKED [guard-main]: запись в %s — репозиторий %s на %s. %s" % ( + os.path.relpath(real, root), root, _label(branch), WT_HINT) + return None + + +def decide_plan(fp, cwd): + """Сообщение блокировки require-plan или None.""" + path = _join(cwd or None, fp) if fp else None + r = resolve(path) if path else None + if not r: + return None + root, branch, real = r + rel = os.path.relpath(real, root) + cp = os.path.join(root, ".claude", "code-paths") + if os.path.isfile(cp): + prefixes = [l.split("#", 1)[0].strip().rstrip("/") for l in open(cp, encoding="utf-8", errors="replace")] + protected = any(p and rel.startswith(p + "/") for p in prefixes) + else: + protected = "/src/" in "/" + rel + if not protected or os.path.exists(os.path.join(root, ".bounded")): + return None + plans = sorted(glob.glob(os.path.join(root, "docs", "superpowers", "plans", "*.md"))) + if plans: + if branch == "" or len(plans) == 1: + return None + for p in plans: + if branch in open(p, encoding="utf-8", errors="replace").read(): + return None + return ("BLOCKED [require-plan]: правка %s без плана для ветки %s. Запусти superpowers:brainstorming; " + "для bounded-задачи создай %s/.bounded." % (rel, _label(branch), root)) + + +def main(argv): + mode = argv[1] if len(argv) > 1 else "" + if mode == "targets": + try: + ts = parse(argv[2] if len(argv) > 2 else "", argv[3] if len(argv) > 3 else os.getcwd()) + except Exception as e: + print("error %s" % e) + return 1 + for t in ts: + print(t.kind, "-" if t.path is None else t.path, "-" if t.op is None else t.op) + return 0 + try: + d = json.loads(sys.stdin.read()) + ti = d.get("tool_input") or {} + if mode == "guard-main": + msg = decide_guard(d.get("tool_name") or "", ti, d.get("cwd") or "") + elif mode == "require-plan": + msg = decide_plan(ti.get("file_path") or ti.get("path") or "", d.get("cwd") or "") + else: + msg = None + except Exception: + return 0 + if msg: + sys.stderr.write(msg + "\n") + return 2 + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) +``` + +- [ ] **Step 4: Прогнать тест** + +Run: `bash tests/run.sh hook-parse` +Expected: `passed 80, failed 0`. + +- [ ] **Step 5: Коммит** + +```bash +git add hooks/harness_target.py tests/test-hook-parse.sh +git commit -m "feat(hooks): harness_target.py — разбор команды на цели записи (#6)" -m "Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 2: guard-main на ядре + +**Files:** +- Modify: `hooks/guard-main.sh` (заменить целиком) +- Test: `tests/test-hook-guard-main.sh` + +**Interfaces:** +- Consumes: `python3 hooks/harness_target.py guard-main` (задача 1). +- Produces: `hooks/guard-main.sh` — PreToolUse-хук, stdin JSON → exit 0/2. + +- [ ] **Step 1: Написать тест сценариев спеки** + +Создать `tests/test-hook-guard-main.sh`: + +```bash +# guard-main.sh: решение по цели записи (спека hook-guards) в песочнице из настоящих git-репозиториев. +G="$ROOT/hooks/guard-main.sh" +S="$TMP/sb"; mkdir -p "$S/ws" "$HOME/.claude/projects/p/memory" +mk(){ git init -q "$1" && git -C "$1" symbolic-ref HEAD "refs/heads/${2:-main}" && git -C "$1" commit -q --allow-empty -m init; } +mk "$S/m" # репозиторий на main +mk "$S/f"; git -C "$S/f" worktree add -q "$S/wt" -b feat/6-x # основной checkout на main + worktree на ветке +mk "$S/ms" master # на master +mk "$S/d"; git -C "$S/d" checkout -q --detach # detached HEAD +j(){ python3 -c 'import json,sys; k="command" if sys.argv[2]=="Bash" else "file_path"; print(json.dumps({"cwd":sys.argv[1],"tool_name":sys.argv[2],"tool_input":{k:sys.argv[3]}}))' "$@"; } +rc(){ j "$@" | bash "$G" 2>/dev/null; echo $?; } +err(){ j "$@" | bash "$G" 2>&1 >/dev/null; } +# решение по цели, а не по cwd +assert_eq "чтение с 2>/dev/null на main" "$(rc "$S/m" Bash 'grep -rn "npm i\|npm install" . 2>/dev/null | head')" 0 +assert_eq "fetch 2>&1 | tail на main" "$(rc "$S/m" Bash 'git fetch origin --prune 2>&1 | tail -3')" 0 +assert_eq "Write вне репозитория при cwd на main" "$(rc "$S/m" Write "$HOME/.claude/projects/p/memory/x.md")" 0 +assert_eq "Edit в worktree при cwd на main" "$(rc "$S/f" Edit "$S/wt/a.md")" 0 +assert_eq "Edit в репозитории на main при cwd в worktree" "$(rc "$S/wt" Edit "$S/m/a.md")" 2 +assert_eq "Write на master" "$(rc "$S/ws" Write "$S/ms/a.md")" 2 +assert_eq "Write при detached HEAD" "$(rc "$S/ws" Write "$S/d/a.md")" 2 +# файловые цели Bash +assert_eq "echo > в main" "$(rc "$S/m" Bash 'echo x > notes.md')" 2 +assert_eq "make 2>build.log в main" "$(rc "$S/m" Bash 'make 2>build.log')" 2 +assert_eq "sed -i по файлу main" "$(rc "$S/ws" Bash "sed -i 's/a/b/' $S/m/docs/HARNESS.md")" 2 +assert_eq "sed -i '' по файлу памяти" "$(rc "$S/m" Bash "sed -i '' 's/a/b/' $HOME/.claude/projects/p/memory/x.md")" 0 +assert_eq "docker … sh -c > /tmp" "$(rc "$S/m" Bash "docker compose exec -T db sh -c 'psql -c \"select 1\" > /tmp/x'")" 0 +assert_eq "pip install на main" "$(rc "$S/m" Bash 'pip install requests')" 0 +assert_eq "npm install на main" "$(rc "$S/m" Bash 'npm install')" 2 +assert_eq "цель в переменной" "$(rc "$S/m" Bash 'echo x > "$OUT"')" 0 +assert_eq "цель через \$HOME в main" "$(HOME="$S" rc "$S/ws" Bash 'echo x > $HOME/m/f')" 2 +# каталог внутри команды +assert_eq "cd в worktree и запись" "$(rc "$S/f" Bash "cd $S/wt && echo x > f")" 0 +assert_eq "cd в main и touch" "$(rc "$S/wt" Bash "cd $S/m && touch f")" 2 +assert_eq "mkdir -p .wt/ в не-git workspace" "$(rc "$S/ws" Bash 'mkdir -p .wt/6-x')" 0 +assert_eq "bash -c с записью в main" "$(rc "$S/m" Bash "bash -c 'echo x > f'")" 2 +# git +assert_eq "add && commit в main" "$(rc "$S/m" Bash 'git add -A && git commit -m x')" 2 +assert_eq "commit -C worktree при cwd на main" "$(rc "$S/f" Bash "git -C $S/wt commit -m x")" 0 +assert_eq "worktree add -C из workspace" "$(rc "$S/ws" Bash "git -C $S/m worktree add ../.wt/x -b feat/x")" 0 +assert_eq "pull --ff-only на main" "$(rc "$S/m" Bash 'git pull --ff-only')" 0 +assert_eq "stash list на main" "$(rc "$S/m" Bash 'git stash list')" 0 +assert_eq "-C в переменной — блок" "$(rc "$S/ws" Bash 'git -C "$REPO" commit -m x')" 2 +assert_grep "-C в переменной — подсказка git -C" 'git -C <абсолютный путь>' <(err "$S/ws" Bash 'git -C "$REPO" commit -m x') +assert_eq "switch -c && commit на main" "$(rc "$S/m" Bash 'git switch -q -c chore/archive-x && git add openspec && git commit -m a')" 0 +assert_eq "switch -c && touch на main" "$(rc "$S/m" Bash 'git switch -q -c feat/16 && touch .bounded')" 0 +assert_eq "switch в переменную && commit — блок" "$(rc "$S/m" Bash 'git switch "$B" && git commit -m x')" 2 +assert_eq "switch в переменную && touch — пропуск" "$(rc "$S/m" Bash 'git switch "$B" && touch f')" 0 +assert_eq "commit вне репозитория" "$(rc "$S/ws" Bash 'git commit -m x')" 0 +# push +assert_eq "push с main" "$(rc "$S/m" Bash 'git push')" 2 +assert_eq "push --delete фичи с main" "$(rc "$S/m" Bash 'git push origin --delete feat/6-x')" 0 +assert_eq "push фичи с main" "$(rc "$S/m" Bash 'git push origin feat/6-x')" 0 +assert_eq "push HEAD:main из worktree" "$(rc "$S/wt" Bash 'git push origin HEAD:main')" 2 +assert_eq "push из worktree" "$(rc "$S/wt" Bash 'git push -u origin HEAD')" 0 +# без исключения для openspec (решение владельца 1) +mkdir -p "$S/m/openspec"; echo a > "$S/m/openspec/x.md"; git -C "$S/m" add openspec +assert_eq "commit только openspec/ в main" "$(rc "$S/m" Bash 'git commit -m archive')" 2 +git -C "$S/m" reset -q +assert_eq "openspec archive в main" "$(rc "$S/m" Bash 'openspec archive x')" 2 +# граничные случаи (Review Focus) +ln -s "$S/wt/CLAUDE.md" "$S/ws/CLAUDE.md"; ln -s "$S/m/CLAUDE.md" "$S/ws/MAIN.md" +assert_eq "запись через симлинк в worktree" "$(rc "$S/ws" Write "$S/ws/CLAUDE.md")" 0 +assert_eq "запись через симлинк в main" "$(rc "$S/ws" Write "$S/ws/MAIN.md")" 2 +assert_eq "пустой cwd и относительный путь — пропуск" "$(rc "" Write "a.md")" 0 +assert_eq "запись внутри .git/ — не рабочее дерево" "$(rc "$S/m" Bash 'echo x >> .git/info/exclude')" 0 +assert_eq "путь с пробелом в main" "$(rc "$S/m" Bash 'touch "a b.md"')" 2 +# сбой разбора и сообщение +assert_eq "не JSON — пропуск" "$(printf 'not json' | bash "$G" 2>/dev/null; echo $?)" 0 +assert_eq "незакрытая кавычка — пропуск" "$(rc "$S/m" Bash 'echo "x > f')" 0 +mkdir -p "$TMP/nopy" +assert_eq "нет python3 — пропуск" "$(env PATH="$TMP/nopy" /bin/bash "$G" <<< "$(j "$S/m" Write "$S/m/a.md")" 2>/dev/null; echo $?)" 0 +m=$(err "$S/ws" Write "$S/m/docs/HARNESS.md") +assert_grep "сообщение: префикс" '^BLOCKED \[guard-main\]:' <(printf '%s' "$m") +assert_grep "сообщение: путь отн. корня" 'docs/HARNESS.md' <(printf '%s' "$m") +assert_grep "сообщение: корень и worktree" "$(basename "$S")/m на main.*worktree" <(printf '%s' "$m") +assert_grep "settings.hooks.json регистрирует guard-main" 'guard-main\.sh' "$ROOT/hooks/settings.hooks.json" +``` + +- [ ] **Step 2: Убедиться, что тест падает на старом хуке** + +Run: `bash tests/run.sh hook-guard` +Expected: FAIL на «чтение с 2>/dev/null на main», «Write вне репозитория при cwd на main», «switch -c && commit на main», «сообщение: префикс» и других. + +- [ ] **Step 3: Заменить `hooks/guard-main.sh` обёрткой** + +```bash +#!/usr/bin/env bash +# PreToolUse hook (Edit/Write/MultiEdit/Bash): запрет записи в репозиторий на main/master/detached HEAD. +# Решение — по цели записи, а не по cwd; логика в harness_target.py рядом. Любой сбой — пропуск (exit 0). +py="${0%/*}/harness_target.py" +command -v python3 >/dev/null 2>&1 && [ -f "$py" ] || exit 0 +python3 "$py" guard-main +[ $? -eq 2 ] && exit 2 +exit 0 +``` + +- [ ] **Step 4: Прогнать тесты** + +Run: `bash tests/run.sh hook-` +Expected: `passed 131, failed 0` (parse 80 + guard-main 51). + +- [ ] **Step 5: Коммит** + +```bash +git add hooks/guard-main.sh tests/test-hook-guard-main.sh +git commit -m "feat(hooks): guard-main решает по цели записи, без исключения openspec-only (#6)" -m "B1; RT-1, RT-2, RT-7; решение владельца 1." -m "Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 3: require-plan на ядре + +**Files:** +- Modify: `hooks/require-plan.sh` (заменить целиком) +- Test: `tests/test-hook-require-plan.sh` + +**Interfaces:** +- Consumes: `python3 hooks/harness_target.py require-plan` (задача 1). +- Produces: `hooks/require-plan.sh` — PreToolUse-хук, stdin JSON → exit 0/2. + +- [ ] **Step 1: Написать тест** + +Создать `tests/test-hook-require-plan.sh`: + +```bash +# require-plan.sh: корень, ветка, code-paths, .bounded и планы — из репозитория правимого файла (спека hook-guards). +H="$ROOT/hooks/require-plan.sh" +S="$TMP/rp"; mkdir -p "$S/out" +mk(){ git init -q "$1" && git -C "$1" symbolic-ref HEAD "refs/heads/${2:-main}" && git -C "$1" commit -q --allow-empty -m init; } +mk "$S/a"; touch "$S/a/.bounded" # A: есть .bounded +mk "$S/b" # B: ни плана, ни .bounded +mk "$S/f"; git -C "$S/f" worktree add -q "$S/wt" -b feat/6-x; touch "$S/wt/.bounded" +mk "$S/c"; mkdir -p "$S/c/.claude"; printf 'hooks/ # код\n\nskills/\n' > "$S/c/.claude/code-paths" +mk "$S/p2" feat/6-x; mkdir -p "$S/p2/docs/superpowers/plans" +echo "Ветка: feat/6-x" > "$S/p2/docs/superpowers/plans/2026-10-16-a.md"; echo "другое" > "$S/p2/docs/superpowers/plans/2026-10-11-b.md" +mk "$S/p0" feat/7-y; mkdir -p "$S/p0/docs/superpowers/plans" +echo "Ветка: feat/6-x" > "$S/p0/docs/superpowers/plans/a.md"; echo "другое" > "$S/p0/docs/superpowers/plans/b.md" +mk "$S/p1" feat/7-y; mkdir -p "$S/p1/docs/superpowers/plans"; echo "x" > "$S/p1/docs/superpowers/plans/a.md" +mk "$S/pd"; mkdir -p "$S/pd/docs/superpowers/plans"; echo a > "$S/pd/docs/superpowers/plans/a.md"; echo b > "$S/pd/docs/superpowers/plans/b.md" +git -C "$S/pd" checkout -q --detach +j(){ python3 -c 'import json,sys; print(json.dumps({"cwd":sys.argv[1],"tool_name":"Edit","tool_input":{"file_path":sys.argv[2]}}))' "$@"; } +rc(){ j "$@" | bash "$H" 2>/dev/null; echo $?; } +assert_eq "файл в B без плана при cwd в A с .bounded" "$(rc "$S/a" "$S/b/src/a.py")" 2 +assert_eq "файл в worktree с .bounded при cwd на main" "$(rc "$S/f" "$S/wt/src/a.py")" 0 +assert_eq "src на глубине без плана" "$(rc "$S/b" "$S/b/api/src/a.py")" 2 +assert_eq "не код — пропуск" "$(rc "$S/b" "$S/b/docs/a.md")" 0 +assert_eq "вне репозитория — пропуск" "$(rc "$S/b" "$S/out/src/a.py")" 0 +assert_eq "code-paths: hooks/ без плана" "$(rc "$S/a" "$S/c/hooks/x.sh")" 2 +assert_eq "code-paths: src/ не защищён" "$(rc "$S/c" "$S/c/src/a.py")" 0 +assert_eq "два плана, один с веткой" "$(rc "$S/p2" "$S/p2/src/a.py")" 0 +assert_eq "два плана без ветки" "$(rc "$S/p0" "$S/p0/src/a.py")" 2 +assert_eq "один план — пропуск" "$(rc "$S/p1" "$S/p1/src/a.py")" 0 +assert_eq "detached HEAD при планах — пропуск" "$(rc "$S/pd" "$S/pd/src/a.py")" 0 +assert_eq "не JSON — пропуск" "$(printf 'not json' | bash "$H" 2>/dev/null; echo $?)" 0 +assert_eq "без file_path — пропуск" "$(printf '{"cwd":"%s","tool_input":{}}' "$S/b" | bash "$H" 2>/dev/null; echo $?)" 0 +m=$(j "$S/a" "$S/b/src/a.py" | bash "$H" 2>&1 >/dev/null) +assert_grep "сообщение: префикс и путь" '^BLOCKED \[require-plan\]: правка src/a.py' <(printf '%s' "$m") +assert_grep "сообщение: .bounded репозитория файла" "$(basename "$S")/b/\.bounded" <(printf '%s' "$m") +assert_grep "settings.hooks.json регистрирует require-plan" 'require-plan\.sh' "$ROOT/hooks/settings.hooks.json" +``` + +- [ ] **Step 2: Убедиться, что тест падает на старом хуке** + +Run: `bash tests/run.sh hook-require` +Expected: FAIL на «файл в B без плана при cwd в A с .bounded» (старый хук видит `.bounded` в A) и «сообщение: префикс и путь». + +- [ ] **Step 3: Заменить `hooks/require-plan.sh` обёрткой** + +```bash +#!/usr/bin/env bash +# PreToolUse hook (Edit/Write/MultiEdit): правки в каталогах кода (любой src/ или .claude/code-paths) — только при +# плане для ветки или файле .bounded. Репозиторий — тот, где лежит файл; логика в harness_target.py рядом. +py="${0%/*}/harness_target.py" +command -v python3 >/dev/null 2>&1 && [ -f "$py" ] || exit 0 +python3 "$py" require-plan +[ $? -eq 2 ] && exit 2 +exit 0 +``` + +- [ ] **Step 4: Прогнать тесты** + +Run: `bash tests/run.sh hook-` +Expected: `passed 147, failed 0` (+ require-plan 16). + +- [ ] **Step 5: Коммит** + +```bash +git add hooks/require-plan.sh tests/test-hook-require-plan.sh +git commit -m "feat(hooks): require-plan по репозиторию правимого файла (#6)" -m "Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 4: Регрессионный корпус и экстрактор (B2) + +**Files:** +- Create: `docs/retro/scripts/hook_cases_build.py`, `skills/harness-doctor/scripts/hook_corpus.py` +- Generate: `skills/harness-doctor/fixtures/hook-cases.jsonl` +- Modify: `docs/retro/scripts/hook_blocks_export.py`, `docs/retro/scripts/retro_blocks.py` +- Test: `tests/test-hook-corpus.sh`, `tests/test-hook-export.sh` + +**Interfaces:** +- Consumes: `hooks/guard-main.sh`, `hooks/require-plan.sh` (задачи 2–3); сырьё `docs/retro/fixtures/hook-blocks.jsonl` (не меняется). +- Produces: + - `hook-cases.jsonl`: строка 1 — `{"home": "/Users/andrey.epifancev", "layout": [{"path", "branch"?, "worktree_of"?}]}`; далее записи выгрузки + `id` (`c001`…`c126`, `l001`…`l003`), `expected` (`allow|block`), `why`. + - `python3 skills/harness-doctor/scripts/hook_corpus.py <каталог хуков> [--summary] [--fixtures F]`: без `--summary` — строка `id\twhy\texpected\tgot\tcmd` на запись и `total N/M`; с `--summary` — `MISMATCH …` на расхождение и `корпус хуков: N/M`; exit 0, если всё совпало. + +- [ ] **Step 1: Написать тесты корпуса и экстрактора** + +Создать `tests/test-hook-corpus.sh`: + +```bash +# hook_corpus.py: регрессионный корпус хуков (B2) через источники hooks/ — assert на запись. +R="$ROOT/skills/harness-doctor/scripts/hook_corpus.py" +F="$ROOT/skills/harness-doctor/fixtures/hook-cases.jsonl" +assert_file "hook_corpus.py существует" "$R" +assert_file "корпус hook-cases.jsonl" "$F" +assert_eq "корпус совпадает со сборкой из выгрузки" "$(python3 "$ROOT/docs/retro/scripts/hook_cases_build.py" | cmp -s - "$F" && echo same)" "same" +while IFS=$'\t' read -r id why exp got cmd; do + case "$id" in total*) continue ;; esac + assert_eq "корпус $id ($why): $cmd" "$got" "$exp" +done < <(python3 "$R" "$ROOT/hooks") +``` + +Создать `tests/test-hook-export.sh`: + +```bash +# hook_blocks_export.py: выгружаются только ошибки хуков, а не вывод команд со словом BLOCKED. +X="$ROOT/docs/retro/scripts/hook_blocks_export.py" +T="$TMP/transcripts"; mkdir -p "$T" +cat > "$T/s1.jsonl" <<'EOF' +{"type":"assistant","cwd":"/r","gitBranch":"main","timestamp":"t1","message":{"content":[{"type":"tool_use","id":"u1","name":"Bash","input":{"command":"echo x > f"}}]}} +{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"u1","is_error":true,"content":"PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED [guard-main]: запись в f — репозиторий /r на main."}]}} +{"type":"assistant","cwd":"/r","gitBranch":"main","timestamp":"t2","message":{"content":[{"type":"tool_use","id":"u2","name":"Bash","input":{"command":"cat hooks/require-plan.sh"}}]}} +{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"u2","content":"echo \"BLOCKED: editing $fpath without a plan. Run superpowers:brainstorming\" >&2"}]}} +{"type":"assistant","cwd":"/r","gitBranch":"main","timestamp":"t3","message":{"content":[{"type":"tool_use","id":"u3","name":"Edit","input":{"file_path":"/r/a.md"}}]}} +{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"u3","is_error":true,"content":[{"type":"text","text":"PreToolUse:Edit hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: you are on main. Create an isolated worktree first"}]}]}} +EOF +out=$(python3 "$X" "$T") +assert_eq "две записи: вывод cat с BLOCKED не выгружен" "$(printf '%s\n' "$out" | grep -c .)" "2" +assert_eq "новый префикс → guard-main(Bash)" "$(printf '%s\n' "$out" | sed -n 1p | python3 -c 'import json,sys; r=json.load(sys.stdin); print(r["hook"], r["command"])')" "guard-main(Bash) echo x > f" +assert_eq "старая фраза → guard-main(Edit/Write)" "$(printf '%s\n' "$out" | sed -n 2p | python3 -c 'import json,sys; r=json.load(sys.stdin); print(r["hook"], r["file_path"])')" "guard-main(Edit/Write) /r/a.md" +``` + +- [ ] **Step 2: Убедиться, что тесты падают** + +Run: `bash tests/run.sh hook-corpus; bash tests/run.sh hook-export` +Expected: FAIL «hook_corpus.py существует», «корпус hook-cases.jsonl»; в экстракторе — «две записи: вывод cat с BLOCKED не выгружен» (получено 3) и «новый префикс → guard-main(Bash)» (получено `other`). + +- [ ] **Step 3: Создать сборщик корпуса** + +Создать `docs/retro/scripts/hook_cases_build.py`: + +```python +#!/usr/bin/env python3 +"""Сборка регрессионного корпуса хуков из сырой выгрузки блокировок (B2, #6). + +Запуск из корня репозитория: + python3 docs/retro/scripts/hook_cases_build.py > skills/harness-doctor/fixtures/hook-cases.jsonl +Вход: 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, а не прогоном хука. +""" +import json +import os +import re +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +RAW = os.path.join(HERE, "..", "fixtures", "hook-blocks.jsonl") +HOME = "/Users/andrey.epifancev" +AW, CH = "projects/awp-ca", "projects/cc-skills/claude-harness" + +LAYOUT = [{"path": AW}, {"path": AW + "/reference-old-arm"}] +LAYOUT += [{"path": "%s/%s" % (AW, r), "branch": "main"} + for r in ("arm-core", "docs", "tabular-parser", "scan-gateway", "arm-web", "fixtures")] +LAYOUT += [{"path": "%s/.wt/%s/%s" % (AW, n, r), "worktree_of": "%s/%s" % (AW, r), "branch": "feat/" + n} + for n, r in (("web-proto", "arm-core"), ("web-proto", "arm-web"), ("ocr-q1", "fixtures"), + ("ocr-q1", "scan-gateway"), ("stmt-template", "arm-web"), ("qa-guide-web", "docs"), + ("slice-0", "scan-gateway"), ("slice-3b", "arm-core"))] +LAYOUT += [{"path": CH, "branch": "main"}] +LAYOUT += [{"path": "%s/.wt/%s" % (CH, n), "worktree_of": CH, "branch": b} + for n, b in (("retro", "retro/2026-10-11"), ("roles", "feat/2-roles-and-memory"), + ("guard", "feat/6-guard-main-target"))] +LAYOUT += [{"path": ".claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory"}, + {"path": ".claude/projects/-Users-andrey-epifancev-projects-cc-skills-claude-harness/memory"}, + {"path": ".claude/hooks"}, {"path": ".claude/jobs"}] + +# Блок по спеке: архивный коммит openspec прямо в main (решение владельца 1 — архив отдельным PR). +BLOCK = {"c031", "c069", "c070", "c103"} +# Класс для записей, где он не выводится общим правилом ниже. +WHY = { + "c002": "unparseable", "c008": "var-target", "c023": "outside-repo", "c026": "outside-repo", + "c029": "worktree", "c030": "outside-repo", "c040": "worktree-setup", "c044": "worktree", "c045": "worktree", + "c055": "outside-repo", "c064": "worktree-setup", "c072": "switch-then-commit", "c073": "worktree-setup", + "c078": "outside-repo", "c080": "heredoc", "c081": "heredoc", "c083": "outside-repo", "c091": "heredoc", + "c099": "worktree-setup", "c100": "worktree-setup", "c101": "worktree", "c102": "worktree", + "c104": "worktree-setup", "c106": "worktree-setup", "c119": "heredoc", +} +# Ложные блоки этой сессии (15.10.2026), до перехода в worktree: cwd — основной checkout на main. +LIVE_RE = ('git show origin/feat/6-guard-main-target:docs/retro/fixtures/hook-blocks.jsonl | python3 -c \'\n' + 'import sys,json,re,collections\n' + 'rs=[json.loads(l) for l in sys.stdin if l.strip()]\n' + 'c=collections.Counter()\n' + 'for r in rs:\n' + ' cmd=r.get("command") or ""\n' + ' if re.search(r"\\d>{1,2}\\s*/dev/null|\\d>&\\d",cmd): c["bash: N>/dev/null или N>&M"]+=1\n' + ' if re.search(r"mkdir\\s+-p\\s+\\S*\\.wt/",cmd): c["bash: mkdir -p .wt/"]+=1\n' + ' if re.search(r"\\bsed\\s+-i",cmd): c["bash: sed -i"]+=1\n' + 'for k,v in c.most_common(): print(f"{v:4} {k}")\'') +LIVE = [ + ("l001", "grep-pattern", 'grep -rn "npm i\\|npm install\\|brew install\\|plugin install\\|go install" ' + '--include="*.md" --include="*.sh" . | grep -v "^./docs/retro/2026" | head -20'), + ("l002", "devnull", 'grep -rn "plugin add\\|marketplace\\|caveman@\\|superpowers@" --include="*.md" ' + '--include="*.sh" README.md docs global install.sh 2>/dev/null | head -20'), + ("l003", "regex-text", LIVE_RE), +] +DEVNULL = re.compile(r"\d>{1,2}\s*/dev/null|\d>&\d") + + +def why_of(cid, r): + if cid in BLOCK: + return "main-git" + if cid in WHY: + return WHY[cid] + if "hook error" not in (r.get("message") or ""): + return "not-a-block" # вывод успешной команды, где встретилось слово BLOCKED + if r["tool"] != "Bash": + return "outside-repo" + return "devnull" if DEVNULL.search(r.get("command") or "") else "read-only" + + +def main(): + print(json.dumps({"home": HOME, "layout": LAYOUT}, ensure_ascii=False)) + for n, line in enumerate(l for l in open(RAW, encoding="utf-8") if l.strip()): + r = json.loads(line) + cid = "c%03d" % (n + 1) + r.update({"id": cid, "expected": "block" if cid in BLOCK else "allow", "why": why_of(cid, r)}) + print(json.dumps(r, ensure_ascii=False)) + for cid, why, cmd in LIVE: + print(json.dumps({"id": cid, "session": "live-1015", "cwd": HOME + "/" + CH, "branch": "main", + "tool": "Bash", "command": cmd, "file_path": None, "hook": "guard-main(Bash)", + "message": "", "expected": "allow", "why": why}, ensure_ascii=False)) + + +if __name__ == "__main__": + main() +``` + +- [ ] **Step 4: Создать раннер корпуса** + +Создать `skills/harness-doctor/scripts/hook_corpus.py`: + +```python +#!/usr/bin/env python3 +"""Регрессионный корпус хуков (B2): прогон fixtures/hook-cases.jsonl через guard-main и require-plan в песочнице. + +Запуск: python3 hook_corpus.py <каталог хуков> [--summary] [--fixtures ] +Песочница строится во временном каталоге по раскладке из первой строки корпуса; префикс домашнего каталога записей +заменяется на её home, HOME хуков подменяется туда же. Bash идёт через guard-main, Edit/Write — через guard-main и +require-plan (блок, если блокирует любой). +Вывод: строка на запись «idwhyожидалосьполученокоманда» и итог «total N/M»; +с --summary — только расхождения и «корпус хуков: N/M». Код выхода 0 — всё совпало, 1 — нет. +""" +import json +import os +import shutil +import subprocess +import sys +import tempfile + +HERE = os.path.dirname(os.path.abspath(__file__)) + + +def _git(*args): + subprocess.run(["git", "-c", "user.name=corpus", "-c", "user.email=corpus@example.invalid"] + list(args), + check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + +def build(home, layout): + for e in layout: + p = os.path.join(home, e["path"]) + if "worktree_of" in e: + _git("-C", os.path.join(home, e["worktree_of"]), "worktree", "add", "-q", p, "-b", e["branch"]) + elif "branch" in e: + os.makedirs(p, exist_ok=True) + _git("init", "-q", p) + _git("-C", p, "symbolic-ref", "HEAD", "refs/heads/" + e["branch"]) + _git("-C", p, "commit", "-q", "--allow-empty", "-m", "init") + else: + os.makedirs(p, exist_ok=True) + + +def verdict(hooks, case, prefix, home, env): + swap = lambda s: s.replace(prefix, home) if isinstance(s, str) else s + tool = case["tool"] + ti = {"command": swap(case.get("command"))} if tool == "Bash" else {"file_path": swap(case.get("file_path"))} + payload = json.dumps({"cwd": swap(case.get("cwd")), "tool_name": tool, "tool_input": ti}) + for name in ["guard-main"] + ([] if tool == "Bash" else ["require-plan"]): + r = subprocess.run(["bash", os.path.join(hooks, name + ".sh")], input=payload, universal_newlines=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, timeout=30) + if r.returncode == 2: + return "block" + if r.returncode != 0: + return "error" + return "allow" + + +def main(argv): + args = argv[1:] + summary = "--summary" in args + fx = os.path.join(HERE, "..", "fixtures", "hook-cases.jsonl") + if "--fixtures" in args: + fx = args[args.index("--fixtures") + 1] + pos = [a for k, a in enumerate(args) if not a.startswith("--") and (k == 0 or args[k - 1] != "--fixtures")] + if not pos: + print(__doc__.strip().splitlines()[2]) + return 1 + hooks = os.path.abspath(pos[0]) + lines = [json.loads(l) for l in open(fx, encoding="utf-8") if l.strip()] + head, cases = lines[0], lines[1:] + tmp = tempfile.mkdtemp(prefix="hook-corpus-") + try: + home = os.path.join(tmp, "home") + try: + build(home, head["layout"]) + except Exception as e: + print("корпус хуков: песочница не собрана (%s)" % e) + return 1 + env = {k: v for k, v in os.environ.items() if k not in ("GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE")} + env["HOME"] = home + ok = 0 + for c in cases: + got = verdict(hooks, c, head["home"], home, env) + ok += got == c["expected"] + body = (c.get("command") or c.get("file_path") or "").replace(head["home"], "~") + row = "\t".join([c["id"], c["why"], c["expected"], got, " ".join(body.split())[:70]]) + if not summary: + print(row) + elif got != c["expected"]: + print("MISMATCH " + row) + print(("корпус хуков: %d/%d" if summary else "total %d/%d") % (ok, len(cases))) + return 0 if ok == len(cases) else 1 + finally: + shutil.rmtree(tmp, ignore_errors=True) + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) +``` + +- [ ] **Step 5: Сгенерировать корпус** + +Run: `mkdir -p skills/harness-doctor/fixtures && python3 docs/retro/scripts/hook_cases_build.py > skills/harness-doctor/fixtures/hook-cases.jsonl && wc -l skills/harness-doctor/fixtures/hook-cases.jsonl` +Expected: `130` (шапка + 126 + 3). + +- [ ] **Step 6: Экстрактор — только ошибки хука** + +В `docs/retro/scripts/hook_blocks_export.py`: + +Докстринг — после строки `первая строка ответа хука. Поле expected …` вставить: + +``` +Выгружаются только ошибки хуков (результат начинается с `PreToolUse:<инструмент> hook error`); вывод команд, +в котором встретилось слово BLOCKED (файлы памяти, исходники хуков), не попадает. +``` + +После `import json, sys, glob, os, re` добавить: + +```python +HOOK_ERROR = re.compile(r"\s*PreToolUse:\S+ hook error") +``` + +Заменить начало `hook_of`: + +```python +def hook_of(txt, tool): + if "[guard-main]" in txt: + return "guard-main(Bash)" if tool == "Bash" else "guard-main(Edit/Write)" + if "[require-plan]" in txt: + return "require-plan" + if "shell write" in txt: # формулировки до 0.4.0 +``` + +Заменить фильтр: + +```python + if not HOOK_ERROR.match(txt): + continue # обычный вывод команды, даже если в нём есть слово BLOCKED +``` + +(было `if not ("BLOCKED" in txt and ("hook" in txt.lower() or "BLOCKED:" in txt[:400])):`), и вызов — `"hook": hook_of(txt, name),`. + +- [ ] **Step 7: Тот же фильтр в `retro_blocks.py`** + +В `docs/retro/scripts/retro_blocks.py` после `RE_HEREDOC_PY = …` добавить: + +```python +RE_HOOK_ERROR = re.compile(r"\s*PreToolUse:\S+ hook error") # только ошибка хука, не вывод команды со словом BLOCKED +``` + +Заменить: + +```python + if "BLOCKED" in txt and ("hook" in txt.lower() or "BLOCKED:" in txt[:400]): + if name not in ("Bash", "Edit", "Write", "MultiEdit"): + continue + if "shell write" in txt: +``` + +на: + +```python + if RE_HOOK_ERROR.match(txt) and "BLOCKED" in txt: + if name not in ("Bash", "Edit", "Write", "MultiEdit"): + continue + if "[guard-main]" in txt: + hook = "guard-main(Bash)" if name == "Bash" else "guard-main(Edit/Write)" + elif "[require-plan]" in txt: + hook = "require-plan" + elif "shell write" in txt: +``` + +Run: `python3 -m py_compile docs/retro/scripts/retro_blocks.py docs/retro/scripts/hook_blocks_export.py` +Expected: без вывода. + +- [ ] **Step 8: Прогнать тесты** + +Run: `bash tests/run.sh hook-` +Expected: `passed 282, failed 0` (+ корпус 132, экстрактор 3); в выводе `ok корпус c031 (main-git): …`, `ok корпус c072 (switch-then-commit): …`. + +- [ ] **Step 9: Метрика для PR (не тест)** + +Старые хуки — из `main`, в отдельный каталог (каждая команда отдельно): + +Run: `mkdir -p "$TMPDIR/old-hooks"` +Run: `git show main:hooks/guard-main.sh > "$TMPDIR/old-hooks/guard-main.sh"` +Run: `git show main:hooks/require-plan.sh > "$TMPDIR/old-hooks/require-plan.sh"` +Run: `python3 skills/harness-doctor/scripts/hook_corpus.py "$TMPDIR/old-hooks" | awk -F'\t' 'NF>=4 {n[$4]++; if ($4=="block" && $3=="allow") fp++} END {for (k in n) print k, n[k]; print "ложных:", fp+0}'` +Expected: `allow 88`, `block 41`, `ложных: 37` — записать в описание PR рядом с итогом новых хуков (`block 4`, ложных 0). + +- [ ] **Step 10: Коммит** + +```bash +git add docs/retro/scripts/hook_cases_build.py docs/retro/scripts/hook_blocks_export.py docs/retro/scripts/retro_blocks.py skills/harness-doctor/scripts/hook_corpus.py skills/harness-doctor/fixtures/hook-cases.jsonl tests/test-hook-corpus.sh tests/test-hook-export.sh +git commit -m "test(hooks): регрессионный корпус хуков из транскриптов, экстрактор только по ошибкам хука (#6)" -m "B2, RT-6, O-1." -m "Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 5: Установка, doctor и документация + +**Files:** +- Modify: `install.sh`, `skills/harness-doctor/scripts/doctor.sh`, `skills/harness-doctor/SKILL.md`, `tests/test-install.sh`, `tests/test-doctor.sh`, `README.md`, `docs/HARNESS.md`, `CLAUDE.md`, `roles/dev.md`, `CHANGELOG.md` + +**Interfaces:** +- Consumes: `hooks/harness_target.py` (задача 1), `hook_corpus.py` и корпус (задача 4). +- Produces: строки отчёта `doctor.sh` — `ok hooks/harness_target.py (ядро guard-main и require-plan)`, `ok guard-main: git commit openspec-only на main → exit 2`, `ok корпус хуков: N/N`. + +- [ ] **Step 1: Тесты install и doctor** + +В `tests/test-install.sh` после строки `assert_eq "hooks/role-hint.sh исполняемый" …` добавить: + +```bash +assert_file "hooks/harness_target.py установлен" "$CC/hooks/harness_target.py" +assert_file "корпус хуков установлен" "$CC/skills/harness-doctor/fixtures/hook-cases.jsonl" +assert_file "раннер корпуса установлен" "$CC/skills/harness-doctor/scripts/hook_corpus.py" +``` + +и после `assert_grep "--check: agent briefing" …`: + +```bash +assert_grep "--check: harness_target.py" 'ok +hooks/harness_target\.py' <(printf '%s' "$out") +``` + +В `tests/test-doctor.sh` заменить строку `cp "$ROOT"/hooks/*.sh "$CC/hooks/"; chmod +x "$CC/hooks/"*.sh` на: + +```bash +cp "$ROOT"/hooks/*.sh "$ROOT/hooks/harness_target.py" "$CC/hooks/"; chmod +x "$CC/hooks/"*.sh +mkdir -p "$CC/skills"; cp -R "$ROOT/skills/harness-doctor" "$CC/skills/" +``` + +и после `assert_grep "самотест: role-hint тишина" …` добавить: + +```bash +assert_grep "машина: harness_target.py" 'ok +hooks/harness_target\.py' <(printf '%s' "$out") +assert_grep "самотест: openspec-only коммит на main — блок" 'ok +guard-main: git commit openspec-only на main → exit 2' <(printf '%s' "$out") +assert_grep "самотест: Write на main" 'ok +guard-main: Write на main → exit 2' <(printf '%s' "$out") +assert_grep "самотест: Write на ветке" 'ok +guard-main: Write на ветке → exit 0' <(printf '%s' "$out") +assert_grep "корпус хуков сходится" 'ok +корпус хуков: [0-9]+/[0-9]+' <(printf '%s' "$out") +``` + +- [ ] **Step 2: Убедиться, что тесты падают** + +Run: `bash tests/run.sh install; bash tests/run.sh doctor` +Expected: FAIL «hooks/harness_target.py установлен», «--check: harness_target.py», «машина: harness_target.py», «самотест: openspec-only…», «корпус хуков сходится». + +- [ ] **Step 3: `install.sh`** + +Заменить строку копирования хуков: + +```bash +cp "$HERE/hooks/guard-main.sh" "$HERE/hooks/require-plan.sh" "$HERE/hooks/harness-check.sh" "$HERE/hooks/role-hint.sh" \ + "$HERE/hooks/harness_target.py" "$CC/hooks/" +``` + +В `check()` список файлов: после `hooks/require-plan.sh` вставить `hooks/harness_target.py`: + +```bash + for f in CLAUDE.md hooks/guard-main.sh hooks/require-plan.sh hooks/harness_target.py hooks/harness-check.sh hooks/role-hint.sh \ +``` + +В финальной строке `echo "done: …"` после `role-hint}.sh` дописать ` + harness_target.py`. + +- [ ] **Step 4: `doctor.sh`** + +После цикла `for h in guard-main require-plan harness-check role-hint; do … done` добавить: + +```bash +[ -f "$CC/hooks/harness_target.py" ] && ok "hooks/harness_target.py (ядро guard-main и require-plan)" || miss "hooks/harness_target.py (bash install.sh)" +``` + +В самотесте после случая `"git worktree на main"` и перед `git -C "$T" switch -q -c feat/1-x` вставить: + +```bash + mkdir -p "$T/openspec"; echo a > "$T/openspec/x.md"; git -C "$T" add openspec 2>/dev/null + hk guard-main "{\"cwd\":\"$T\",\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"git commit -m archive\"}}" 2 "git commit openspec-only на main" + git -C "$T" reset -q 2>/dev/null +``` + +После `rm -rf "$T"` (конец самотеста, перед `echo "## Проект: …"`) вставить: + +```bash +HC="$CC/skills/harness-doctor/scripts/hook_corpus.py" +if [ -f "$HC" ] && [ -f "$CC/hooks/harness_target.py" ]; then + out=$(python3 "$HC" "$CC/hooks" --summary 2>&1); rc=$? + printf '%s\n' "$out" | grep '^MISMATCH' | sed 's/^MISMATCH / /' + [ "$rc" = 0 ] && ok "$(printf '%s\n' "$out" | tail -1)" || warn "$(printf '%s\n' "$out" | tail -1) — расхождения выше" +else miss "корпус хуков (skills/harness-doctor/scripts/hook_corpus.py, hooks/harness_target.py)"; fi +``` + +- [ ] **Step 5: Прогнать тесты install и doctor** + +Run: `bash tests/run.sh install; bash tests/run.sh doctor` +Expected: оба `failed 0`. + +- [ ] **Step 6: `SKILL.md` doctor** + +В `skills/harness-doctor/SKILL.md` после абзаца **Самотест хуков** добавить абзац: + +```markdown +**Корпус хуков** — `ok корпус хуков: N/N`: установленные хуки дают на реальных командах пилота те же вердикты, что размечены. `WARN корпус хуков: N/M` и строки над ним — расхождения: `id`, класс, ожидалось, получено, команда. После `install.sh` расхождение — регрессия пакета; разобрать одну команду: `python3 ~/.claude/hooks/harness_target.py targets '<команда>' `. `MISS корпус хуков` — нет `harness_target.py` или раннера, чинится `install.sh`. +``` + +- [ ] **Step 7: Документация** + +`README.md`, блок «Файлы» — строки `hooks/guard-main.sh` и `hooks/require-plan.sh` заменить на: + +``` +hooks/guard-main.sh запрет изменений на main/master/detached HEAD (Edit/Write/Bash) по цели записи, без исключений +hooks/require-plan.sh правки в src/ (или .claude/code-paths) только при плане для ветки или файле .bounded +hooks/harness_target.py ядро обоих хуков: цели записи команды → репозиторий и ветка каждой цели +``` + +`docs/HARNESS.md`, раздел «Хуки» — строки таблицы guard-main и require-plan заменить на: + +```markdown +| Никаких изменений на main / master / detached HEAD | `guard-main.sh` → `harness_target.py`, PreToolUse на Edit/Write/MultiEdit/Bash | решает по цели записи, а не по cwd. Цели: файл Edit/Write; в Bash — редиректы (кроме `/dev/null` и `N>&M`), `tee`, `rm`, `mv`, `cp`, `sed -i`, `npm install`, `openspec archive`… с учётом `cd` и `git -C`; git-мутации (`commit`, `add`, `merge`, `reset`…; `push` — только если пушит в main; `switch -c` в той же команде меняет ветку). Цель вне репозитория или в worktree на ветке — пропуск; неразрешимый путь (`$VAR`) — пропуск; git-мутация с неопределённым репозиторием — блок. Исключений нет: архив OpenSpec — отдельным PR | +| Правки в коде только при плане или `.bounded` | `require-plan.sh` → `harness_target.py`, PreToolUse на Edit/Write/MultiEdit | по репозиторию правимого файла, а не по cwd: защищает любой `src/` на любой глубине (`api/src/`, `packages/*/src/`) или каталоги из `.claude/code-paths`; ищет план в `docs/superpowers/plans/` для ветки этого репозитория или файл `.bounded` в его корне | +``` + +и абзац после таблицы заменить на: + +```markdown +Exit 2 блокирует вызов и показывает агенту stderr, поэтому агент сам делает то, что просит сообщение. Сообщение начинается с `BLOCKED [guard-main]:` или `BLOCKED [require-plan]:` и называет цель, репозиторий и выход. Оба обхода, замеченные на пилоте (запись через `echo >` вместо Write; push через detached HEAD), закрыты. + +Регрессия хуков — корпус `skills/harness-doctor/fixtures/hook-cases.jsonl`: реальные команды из транскриптов пилота с ожидаемым вердиктом. По источникам — `bash tests/run.sh hook`, по установленным копиям — строка «корпус хуков» в `doctor.sh`. Корпус собирается из сырой выгрузки `docs/retro/scripts/hook_cases_build.py`; разметка — в этом скрипте. Разбор одной команды: `python3 ~/.claude/hooks/harness_target.py targets '<команда>' `. +``` + +`CLAUDE.md` репозитория: +- «Стек и ограничения», первый пункт — начало до «Внешние CLI» заменить на: «Bash (`set -euo pipefail`), markdown и `python3` stdlib (3.8+). Логика guard-main и require-plan — `hooks/harness_target.py`, их `.sh` — обёртки; в остальных хуках и скриптах JSON разбирается встроенным `python3`. Других зависимостей времени выполнения нет.» +- «Архитектура», пункт `hooks/` — описания guard-main и require-plan заменить на: «`guard-main.sh` (PreToolUse на Edit/Write/MultiEdit/Bash): запрет записи в репозиторий на main/master/detached HEAD по цели записи (файл, цели команды Bash, git-мутации), без исключений. `require-plan.sh` (PreToolUse на Edit/Write/MultiEdit): правки в каталогах кода репозитория файла только при плане для ветки или файле `.bounded`. Обе — обёртки над `harness_target.py`: `parse` (команда → цели, без ФС) → `resolve` (путь → корень и ветка) → `decide`; режим `targets` — отладка разбора.» +- пункт `skills/harness-doctor/` — дописать: «`fixtures/hook-cases.jsonl` + `scripts/hook_corpus.py` — регрессионный корпус хуков (реальные команды с ожидаемым вердиктом, прогон в песочнице); лежит в skill, чтобы `doctor.sh` гонял его по установленным копиям.» +- «Команды», пункт ретро-скриптов — дописать «; `hook_cases_build.py > skills/harness-doctor/fixtures/hook-cases.jsonl` — сборка размеченного корпуса из выгрузки.» и добавить пункт: «`python3 skills/harness-doctor/scripts/hook_corpus.py hooks [--summary]` — корпус хуков по источникам. `python3 hooks/harness_target.py targets '<команда>' [cwd]` — какие цели записи видит хук.» +- «Тесты», третий пункт (про «пока не сделаны») заменить двумя: «Хуки: `test-hook-parse` — разбор команды на цели; `test-hook-guard-main`, `test-hook-require-plan` — сценарии спеки `hook-guards` в песочнице из настоящих репозиториев; `test-hook-corpus` — корпус реальных команд (assert на запись, корпус должен совпадать со сборкой `hook_cases_build.py`); `test-hook-export` — экстрактор. Плюс `role-hint` и скрипты `install.sh`, `doctor.sh`, `link-roles.sh`.» и «Правишь разбор или вердикт — сначала случай в `test-hook-parse`/`test-hook-guard-main`; меняется ожидаемый вердикт реальной команды — разметка в `hook_cases_build.py` и пересборка корпуса.» + +`roles/dev.md`, раздел «Исполнение» — пункт про `cd` заменить на: + +```markdown +- Пока работает фоновый SDD-контроллер, в главной сессии не делать `cd` в основной checkout: cwd оболочки общий, и команды implementer-ов с относительными путями пойдут в main — guard-main их заблокирует, и они остановятся. Команды с абсолютными путями worktree и `git -C ` от cwd не зависят. +``` + +`CHANGELOG.md` — над `## 0.3.0` вставить: + +```markdown +## 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/`. +- **Несовместимо:** исключение для 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` показывает, какие цели видит хук. +- Сообщения блокировки: `BLOCKED [guard-main]:` / `BLOCKED [require-plan]:`, цель, корень репозитория, выход. +- Регрессионный корпус хуков (B2, RT-6): 126 записей из транскриптов пилота + 3 живых случая, размечены по спеке (`docs/retro/scripts/hook_cases_build.py`), прогон в песочнице — `tests/test-hook-corpus.sh` по источникам и строка «корпус хуков» в `doctor.sh` по установленным копиям. В песочнице старые хуки дают на корпусе 41 блок (37 ложных), новые — 4 (архивные коммиты в main). +- `hook_blocks_export.py` и `retro_blocks.py` считают только ошибки хуков: из 126 выгруженных записей 49 оказались выводом команд со словом BLOCKED (O-1), счёт RT-1 в ретро завышен. +- Тесты: `test-hook-parse`, `test-hook-guard-main`, `test-hook-require-plan`, `test-hook-corpus`, `test-hook-export`. +``` + +- [ ] **Step 8: Весь набор** + +Run: `bash tests/run.sh` +Expected: `passed 448, failed 0`. + +- [ ] **Step 9: Коммит** + +```bash +git add install.sh skills/harness-doctor/scripts/doctor.sh skills/harness-doctor/SKILL.md tests/test-install.sh tests/test-doctor.sh README.md docs/HARNESS.md CLAUDE.md roles/dev.md CHANGELOG.md +git commit -m "feat: install/doctor ставят и проверяют ядро и корпус хуков; документация 0.4.0 (#6)" -m "Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +## После задач + +- [ ] `superpowers:verification-before-completion`: `bash tests/run.sh` целиком, `OPENSPEC_TELEMETRY=0 openspec validate hooks-write-target --strict`, `git status --short` пуст. +- [ ] Ревью ветки целиком (opus), находки — `C-n`/`I-n`/`M-n` со статусами. +- [ ] `superpowers:finishing-a-development-branch`: push ветки, PR в Gitea (`tea pulls create --login home`) со ссылкой на #6, метрикой из задачи 4 (шаг 9), рулингами и отложенными находками. Merge — владелец. diff --git a/hooks/guard-main.sh b/hooks/guard-main.sh index d539549..9e3e53c 100755 --- a/hooks/guard-main.sh +++ b/hooks/guard-main.sh @@ -1,44 +1,8 @@ #!/usr/bin/env bash -# PreToolUse hook: block file changes on main/master and on detached HEAD. -# Covers Edit/Write/MultiEdit directly and Bash commands that look like writes. -# Single exception: commits/pushes to main that touch only openspec/ (archive step). -set -euo pipefail -input=$(cat) -eval "$(printf '%s' "$input" | python3 -c ' -import sys, json, shlex -d = json.load(sys.stdin); ti = d.get("tool_input", {}) or {} -print("cwd=" + shlex.quote(d.get("cwd", ""))) -print("tool=" + shlex.quote(d.get("tool_name", ""))) -print("cmd=" + shlex.quote(ti.get("command", "") or ""))' 2>/dev/null || echo 'cwd=; tool=; cmd=')" -[ -n "$cwd" ] || exit 0 -git -C "$cwd" rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0 -branch=$(git -C "$cwd" branch --show-current 2>/dev/null || true) -case "$branch" in main|master|"") ;; *) exit 0 ;; esac -label=${branch:-"detached HEAD"} - -only_openspec() { # $1 = git diff range/args; true if every changed path is under openspec/ - local files - files=$(git -C "$cwd" diff --name-only $1 2>/dev/null || true) - [ -n "$files" ] && ! printf '%s\n' "$files" | grep -vq '^openspec/' -} - -if [ "$tool" = "Bash" ]; then - case "$cmd" in - *"git worktree"*|*"git checkout -b"*|*"git switch -c"*|*"openspec archive"*|*"/.claude/harness/adopt-project.sh"*) exit 0 ;; - esac - # archive exception: commit of staged openspec-only changes, push of openspec-only commits - if printf '%s' "$cmd" | grep -Eq '\bgit\s+commit\b'; then - only_openspec "--cached" && exit 0 - fi - if printf '%s' "$cmd" | grep -Eq '\bgit\s+push\b'; then - only_openspec "origin/main...HEAD" && exit 0 - fi - if printf '%s' "$cmd" | grep -Eq '(^|[^<])>{1,2}[^&]|\btee\b|\bsed\s+-i|\bmv\b|\bcp\b|\brm\b|\btouch\b|\bmkdir\b|\bgit\s+(add|commit|push|apply|stash\s+pop)\b|\bnpm\s+(i|install|init)\b|\bpip\s+install\b'; then - echo "BLOCKED: shell write on $label ($(printf '%s' "$cmd" | head -c 80)). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly." >&2 - exit 2 - fi - exit 0 -fi - -echo "BLOCKED: you are on $label. Create an isolated worktree first (superpowers:using-git-worktrees), never edit main directly." >&2 -exit 2 +# PreToolUse hook (Edit/Write/MultiEdit/Bash): запрет записи в репозиторий на main/master/detached HEAD. +# Решение — по цели записи, а не по cwd; логика в harness_target.py рядом. Любой сбой — пропуск (exit 0). +py="${0%/*}/harness_target.py" +command -v python3 >/dev/null 2>&1 && [ -f "$py" ] || exit 0 +python3 "$py" guard-main +[ $? -eq 2 ] && exit 2 +exit 0 diff --git a/hooks/harness_target.py b/hooks/harness_target.py new file mode 100644 index 0000000..0c7d6c2 --- /dev/null +++ b/hooks/harness_target.py @@ -0,0 +1,716 @@ +#!/usr/bin/env python3 +"""claude-harness: общее ядро хуков guard-main и require-plan — решение по цели записи, а не по cwd. + +Обёртки hooks/guard-main.sh и hooks/require-plan.sh передают сюда JSON вызова из stdin: + python3 harness_target.py guard-main|require-plan < hook.json → exit 0 (пропустить) | 2 (блок, причина в stderr) +Отладка разбора без файловой системы: + 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. +""" +import glob +import json +import os +import re +import subprocess +import sys +from collections import namedtuple + +# kind: file | git | switch; path: абсолютный путь или None (не разрешился); op: что делает команда. +# Для switch path — каталог репозитория, op — новая ветка (None — не определена). +Target = namedtuple("Target", "kind path op") + +PROTECTED = ("main", "master", "") # "" — detached HEAD +SEPS = (";", "&&", "||", "|", "|&", "&", ";;", "\n", "(", ")") +OPS = sorted(["&&", "||", ";;", ";", "|&", "|", "&>>", "&>", "&", "(", ")", ">>", ">|", ">&", ">", + "<<<", "<<-", "<<", "<&", "<>", "<"], key=len, reverse=True) +OP_CHARS = set(";&|()<>") +WRITE_REDIRS = (">", ">>", ">|", "&>", "&>>") +KEYWORDS = ("if", "then", "else", "elif", "fi", "do", "done", "while", "until", "!", "{", "}") +SKIP_HEADS = ("for", "case", "select", "function") +PREFIXES = ("sudo", "env", "nohup", "command", "exec", "nice", "time", "builtin", "xargs", "timeout", "ionice", + "stdbuf") +# Опции префиксов, которые забирают следующее слово как значение (иначе значение примут за голову команды). +PREFIX_VALUED = { + "sudo": ("-u", "-g", "-C", "-D", "-h", "-p", "-r", "-t", "-U", "--user", "--group", "--chdir"), + "nice": ("-n", "--adjustment"), + "env": ("-u", "--unset", "-S", "--split-string"), + "timeout": ("-s", "--signal", "-k", "--kill-after"), + "ionice": ("-c", "--class", "-n", "--classdata", "-p", "--pid", "-P", "--pgid", "-u", "--uid"), + "stdbuf": ("-i", "--input", "-o", "--output", "-e", "--error"), + "xargs": ("-I", "-n", "-P", "-L", "-d", "-a", "-E", "-s", "--max-args", "--max-procs", "--max-lines", + "--delimiter", "--arg-file", "--replace"), +} +NOFOLLOW = ("rm", "rmdir", "unlink", "ln", "mv") # меняют саму ссылку, а не её цель +CUR_BRANCH = re.compile(r"(\$\(|`)\s*git\s+(branch\s+--show-current|rev-parse\s+--abbrev-ref\s+HEAD|" + r"symbolic-ref\s+(-q\s+)?--short\s+HEAD)\s*(\)|`)") +ALL_PATHS = ("tee", "touch", "mkdir", "rm", "rmdir", "truncate", "unlink", "mv", "shred") +DEST = ("cp", "install", "ln", "rsync") +GIT_MUT = ("commit", "merge", "rebase", "cherry-pick", "revert", "reset", "am", "add", "rm", "mv", "restore") +ASSIGN = re.compile(r"[A-Za-z_][A-Za-z0-9_]*(\[[^]]*\])?\+?=") +FD = re.compile(r"\d+(?=[<>])") +VAR = re.compile(r"[A-Za-z_][A-Za-z0-9_]*|[0-9?$!#*@-]") + + +def _home(): + return os.environ.get("HOME", "") + + +# ---------- лексер ---------- + +class Lexer: + """Токены команды: ("w", текст, динамическое?) и ("op", оператор). Тело heredoc пропускается. + + Динамическое слово содержит $VAR, $(…) или `…` — в тексте остаётся сырой вид подстановки. + ~, $HOME и ${HOME} раскрываются по переменной окружения HOME. + """ + + def __init__(self, s, i=0): + self.s, self.i, self.heredocs = s, i, [] + + def tokens(self, until_paren=False): + s, out, depth = self.s, [], 0 + while self.i < len(s): + c = s[self.i] + if c in " \t": + self.i += 1 + elif s.startswith("\\\n", self.i): + self.i += 2 + elif c == "\n": + self.i += 1 + out.append(("op", "\n")) + self._skip_heredocs() + elif c == "#": + j = s.find("\n", self.i) + self.i = len(s) if j < 0 else j + elif FD.match(s, self.i): + m = FD.match(s, self.i) + self.i = m.end() + op = self._op() + out.append(("op", m.group() + op)) + elif c in OP_CHARS: + op = self._op() + if until_paren and op == "(": + depth += 1 + elif until_paren and op == ")": + if depth == 0: + return out + depth -= 1 + out.append(("op", op)) + if op in ("<<", "<<-"): + w = self._word() + if w is not None: + self.heredocs.append((w[0], op == "<<-")) + out.append(("w",) + w) + else: + out.append(("w",) + self._word()) + if until_paren: + raise ValueError("unclosed $(") + return out + + def _op(self): + op = next(o for o in OPS if self.s.startswith(o, self.i)) + self.i += len(op) + return op + + def _skip_heredocs(self): + s = self.s + for delim, strip_tabs in self.heredocs: + while self.i < len(s): + j = s.find("\n", self.i) + line = s[self.i:len(s) if j < 0 else j] + self.i = len(s) if j < 0 else j + 1 + if (line.lstrip("\t") if strip_tabs else line) == delim: + break + self.heredocs = [] + + def _word(self): + s, n = self.s, len(self.s) + while self.i < n and s[self.i] in " \t": + self.i += 1 + start, buf, dyn = self.i, [], False + while self.i < n: + c = s[self.i] + if c in " \t\n" or c in OP_CHARS: + break + if c == "\\": + if self.i + 1 < n and s[self.i + 1] != "\n": + buf.append(s[self.i + 1]) + self.i += 2 + elif c == "'": + j = s.find("'", self.i + 1) + if j < 0: + raise ValueError("unclosed '") + buf.append(s[self.i + 1:j]) + self.i = j + 1 + elif c == '"': + self.i += 1 + while True: + if self.i >= n: + raise ValueError('unclosed "') + c = s[self.i] + if c == '"': + self.i += 1 + break + if c == "\\" and self.i + 1 < n and s[self.i + 1] in '"\\$`\n': + if s[self.i + 1] != "\n": + buf.append(s[self.i + 1]) + self.i += 2 + elif c in "$`": + t, d = self._dollar() + buf.append(t) + dyn = dyn or d + else: + buf.append(c) + self.i += 1 + elif c in "$`": + t, d = self._dollar() + buf.append(t) + dyn = dyn or d + elif c == "~" and self.i == start and (self.i + 1 >= n or s[self.i + 1] in "/ \t\n;&|()<>"): + buf.append(_home()) + self.i += 1 + else: + buf.append(c) + self.i += 1 + if self.i == start: + return None + return "".join(buf), dyn + + def _dollar(self): + """$…/`…` с позиции i → (текст, динамическое?).""" + s, i = self.s, self.i + if s[i] == "`": + j = i + 1 + while j < len(s) and s[j] != "`": + j += 2 if s[j] == "\\" else 1 + if j >= len(s): + raise ValueError("unclosed `") + self.i = j + 1 + return s[i:j + 1], True + if s.startswith("$(", i): # подстановка и арифметика $((…)): пропуск до парной скобки + sub = Lexer(s, i + 2) + sub.tokens(until_paren=True) + self.i = sub.i + return s[i:self.i], True + if s.startswith("${", i): + j = s.find("}", i) + if j < 0: + raise ValueError("unclosed ${") + self.i = j + 1 + return (_home(), False) if s[i + 2:j] == "HOME" else (s[i:j + 1], True) + m = VAR.match(s, i + 1) + if not m: + self.i = i + 1 + return "$", False + self.i = m.end() + return (_home(), False) if m.group() == "HOME" else (s[i:m.end()], True) + + +# ---------- разбор ---------- + +def _join(cur, p): + """Путь из команды Bash: у glob — каталог до первого метасимвола, дальше как _abs.""" + if p is None: + return None + m = re.search(r"[*?\[]", p) + if m: + p = os.path.dirname(p[:m.start()]) or "." + return _abs(cur, p) + + +def _abs(cur, p): + """Абсолютный нормализованный путь; None, если относительный и cwd неизвестен. Литерал, без glob — + для file_path инструментов (src/[slug].ts — имя файла, а не шаблон).""" + if p is None: + return None + if os.path.isabs(p): + return os.path.normpath(p) + if not cur: + return None + return os.path.normpath(os.path.join(cur, p)) + + +def _path(w, cur): + text, dyn = w + return None if dyn else _join(cur, text) + + +def _operands(args, valued=()): + """Аргументы-операнды без опций; опции из valued забирают следующее слово как значение.""" + out, k, opts_done = [], 0, False + while k < len(args): + t = args[k][0] + if not opts_done and t == "--": + opts_done = True + elif not opts_done and t.startswith("-") and t != "-": + if t in valued: + k += 1 + else: + out.append(args[k]) + k += 1 + return out + + +def _branch(w): + """Имя ветки из слова. Для динамического — '<статический префикс>*', если префикс не может стать + защищённой веткой, иначе None (не определена).""" + text, dyn = w + if not dyn: + return text + cut = min([i for i in (text.find("$"), text.find("`")) if i >= 0] or [len(text)]) + prefix = text[:cut] + if prefix and not any(p.startswith(prefix) for p in ("main", "master", "HEAD", "refs/heads/main", + "refs/heads/master")): + return prefix + "*" + return None + + +class _State: + def __init__(self, cwd, out, depth): + self.cur, self.out, self.depth, self.dirs = cwd, out, depth, [] + + +def parse(cmd, cwd, _out=None, _depth=0): + """Цели записи команды Bash в порядке выполнения. Чистая функция: файловую систему не читает. + cwd — каталог, от которого считаются относительные пути (None — неизвестен).""" + out = [] if _out is None else _out + if _depth > 5: + return out + st = _State(cwd or None, out, _depth) + toks = Lexer(cmd).tokens() + stack, words, redirs, prev = [], [], [], None + i = 0 + while i <= len(toks): + t = toks[i] if i < len(toks) else ("op", "\n") + if t[0] == "w": + words.append((t[1], t[2])) + i += 1 + continue + op = t[1] + if op in SEPS: + if words or redirs: + new = _simple(words, redirs, st) + if new is not _SAME and op not in ("|", "|&") and prev not in ("|", "|&"): + st.cur = new + words, redirs = [], [] + if op == "(": + stack.append(st.cur) + elif op == ")" and stack: + st.cur = stack.pop() + prev = op + i += 1 + continue + nxt = toks[i + 1] if i + 1 < len(toks) and toks[i + 1][0] == "w" else None + redirs.append((op, (nxt[1], nxt[2]) if nxt else None)) + i += 2 if nxt else 1 + return out + + +_SAME = object() + + +def _simple(words, redirs, st): + """Одна простая команда: цели в st.out; возвращает новый каталог для cd/pushd/popd или _SAME.""" + cur, out = st.cur, st.out + for op, w in redirs: + o = op.lstrip("0123456789") + if w is None: + continue + if o in WRITE_REDIRS or (o == ">&" and not re.fullmatch(r"\d+-?|-", w[0])): + if not w[1] and w[0].startswith("/dev/"): + continue + out.append(Target("file", _path(w, cur), op)) + ws = list(words) + while ws and ws[0][0] in KEYWORDS and not ws[0][1]: + ws.pop(0) + if not ws or ws[0][0] in SKIP_HEADS: + return _SAME + xargs = False + while ws: + h = ws[0][0] + if ASSIGN.match(h): + ws.pop(0) + elif h in PREFIXES and not ws[0][1]: + xargs = xargs or h == "xargs" + ws.pop(0) + valued = PREFIX_VALUED.get(h, ()) + while ws: + t = ws[0][0] + if h == "env" and ASSIGN.match(t): + ws.pop(0) + elif h == "env" and (t in ("-C", "--chdir") or t.startswith("--chdir=")): + ws.pop(0) + if "=" in t: + v = (t.split("=", 1)[1], False) + else: + v = ws.pop(0) if ws else ("", True) + cur = None if v[1] else _abs(cur, v[0]) # env -C DIR: каталог только этой команды + elif t.startswith("-") and t != "-": + ws.pop(0) + if t in valued and ws: + ws.pop(0) + else: + break + if h == "timeout" and ws: + ws.pop(0) # длительность + else: + break + if not ws or ws[0][1]: + return _SAME # пусто или голова — переменная: неизвестная команда + head, args = os.path.basename(ws[0][0]), ws[1:] + + def file_(w, op): + out.append(Target("file", None if xargs else _path(w, cur), op)) + + if head in ("cd", "pushd"): + a = _operands(args) + if not a: + new = _home() or None + else: + new = None if a[0][1] or a[0][0] == "-" else _join(cur, a[0][0]) + if head == "pushd": + st.dirs.append(cur) + return new + if head == "popd": + return st.dirs.pop() if st.dirs else None + if head in ("sh", "bash", "zsh", "dash"): + for k, (t, d) in enumerate(args): + if t.startswith("-") and not t.startswith("--") and "c" in t[1:]: + if k + 1 < len(args) and not args[k + 1][1]: + parse(args[k + 1][0], cur, out, st.depth + 1) + break + return _SAME + if head == "eval": + parse(" ".join(t for t, d in args), cur, out, st.depth + 1) + return _SAME + if xargs and head in ALL_PATHS + DEST + ("chmod", "chown", "chgrp", "sed", "perl"): + out.append(Target("file", None, head)) # пути придут из stdin + return _SAME + if head in ALL_PATHS: + for w in _operands(args, ("-s", "--size") if head == "truncate" else ()): + file_(w, head) + elif head in ("chmod", "chown", "chgrp"): + for w in _operands(args)[1:]: + file_(w, head) + elif head in DEST: + tdir = next((args[k + 1] for k, (t, d) in enumerate(args[:-1]) if t == "-t"), None) + tdir = tdir or next(((t.split("=", 1)[1], d) for t, d in args if t.startswith("--target-directory=")), None) + ops = _operands(args, ("-t", "-S", "--suffix")) + if tdir: + dst = tdir + elif len(ops) >= 2: + dst = ops[-1] + elif head == "ln" and ops: + dst = (".", False) # ln -s цель — ссылка в текущем каталоге + else: + dst = None + if dst is not None and not (head == "rsync" and re.match(r"[^/]*:", dst[0])): + file_(dst, head) + elif head == "sed": + inplace = any(t == "-i" or t.startswith("-i") or t.startswith("--in-place") + or re.fullmatch(r"-[A-Za-z]*i[A-Za-z]*", t) for t, d in args) + if inplace: + rest = [w for k, w in enumerate(args) + if not (w[0] == "" and k > 0 and args[k - 1][0] == "-i")] # BSD: sed -i '' … + has_script = any(t in ("-e", "-f", "--expression", "--file") or t.startswith("--expression=") + for t, d in rest) + ops = _operands(rest, ("-e", "-f", "-l", "--expression", "--file")) + for w in (ops if has_script else ops[1:]): + file_(w, "sed -i") + elif head == "perl": + if any(re.fullmatch(r"-[A-Za-z]*i\S*", t) for t, d in args): + has_script = any(re.fullmatch(r"-[A-Za-z]*[eE]", t) for t, d in args) + ops = _operands(args, ("-e", "-E", "-pe", "-ne", "-pie", "-npe")) + for w in (ops if has_script else ops[1:]): + file_(w, "perl -i") + elif head == "dd": + for t, d in args: + if t.startswith("of="): + file_((t[3:], d), "dd") + elif head == "tar": + first = args[0][0] if args else "" + if any(t in ("--extract", "--get") or (re.fullmatch(r"-[A-Za-z]+", t) and "x" in t) for t, d in args) \ + or (re.fullmatch(r"[A-Za-z]+", first) and "x" in first): + dst = next((args[k + 1] for k, (t, d) in enumerate(args[:-1]) if t == "-C"), None) + dst = dst or next(((t.split("=", 1)[1], d) for t, d in args if t.startswith("--directory=")), None) + file_(dst or (".", False), "tar -x") + elif head == "unzip": + if not any(t in ("-l", "-t", "-v", "-Z", "-p") for t, d in args): + dst = next((args[k + 1] for k, (t, d) in enumerate(args[:-1]) if t == "-d"), None) + file_(dst or (".", False), "unzip") + elif head == "patch": + if "--dry-run" not in [t for t, d in args]: + dst = next((args[k + 1] for k, (t, d) in enumerate(args[:-1]) if t == "-d"), None) + dst = dst or next(((t.split("=", 1)[1], d) for t, d in args if t.startswith("--directory=")), None) + file_(dst or (".", False), "patch") + elif head in ("npm", "yarn", "pnpm"): + if not any(t in ("-g", "--global") for t, d in args): + ops = _operands(args) + sub = ops[0][0] if ops else ("install" if head == "yarn" else "") + if sub in ("install", "i", "ci", "add", "init"): + out.append(Target("file", cur, head + " " + sub)) + elif head == "openspec": + ops = _operands(args) + if ops and ops[0][0] in ("archive", "new", "init", "update"): + out.append(Target("file", cur, "openspec " + ops[0][0])) + elif head == "git": + _git(args, st, cur) + return _SAME + + +def _git(args, st, cur): + repo, k, unknown = cur, 0, False + while k < len(args): + t, d = args[k] + if t == "-C" and k + 1 < len(args): + v = args[k + 1] + if v[1]: + unknown = True + else: + repo = _join(repo, v[0]) + k += 2 + elif t in ("-c", "--namespace", "--exec-path", "--config-env") and k + 1 < len(args): + k += 2 + elif t.startswith("--git-dir") or t.startswith("--work-tree"): + unknown = True + k += 1 if "=" in t else 2 + elif t.startswith("-"): + k += 1 + else: + break + if k >= len(args): + return + sub, rest = args[k][0], args[k + 1:] + path = None if unknown or repo is None else repo + words = [t for t, d in rest] + add = st.out.append + if sub in GIT_MUT: + add(Target("git", path, sub)) + elif sub == "apply": + if not any(w in ("--check", "--stat", "--numstat", "--summary") for w in words): + add(Target("git", path, "apply")) + elif sub == "stash": + s2 = next((w for w in words if not w.startswith("-")), "push") + if s2 not in ("list", "show"): + add(Target("git", path, "stash")) + elif sub == "checkout": + flag = next((j for j, w in enumerate(words) if w in ("-b", "-B", "--orphan")), None) + if flag is not None and flag + 1 < len(rest): + add(Target("switch", path, _branch(rest[flag + 1]))) + elif "--" in words and words.index("--") < len(words) - 1: + add(Target("git", path, "checkout --")) + else: + # checkout без «--»: ветка или путь — неизвестно; защищённые имена и «-» — точно смена ветки, + # переменная — ветка не определена (последующие git-мутации блокируются) + ops = _operands(rest) + if ops and (ops[0][1] or ops[0][0] in ("main", "master", "-")): + add(Target("switch", path, None if ops[0][1] or ops[0][0] == "-" else ops[0][0])) + elif sub == "switch": + name, found = None, False + for j, w in enumerate(words): + if w in ("-c", "-C", "--create", "--force-create", "--orphan") and j + 1 < len(rest): + name, found = _branch(rest[j + 1]), True + break + if w in ("-d", "--detach"): + name, found = "", True + break + if not found: + ops = _operands(rest) + if ops: + name, found = (None if ops[0] == ("-", False) else _branch(ops[0])), True # «-» — предыдущая + if found: + add(Target("switch", path, name)) + elif sub == "push": + _push(rest, path, add) + + +def _push(rest, path, add): + delete, everything, tags, pos, k = False, False, False, [], 0 + while k < len(rest): + t = rest[k][0] + if t in ("--delete", "-d"): + delete = True + elif t == "--tags": + tags = True + elif t in ("--all", "--mirror", "--branches"): + everything = True + elif t in ("--repo", "-o", "--push-option", "--receive-pack", "--exec"): + k += 1 + elif not t.startswith("-"): + pos.append(rest[k]) + k += 1 + if everything: + add(Target("git", path, "push --all")) + return + refs = pos[1:] + if not refs: + if not delete and not tags: # --tags без refspec пушит только теги + add(Target("git", path, "push")) + return + for text, dyn in refs: + src, colon, dst = text.lstrip("+").partition(":") + if delete or not colon: + dst = src + if dst.startswith("refs/heads/"): + dst = dst[len("refs/heads/"):] + if dyn and CUR_BRANCH.fullmatch(dst): + add(Target("git", path, "push")) # цель — $(git branch --show-current) и т. п., то есть текущая ветка + continue + if dst == "HEAD" and not delete and not colon: + add(Target("git", path, "push")) + continue + name = _branch((dst, dyn)) + if name is None: + add(Target("git", path, "push?")) + elif name in ("main", "master"): + add(Target("git", path, "push>" + name)) + + +# ---------- файловая система и вердикт ---------- + +_cache = {} + + +def _git_out(d, *args): + env = {k: v for k, v in os.environ.items() if k not in ("GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE")} + r = subprocess.run(["git", "-C", d] + list(args), stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, + universal_newlines=True, timeout=5, env=env) + return r.stdout.strip() if r.returncode == 0 else None + + +def resolve(path, follow=True): + """(корень, ветка, реальный путь) git-репозитория, в котором лежит path, или None — вне репозитория. + Ветка '' — detached HEAD. Симлинки раскрываются до поиска (в том числе висячие: запись создаст файл у цели + ссылки), затем берётся ближайший существующий предок. follow=False — для команд, меняющих саму ссылку + (rm, ln, mv): раскрывается только каталог, последний элемент пути остаётся как есть.""" + if follow: + real = p = os.path.realpath(path) + else: + p = os.path.realpath(os.path.dirname(path)) # поиск от каталога: exists/isdir разыменовали бы ссылку + real = os.path.join(p, os.path.basename(path)) + while not os.path.exists(p): + parent = os.path.dirname(p) + if parent == p: + return None + p = parent + d = p if os.path.isdir(p) else os.path.dirname(p) + if d not in _cache: + root = _git_out(d, "rev-parse", "--show-toplevel") + branch = _git_out(root, "branch", "--show-current") if root else None + _cache[d] = (root, branch or "") if root else None + hit = _cache[d] + if not hit: + return None + return hit[0], hit[1], real + + +def _label(branch): + return "detached HEAD" if branch == "" else branch + + +WT_HINT = "Работай в worktree на ветке (superpowers:using-git-worktrees)." + + +def decide_guard(tool, ti, cwd): + """Сообщение блокировки guard-main или None.""" + cwd = cwd or None + if tool == "Bash": + targets = parse(ti.get("command") or "", cwd) + elif tool in ("Edit", "Write", "MultiEdit"): + fp = ti.get("file_path") or "" + targets = [Target("file", _abs(cwd, fp), tool)] if fp else [] + else: + return None + over = {} # корень → ветка после git switch/checkout -b в этой же команде (None — не определена) + for t in targets: + if t.kind == "switch": + r = resolve(t.path) if t.path else None + if r: + over[r[0]] = t.op + continue + if t.path is None: + if t.kind == "git": + if t.op == "push?": + return "BLOCKED [guard-main]: git push — целевая ветка не определена. Укажи ветку явно." + return ("BLOCKED [guard-main]: git %s — репозиторий не определён (переменная в -C, --git-dir или " + "--work-tree). Укажи git -C <абсолютный путь>." % t.op) + continue + r = resolve(t.path, follow=not (t.kind == "file" and t.op in NOFOLLOW)) + if not r: + continue + root, branch, real = r + if root in over: + branch = over[root] + if t.kind == "git": + if t.op.startswith("push>") or t.op == "push --all": + return "BLOCKED [guard-main]: git %s — репозиторий %s. В main только через PR." % ( + t.op.replace(">", " в "), root) + if t.op == "push?": + return "BLOCKED [guard-main]: git push — целевая ветка не определена. Укажи ветку явно." + if branch is None or branch in PROTECTED: + return "BLOCKED [guard-main]: git %s — репозиторий %s на %s. В main только через PR. %s" % ( + t.op, root, "неизвестной ветке" if branch is None else _label(branch), WT_HINT) + elif branch in PROTECTED: + return "BLOCKED [guard-main]: запись в %s — репозиторий %s на %s. %s" % ( + os.path.relpath(real, root), root, _label(branch), WT_HINT) + return None + + +def decide_plan(fp, cwd): + """Сообщение блокировки require-plan или None.""" + path = _abs(cwd or None, fp) if fp else None + r = resolve(path) if path else None + if not r: + return None + root, branch, real = r + rel = os.path.relpath(real, root) + cp = os.path.join(root, ".claude", "code-paths") + if os.path.isfile(cp): + prefixes = [l.split("#", 1)[0].strip().rstrip("/") for l in open(cp, encoding="utf-8", errors="replace")] + protected = any(p and rel.startswith(p + "/") for p in prefixes) + else: + protected = "/src/" in "/" + rel + if not protected or os.path.exists(os.path.join(root, ".bounded")): + return None + plans = sorted(glob.glob(os.path.join(root, "docs", "superpowers", "plans", "*.md"))) + if plans: + if branch == "" or len(plans) == 1: + return None + for p in plans: + if branch in open(p, encoding="utf-8", errors="replace").read(): + return None + return ("BLOCKED [require-plan]: правка %s без плана для ветки %s. Запусти superpowers:brainstorming; " + "для bounded-задачи создай %s/.bounded." % (rel, _label(branch), root)) + + +def main(argv): + mode = argv[1] if len(argv) > 1 else "" + if mode == "targets": + try: + ts = parse(argv[2] if len(argv) > 2 else "", argv[3] if len(argv) > 3 else os.getcwd()) + except Exception as e: + print("error %s" % e) + return 1 + for t in ts: + print(t.kind, "-" if t.path is None else t.path, "-" if t.op is None else t.op) + return 0 + try: + d = json.loads(sys.stdin.read()) + ti = d.get("tool_input") or {} + if mode == "guard-main": + msg = decide_guard(d.get("tool_name") or "", ti, d.get("cwd") or "") + elif mode == "require-plan": + msg = decide_plan(ti.get("file_path") or ti.get("path") or "", d.get("cwd") or "") + else: + msg = None + except Exception: + return 0 + if msg: + sys.stderr.write(msg + "\n") + return 2 + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/hooks/require-plan.sh b/hooks/require-plan.sh index 3d099f9..7c70ad2 100755 --- a/hooks/require-plan.sh +++ b/hooks/require-plan.sh @@ -1,33 +1,8 @@ #!/usr/bin/env bash -# PreToolUse hook: edits in code dirs (any src/ by default, or .claude/code-paths) require a plan for this branch or a .bounded marker. -set -euo pipefail -input=$(cat) -read -r cwd fpath < <(printf '%s' "$input" | python3 -c ' -import sys,json -d=json.load(sys.stdin); ti=d.get("tool_input",{}) or {} -print(d.get("cwd",""), ti.get("file_path") or ti.get("path") or "")' 2>/dev/null || echo " ") -[ -n "$cwd" ] && [ -n "$fpath" ] || exit 0 -root=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null || true) -[ -n "$root" ] || exit 0 -# Защищаемые пути. По умолчанию — любой каталог src/ на любой глубине (src/, api/src/, packages/*/src/). -# Проект может переопределить списком префиксов (относительно корня, по одному на строку) в .claude/code-paths. -rel=${fpath#"$root"/} -protected=0 -if [ -f "$root/.claude/code-paths" ]; then - while IFS= read -r p; do - p=${p%%#*}; p=${p## }; p=${p%% }; [ -n "$p" ] || continue - case "$rel" in "${p%/}"/*) protected=1; break ;; esac - done < "$root/.claude/code-paths" -else - case "/$rel" in */src/*) protected=1 ;; esac -fi -[ "$protected" = 1 ] || exit 0 -[ -f "$root/.bounded" ] && exit 0 -branch=$(git -C "$cwd" branch --show-current 2>/dev/null || true) -if ls "$root"/docs/superpowers/plans/*.md >/dev/null 2>&1; then - if [ -z "$branch" ] || grep -lq -- "$branch" "$root"/docs/superpowers/plans/*.md 2>/dev/null; then exit 0; fi - # any plan exists but none names this branch: still allow if only one plan (single-feature repo) - [ "$(ls "$root"/docs/superpowers/plans/*.md | wc -l)" -eq 1 ] && exit 0 -fi -echo "BLOCKED: editing $fpath without a plan. Run superpowers:brainstorming; for a bounded change create '$root/.bounded' first." >&2 -exit 2 +# PreToolUse hook (Edit/Write/MultiEdit): правки в каталогах кода (любой src/ или .claude/code-paths) — только при +# плане для ветки или файле .bounded. Репозиторий — тот, где лежит файл; логика в harness_target.py рядом. +py="${0%/*}/harness_target.py" +command -v python3 >/dev/null 2>&1 && [ -f "$py" ] || exit 0 +python3 "$py" require-plan +[ $? -eq 2 ] && exit 2 +exit 0 diff --git a/install.sh b/install.sh index ba1bc01..17f0228 100755 --- a/install.sh +++ b/install.sh @@ -25,7 +25,7 @@ check() { echo " WARN codegraph prompt-hook в settings.json — рекомендуется отключить на время пилота" fi echo "== Установленная часть обвязки ($CC)" - for f in CLAUDE.md hooks/guard-main.sh hooks/require-plan.sh hooks/harness-check.sh hooks/role-hint.sh \ + for f in CLAUDE.md hooks/guard-main.sh hooks/require-plan.sh hooks/harness_target.py hooks/harness-check.sh hooks/role-hint.sh \ roles/dev.md roles/admin.md roles/analyst.md roles/qa.md skills/role/SKILL.md agents/briefing.md \ harness/adopt-project.sh harness/new-project.sh harness/link-roles.sh skills/harness-doctor/SKILL.md; do [ -f "$CC/$f" ] && echo " ok $f" || { echo " MISS $f (bash install.sh)"; ok=1; } @@ -45,7 +45,8 @@ fi cp "$HERE/global/CLAUDE.md" "$CC/CLAUDE.md" # 2. Хуки -cp "$HERE/hooks/guard-main.sh" "$HERE/hooks/require-plan.sh" "$HERE/hooks/harness-check.sh" "$HERE/hooks/role-hint.sh" "$CC/hooks/" +cp "$HERE/hooks/guard-main.sh" "$HERE/hooks/require-plan.sh" "$HERE/hooks/harness-check.sh" "$HERE/hooks/role-hint.sh" \ + "$HERE/hooks/harness_target.py" "$CC/hooks/" # 2a. Скрипты бутстрапа проекта — агент запускает их из сессии mkdir -p "$CC/harness" rm -rf "$CC/harness/project-template" @@ -93,6 +94,6 @@ json.dump(s, open(sp,"w"), ensure_ascii=False, indent=2) print("settings.json: hooks merged") PY -echo "done: $CC/CLAUDE.md, $CC/skills/harness-doctor, $CC/hooks/{guard-main,require-plan,harness-check,role-hint}.sh, $CC/roles/, $CC/skills/role, $CC/agents/briefing.md, $CC/harness/, ~/.config/caveman/config.json" +echo "done: $CC/CLAUDE.md, $CC/skills/harness-doctor, $CC/hooks/{guard-main,require-plan,harness-check,role-hint}.sh + harness_target.py, $CC/roles/, $CC/skills/role, $CC/agents/briefing.md, $CC/harness/, ~/.config/caveman/config.json" echo "Плагины Claude Code ставятся вручную: /plugin install superpowers@claude-plugins-official" check || true diff --git a/openspec/changes/hooks-write-target/.openspec.yaml b/openspec/changes/hooks-write-target/.openspec.yaml new file mode 100644 index 0000000..a216797 --- /dev/null +++ b/openspec/changes/hooks-write-target/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-15 diff --git a/openspec/changes/hooks-write-target/design.md b/openspec/changes/hooks-write-target/design.md new file mode 100644 index 0000000..68efa00 --- /dev/null +++ b/openspec/changes/hooks-write-target/design.md @@ -0,0 +1,220 @@ +# Design: хуки решают по цели записи + +Мотивация — `proposal.md`, требования — `specs/hook-guards/spec.md`. Здесь — как это устроено. + +## Context + +- `hooks/guard-main.sh` (44 строки): берёт ветку cwd; для Bash ищет в тексте команды регулярное выражение записи + (`>`, `tee`, `rm`, `git add|commit|push`, `npm i` …); исключения — литералы `git worktree`, `git checkout -b`, + `openspec archive`, `adopt-project.sh` и openspec-only коммит/push. +- `hooks/require-plan.sh` (33 строки): корень и ветка — от cwd; защищённые пути — `src/` или `.claude/code-paths`. +- Ограничения пакета (CLAUDE.md): bash и markdown, `python3` в PATH обязателен, других зависимостей нет; блок — + только `exit 2` с сообщением в stderr; любой сбой разбора — пропуск. +- Самотест хуков в `doctor.sh` — 9 случаев на установленных копиях; `tests/run.sh` хуки guard-main/require-plan не + покрывает. +- Сырьё: `docs/retro/fixtures/hook-blocks.jsonl` — 126 записей из 42 сессий (выгрузка 12.10.2026 на рабочей машине, + коммит 2549e86). Пути — с другой машины (`/Users/andrey.epifancev/projects/awp-ca/...`), поле `branch` в 108 + записях равно `HEAD` и для воспроизведения непригодно. +- O-1. Настоящих ошибок хука в выгрузке 76 (результат начинается с `PreToolUse:<инструмент> hook error`); 49 записей — + вывод успешных команд, где встретилось слово BLOCKED: `cat` файла памяти `workspace-hooks-and-worktrees.md` и + исходников хуков; ещё одна — вывод теста require-plan. Тем же фильтром пользуется `retro_blocks.py`, так что счёт + RT-1 в ретро завышен. Шумовые записи остаются в корпусе как случаи «пропустить»: в реальности они прошли. + +Разбивка сырья (запись может попасть в несколько классов): 65 — `N>/dev/null` или `N>&M`; 54 — путь в `~/.claude`; +48 — `cd` внутри команды; 14 — heredoc; 11 — cwd в `.wt/`; 10 — `git -C … worktree`; 10 — Edit/Write вне +репозитория; 8 — `git commit/push`; 3 — `mkdir -p .wt/`; 1 — `sed -i` (c083: правил файл памяти, а не docs, как +считалось в ретро, — тоже ложный блок). По разметке блок ожидается у 4 записей: архивные коммиты openspec прямо в main +с push в main (c031, c069, c070, c103). + +## Goals / Non-Goals + +**Goals:** +- Одна модель решения для обоих хуков: цель записи → репозиторий → ветка. +- Разбор команды проверяется без файловой системы; вердикт — на корпусе реальных команд в песочнице. +- Существующие установки обновляются повторным `install.sh` без перерегистрации хуков. + +**Non-Goals:** +- Полный разбор shell: подстановки, переменные, функции, алиасы не вычисляются. +- require-plan для записей через Bash (matcher хука остаётся Edit|Write|MultiEdit). +- Учёт `.gitignore`: запись в игнорируемый путь репозитория на main блокируется так же, как в отслеживаемый. +- allow-правило классификатора для удаления веток (B3), workspace-хук awp-ca (B10). + +## Decisions + +### 1. Ядро на Python, хуки — обёртки +``` +settings.hooks.json (без изменений) + ▼ ▼ +hooks/guard-main.sh hooks/require-plan.sh + нет python3 → exit 0 то же + exec python3 harness_target.py guard-main | require-plan + ▼ +hooks/harness_target.py (stdlib) + parse(cmd, cwd) → [Target(kind=file|git, path|None, op)] чистая функция + resolve(path) → (root, branch) | None git от ближайшего существующего предка + decide(hook, input) → allow | block(сообщение) + CLI: guard-main | require-plan — тело хука (stdin JSON → exit 0/2, stderr) + targets — отладка: цели в JSON +``` +Отдельный `.py`, а не встроенный heredoc: лексер и разбор с `cd`, heredoc и рекурсией `sh -c` — около 450 строк, +внутри bash он нечитаем и не тестируется по частям. Имена `.sh` остаются точками входа, поэтому `settings.hooks.json`, +`doctor.sh` и слияние настроек в `install.sh` не меняются. `install.sh` копирует `harness_target.py` рядом с хуками, +`--check` и `doctor.sh` проверяют его наличие. + +Альтернативы: (B) точечно чинить регулярные выражения в bash — дифф мал, но решение остаётся текстовым: `grep "npm i"`, +`cd` внутри команды и cwd оболочки продолжают давать ложные блоки; (C) проверять в Bash только git-мутации — молча +проходит распознанная запись редиректом или `sed -i` в main. Выбран A (владелец, 15.10.2026). + +### 2. Разбор команды (`parse`) +- Префикс дескриптора (`2>`, `2>&1`) распознаётся лексером только в начале токена, чтобы `echo 2 > f` и `cmd 2>f` + не путались. +- Простые команды разделяются по `;`, `&&`, `||`, `|`, `&` и переводам строк вне кавычек. `( … )` открывает вложенный + контекст каталога; `cd` из него наружу не выходит, через `|` — тоже. +- Голова команды ищется после присваиваний `VAR=x` и префиксов `sudo`, `env`, `time`, `nohup`, `command`, `xargs`. + У `xargs` цели неизвестны (пути приходят из stdin) — файловые цели неразрешимы, git-репозиторий — текущий каталог. +- Тело heredoc пропускается; редирект перед `<`, `git switch `, `git checkout -b|-B ` дают событие смены ветки; `decide` применяет его + к последующим целям того же репозитория (поток «ветка архива → add → commit → push» в одной команде, c072). +- Таблица глаголов и git-подкоманд — в спеке (требования «Редирект», «Команды записи», «Git-мутации», «push», + «Смена ветки»). + +### 3. `resolve` и вердикт +- Относительный путь склеивается с текущим каталогом разбора; берётся ближайший существующий предок, у него + раскрываются симлинки (`realpath`) — запись через симлинк workspace попадает туда, куда указывает ссылка. +- `git -C <предок> rev-parse --show-toplevel` → корень; `git -C <корень> branch --show-current` → ветка (пусто — + detached). Внутри `.git/` rev-parse не даёт корня — цель считается вне репозитория. Кеш по каталогу на вызов. +- Нет целей — git не вызывается (быстрый путь для чтения). + +| | Репозиторий на main/master/detached | На ветке | Вне репозитория | Не разрешилась | +|---|---|---|---|---| +| **file** | блок | пропуск | пропуск | пропуск | +| **git** | блок | пропуск | пропуск | блок | + +Гибрид выбран владельцем: файловую запись в main видно в `git status` и на ревью, git-мутация в main необратима. + +### 4. require-plan +`resolve(file_path)` даёт корень и ветку файла; дальше прежняя логика: `.claude/code-paths` корня или `src/` на любой +глубине, `.bounded` в корне, `docs/superpowers/plans/*.md` с именем ветки, единственный план, detached HEAD при наличии +плана. Логика переносится в Python как есть и фиксируется тестами. + +### 5. Сообщения +`BLOCKED [guard-main]: запись в <путь отн. корня> — репозиторий <корень> на <ветка>. Работай в worktree на ветке +(superpowers:using-git-worktrees).` Варианты для git-мутации, неопределённого репозитория (`Укажи git -C <абсолютный +путь>`) и require-plan. Префикс в квадратных скобках — для экстрактора и ретро-скриптов; старые английские фразы +экстрактор распознаёт по-прежнему. + +### 6. Корпус и прогон (B2) +``` +skills/harness-doctor/ + fixtures/hook-cases.jsonl размеченный корпус; первая строка — {"home": …, "layout": [...]} + scripts/hook_corpus.py [--summary] строит песочницу по layout и прогоняет корпус; exit 0/1 +docs/retro/fixtures/hook-blocks.jsonl сырая выгрузка, не меняется +docs/retro/scripts/hook_cases_build.py сборка корпуса из выгрузки: таблица разметки, layout, живые случаи +``` +Песочница и прогон — один Python-скрипт, а не два bash-скрипта: раскладка и записи — JSON, `subprocess` на запись +проще и быстрее, чем bash с встроенным python. Корпус — артефакт сборки: тест проверяет, что файл совпадает с выводом +`hook_cases_build.py`, так что разметка правится только в скрипте. +- Корпус лежит в skill, потому что `doctor.sh` проверяет установленные копии, а `install.sh` уже копирует + `skills/harness-doctor` целиком. +- Запись: поля выгрузки плюс `id` (`c001`…; живые — `l001`…), `expected`, `why` (класс: `devnull`, `read-only`, + `outside-repo`, `worktree`, `git-nonmut`, `push-feature`, `main-write`, `main-git`, `not-a-block`, …). Спорные — + `why` с суффиксом `?`; владелец смотрит список `block` и спорные. +- `layout`: список `{"path": …, "git": "main" | "<ветка>" | null, "worktree_of": …}` — репозитории awp-ca + (`arm-core`, `docs`, `tabular-parser`, `scan-gateway`, `arm-web`) на main, их worktree `.wt/<имя>/<репо>` на ветках, + `cc-skills/claude-harness` на main и его `.wt/*`, каталоги памяти вне git. +- Прогон: префикс `/Users/andrey.epifancev` в `cwd`, `command`, `file_path` заменяется на `$SANDBOX/home`, `HOME` + подменяется туда же. Edit/Write прогоняются через guard-main (и через require-plan, если у записи `hook` — + require-plan); Bash — через guard-main. +- `tests/test-hook-parse.sh` — ручные случаи на правила разбора, проверка вывода `targets`. +- `tests/test-hook-guard-main.sh` — сценарии спеки в песочнице из настоящих репозиториев. +- `tests/test-hook-require-plan.sh` — сценарии require-plan. +- `tests/test-hook-corpus.sh` — весь корпус через `hooks/*.sh`, assert на запись с `id` и `why` в имени. +- `tests/test-hook-export.sh` — экстрактор на фиктивном транскрипте. +- `doctor.sh`: наличие `harness_target.py`, прежние 9 случаев плюс «openspec-only коммит на main → exit 2», строка + корпуса. +- Разовая метрика для PR: в песочнице старые хуки дают на корпусе 41 блок (37 ложных), новые — 4 (0 ложных, + 0 пропущенных). + +### 7. Экстрактор +`hook_blocks_export.py` и `retro_blocks.py` считают результат блокировкой, только если он начинается с +`PreToolUse:<инструмент> hook error` (O-1); классификация хука — по префиксу `[guard-main]`/`[require-plan]`, затем +по старым фразам. Признак `is_error` не используется: в выгрузке его нет, проверить на старых транскриптах нельзя. + +## Risks / Trade-offs + +- [Разбор не видит запись внутри скрипта, `python3 -c`, `make`] → файловые записи и так fail-open по решению владельца; + git-мутации из скриптов не ловятся, как и сейчас. Видно в `git status` и на ревью. +- [Время старта python на каждый Bash-вызов, 30–50 мс] → быстрый путь без вызовов git; старый хук вызывал git на + каждом вызове. +- [Чужой `cd` в main-checkout при относительных командах implementer-а] → блок остаётся и он верный: коммит ушёл бы в + main. Правило «пути worktree абсолютные» уже в `roles/dev.md`; уточнение про относительные команды — там же. +- [Разметка корпуса делается агентом] → владелец проверяет список `block` и спорные; корпус — данные, правятся одной + строкой. +- [Корпус содержит рабочие пути и команды] → секреты заменены при выгрузке (``), репозиторий приватный; + корпус ставится только локально в `~/.claude/skills/harness-doctor/`. + +## Migration Plan + +1. PR в main, merge владельцем. +2. `bash install.sh` на каждой машине — подменяет тела хуков и кладёт `harness_target.py`; `settings.json` не меняется. +3. `doctor.sh` — самотест и корпус по установленным копиям. +4. Откат: `bash install.sh` из предыдущего тега; регистрация хуков та же. + +## Decisions made without discussion + +- 15.10.2026. Основа корпуса — реальная выгрузка (126 записей), а не синтетика по категориям ретро. Обоснование: + владелец выбрал синтетику, когда агент ошибочно считал данные недоступными; выгрузка нашлась в комментарии к #6. + Сказано владельцу в чате, возражений нет. Цена ошибки: переразметка корпуса. +- 15.10.2026. Запись в git-игнорируемый путь репозитория на main блокируется (без `git check-ignore`). Обоснование: + меньше правил, нет случаев в корпусе, где это дало бы ложный блок по делу. Цена ошибки: одно условие в `decide`. +- 15.10.2026. `NotebookEdit` не покрывается: matcher хуков не меняется. Цена ошибки: строка в `settings.hooks.json`. +- 15.10.2026. `git tag`, `git config`, `git remote`, `git gc` не считаются мутациями main. Цена ошибки: строка в + списке мутаций. +- 15.10.2026. `git checkout ` без `--` не считается мутацией: без файловой системы не отличить ветку от пути, а + чистый main-checkout отменять нечего. `git stash list|show` — чтение. Цена ошибки: условие в списке мутаций. +- 15.10.2026. Worktree задачи — `../cc-harness.wt/6-guard-main-target` (вне репозитория), так как `.wt/` не в + `.gitignore`. Цена ошибки: нет. +- 15.10.2026. Имя change — `hooks-write-target`, capability — `hook-guards`, версия — 0.4.0. Цена ошибки: + переименование до archive. +- 15.10.2026. Владелец велел после спеки сразу переходить к плану, после плана — к реализации, без отдельного ревью + спеки и плана. Режим исполнения — по `roles/dev.md` (SDD; до 5 задач — executing-plans). +- 16.10.2026. Добавлено требование «Смена ветки внутри команды» (на разметке корпуса, c072): без него поток архива + отдельным PR в одной команде (`switch -c chore/archive-… && add && commit`) блокировался бы. Цена ошибки: событие + switch в `decide`. +- 16.10.2026. Корпус: песочница и прогон — один `hook_corpus.py` вместо `hook-sandbox.sh` + `hook-corpus.sh`; + разметка — в `hook_cases_build.py`, корпус — его вывод. Цена ошибки: нет, интерфейс для тестов и doctor тот же. +- 16.10.2026. `retro_blocks.py` получает тот же фильтр ошибок хука, что и экстрактор (O-1), иначе метрика ложных + блокировок для `/harness-retro` (B12) считается с шумом. Цена ошибки: числа старого ретро не воспроизводятся + новым скриптом — они и были завышены. +- 16.10.2026. Шумовые записи корпуса (49) не удаляются, а размечаются `not-a-block`/`allow`: это реальные команды, + которые прошли, и новый хук обязан их пропускать. Цена ошибки: нет. +- 16.10.2026. Лексер собственный, без `shlex` (см. Decisions, 2). Цена ошибки: ~150 строк своего разбора; покрыт + `test-hook-parse` и корпусом. +- 16.10.2026, финальное ревью (I-1). `git checkout main|master` — смена ветки; `git checkout -`, `git switch -` и + ветка в переменной — ветка не определена (git-мутации после неё блокируются). `checkout <другое имя>` событием не + считается: без ФС не отличить ветку от пути. Цена ошибки: ложный блок `checkout "$f" && git commit`. +- 16.10.2026, финальное ревью (I-2). Префиксы `timeout`, `ionice`, `stdbuf` добавлены, у `sudo`, `nice`, `env`, + `timeout`, `ionice`, `stdbuf`, `xargs` известны опции со значением; у `timeout` снимается длительность; `env -C DIR` + меняет каталог только этой команды. Цена ошибки: незнакомая опция со значением снова спрячет голову (fail-open). +- 16.10.2026, финальное ревью (M-3). `rm`, `rmdir`, `unlink`, `ln`, `mv` оцениваются по месту ссылки, а не по её + цели. Известные пробелы: `rm -rf link/` с косой чертой (удаляет содержимое цели) и `mv a link-на-каталог` (кладёт + в цель) оцениваются по месту ссылки. Цена ошибки: пропуск записи в main через ссылку на каталог. +- 16.10.2026, финальное ревью (M-4). `file_path` Edit/Write — литерал: glob-обрезка только для путей из команд Bash. +- 16.10.2026, финальное ревью (M-5). `git push --tags` без refspec не считается push ветки; refspec-подстановка + текущей ветки — push текущей ветки. Цена ошибки: нет (подстановки перечислены явно, прочие — «не определена»). +- 16.10.2026, финальное ревью (M-7), решения прототипа, не записанные раньше: + - `git push` с неизвестной целевой веткой (`"$BR"`) блокируется при любой текущей ветке; в сообщении нет корня + репозитория. Цена ошибки: один ложный блок, сообщение просит указать ветку явно. + - Динамическое имя ветки со статическим префиксом, который не может стать защищённым (`feat/$ID`), считается + рабочей веткой (`_branch`). Цена ошибки: нет — `feat/…` не может быть `main`. + - `openspec new|init|update` считаются записью в cwd, как и `archive` (пишут в `openspec/`). + - Целями не считаются все `/dev/*`, а не только четыре пути из спеки (`/dev/fd/N` и т. п. — тоже не файлы + репозитория). + - Git-мутация при пустом `cwd` во входе хука блокируется («репозиторий не определён»). Claude Code `cwd` передаёт + всегда; пропуск такого входа — deferred (M-1). diff --git a/openspec/changes/hooks-write-target/proposal.md b/openspec/changes/hooks-write-target/proposal.md new file mode 100644 index 0000000..06aaf5a --- /dev/null +++ b/openspec/changes/hooks-write-target/proposal.md @@ -0,0 +1,53 @@ +# Proposal: хуки решают по цели записи + +Issue: #6. Источники: ретро `docs/retro/2026-10-11.md` — B1, B2, решение владельца 1; находки RT-1, RT-2, RT-6, RT-7. + +## Why + +guard-main решает по тексту команды и по cwd оболочки, а не по тому, куда идёт запись. По подсчёту ретро около трёх +четвертей блокировок за период пилота ложные: `2>/dev/null` считается записью, `grep "npm i"` — установкой пакета, Edit +файла памяти вне репозитория — записью в main, потому что cwd стоит на main-checkout (RT-1). Чужой `cd` останавливает +фоновый SDD-контроллер (RT-2), отказ хука запускает каскад отказов классификатора auto mode (RT-7). Реальных нарушений +main за тот же период — ноль, перехват по делу — единицы. Регрессионных тестов на реальных командах у хуков нет (RT-6), +поэтому любая правка регулярного выражения проверяется только в бою. require-plan болен тем же: корень, ветку, план и +`code-paths` он берёт от cwd, а не от репозитория правимого файла. + +## What Changes + +- guard-main определяет цели записи — файлы и git-мутации — и решает по ветке репозитория каждой цели. cwd служит + только базой для относительных путей. +- Неразрешимая файловая цель пропускается; git-мутация с неопределённым репозиторием блокируется. +- `git push` считается записью в main, только если он пушит в main; `push origin ` и + `push origin --delete ` из main-checkout проходят. +- **BREAKING:** исключение для openspec-only коммитов и для `openspec archive` на main удаляется (решение владельца 1: + архив всегда отдельным PR). Правила процесса уже приведены к этому в 0.3.0 (`roles/dev.md`); меняется поведение хука. +- `pip install` больше не блокируется (пишет в окружение, не в репозиторий); `npm/yarn/pnpm install` в репозитории на + main — блокируется, как и раньше. +- require-plan берёт корень, ветку, `.claude/code-paths`, `.bounded` и планы из репозитория правимого файла. +- Сообщения блокировки получают стабильный префикс `BLOCKED [guard-main]:` / `BLOCKED [require-plan]:`, называют цель + и выход. +- Регрессионный корпус: 126 записей из транскриптов плюс живые случаи, каждая с ожидаемым вердиктом; прогон из + `tests/run.sh` (источники) и из `doctor.sh` (установленные копии). +- Экстрактор `hook_blocks_export.py` выгружает только настоящие ошибки хуков. + +## Capabilities + +### New Capabilities +- `hook-guards`: блокирующие хуки обвязки — защита main/master/detached HEAD от записи (guard-main), требование плана + для правок в коде (require-plan), их регрессионный корпус. + +### Modified Capabilities + +Нет: `openspec/specs/` пуст, это первая capability обвязки. + +## Impact + +- Код: `hooks/guard-main.sh`, `hooks/require-plan.sh` (становятся обёртками), новый `hooks/harness_target.py` (stdlib + python3, новых зависимостей нет), `install.sh` (копирование и `--check`), `skills/harness-doctor/` (корпус, песочница, + раннер, строка в отчёте), `docs/retro/scripts/hook_blocks_export.py`. +- Тесты: `tests/test-hook-parse.sh`, `tests/test-hook-corpus.sh`, случаи require-plan. +- Документация: `README.md`, `docs/HARNESS.md`, `CLAUDE.md` и `ARCHITECTURE.md` репозитория, `roles/dev.md` (уточнение + про `cd`), `CHANGELOG.md` 0.4.0. +- Установленные копии обновляются повторным `bash install.sh`; регистрация хуков в `settings.json` не меняется. +- Вне задачи: B3 (allow-правило удаления веток), B4 кроме решения 1, B10 (workspace-хук awp-ca), запись в код через Bash + для require-plan. diff --git a/openspec/changes/hooks-write-target/specs/hook-guards/spec.md b/openspec/changes/hooks-write-target/specs/hook-guards/spec.md new file mode 100644 index 0000000..0bc8893 --- /dev/null +++ b/openspec/changes/hooks-write-target/specs/hook-guards/spec.md @@ -0,0 +1,326 @@ +# Spec Delta: hook-guards + +## Purpose + +Блокирующие хуки обвязки Claude Code: guard-main не даёт изменить main, master и detached HEAD в обход PR, require-plan +не даёт править код без плана или маркера `.bounded`. Хуки решают по цели записи и проверяются регрессионным корпусом +реальных команд. + +## ADDED 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** экстрактор эту запись не выгружает diff --git a/roles/dev.md b/roles/dev.md index 8acde19..12dcc0b 100644 --- a/roles/dev.md +++ b/roles/dev.md @@ -17,7 +17,7 @@ ### Исполнение - Работа только в worktree. Ветка называется по issue: `feat/-`. Пути worktree в планах и брифах — абсолютные. -- Пока работает фоновый SDD-контроллер, в главной сессии не делать `cd` в основной checkout: cwd оболочки общий, контроллер и implementer-ы получат `BLOCKED: shell write on main` и остановятся. +- Пока работает фоновый SDD-контроллер, в главной сессии не делать `cd` в основной checkout: cwd оболочки общий, и команды implementer-ов с относительными путями пойдут в main — guard-main их заблокирует, и они остановятся. Команды с абсолютными путями worktree и `git -C ` от cwd не зависят. - Режим по умолчанию: subagent-driven-development. Контроллер SDD запускается фоновым сабагентом **на модели sonnet** (не на модели сессии); сессия только ждёт его отчёт. Для планов до 5 задач — executing-plans (Native). Режим не спрашивать, он задан здесь. - Модели исполнителей по SDD Model Selection: задача с готовым кодом в плане — haiku, интеграция — sonnet, ревью всей ветки — opus. Модель в dispatch указывать всегда. - Ревьюер задачи проверяет trailer коммита (`Co-Authored-By`, как указано в плане); чужой trailer — находка `T.`, переписывать историю нельзя (force-push запрещён). diff --git a/skills/harness-doctor/SKILL.md b/skills/harness-doctor/SKILL.md index 53e2a05..555b971 100644 --- a/skills/harness-doctor/SKILL.md +++ b/skills/harness-doctor/SKILL.md @@ -30,6 +30,8 @@ description: Диагностика обвязки Claude Code (claude-harness) **Самотест хуков** — любое `WARN … (ожидалось N)` означает, что хук установлен, но не делает того, что должен. Причины по убыванию частоты: правлен руками; `python3` не в PATH сессии; settings.json зовёт другой путь. Чинится `install.sh`; если и после него WARN — приложить строку к находке, это дефект пакета. +**Корпус хуков** — `ok корпус хуков: N/N`: установленные хуки дают на реальных командах пилота те же вердикты, что размечены. `WARN корпус хуков: N/M` и строки над ним — расхождения: `id`, класс, ожидалось, получено, команда. После `install.sh` расхождение — регрессия пакета; разобрать одну команду: `python3 ~/.claude/hooks/harness_target.py targets '<команда>' `. `MISS корпус хуков` — нет `harness_target.py` или раннера, чинится `install.sh`. + **Проект** - `MISS CLAUDE.md`/`openspec/`/`.codegraph/` или `WARN без секции` → обвязка не подключена: `bash ~/.claude/harness/adopt-project.sh ` (сам делает ветку и ff-merge; дерево должно быть чистым). - «worktree; основной: …» + «MCP смотрит на индекс основного checkout» → в вызовах `codegraph_explore` передавать `projectPath=`; если индекса в worktree нет — `codegraph init` там. diff --git a/skills/harness-doctor/fixtures/hook-cases.jsonl b/skills/harness-doctor/fixtures/hook-cases.jsonl new file mode 100644 index 0000000..9f5cc07 --- /dev/null +++ b/skills/harness-doctor/fixtures/hook-cases.jsonl @@ -0,0 +1,130 @@ +{"home": "/Users/andrey.epifancev", "layout": [{"path": "projects/awp-ca"}, {"path": "projects/awp-ca/reference-old-arm"}, {"path": "projects/awp-ca/arm-core", "branch": "main"}, {"path": "projects/awp-ca/docs", "branch": "main"}, {"path": "projects/awp-ca/tabular-parser", "branch": "main"}, {"path": "projects/awp-ca/scan-gateway", "branch": "main"}, {"path": "projects/awp-ca/arm-web", "branch": "main"}, {"path": "projects/awp-ca/fixtures", "branch": "main"}, {"path": "projects/awp-ca/.wt/web-proto/arm-core", "worktree_of": "projects/awp-ca/arm-core", "branch": "feat/web-proto"}, {"path": "projects/awp-ca/.wt/web-proto/arm-web", "worktree_of": "projects/awp-ca/arm-web", "branch": "feat/web-proto"}, {"path": "projects/awp-ca/.wt/ocr-q1/fixtures", "worktree_of": "projects/awp-ca/fixtures", "branch": "feat/ocr-q1"}, {"path": "projects/awp-ca/.wt/ocr-q1/scan-gateway", "worktree_of": "projects/awp-ca/scan-gateway", "branch": "feat/ocr-q1"}, {"path": "projects/awp-ca/.wt/stmt-template/arm-web", "worktree_of": "projects/awp-ca/arm-web", "branch": "feat/stmt-template"}, {"path": "projects/awp-ca/.wt/qa-guide-web/docs", "worktree_of": "projects/awp-ca/docs", "branch": "feat/qa-guide-web"}, {"path": "projects/awp-ca/.wt/slice-0/scan-gateway", "worktree_of": "projects/awp-ca/scan-gateway", "branch": "feat/slice-0"}, {"path": "projects/awp-ca/.wt/slice-3b/arm-core", "worktree_of": "projects/awp-ca/arm-core", "branch": "feat/slice-3b"}, {"path": "projects/cc-skills/claude-harness", "branch": "main"}, {"path": "projects/cc-skills/claude-harness/.wt/retro", "worktree_of": "projects/cc-skills/claude-harness", "branch": "retro/2026-10-11"}, {"path": "projects/cc-skills/claude-harness/.wt/roles", "worktree_of": "projects/cc-skills/claude-harness", "branch": "feat/2-roles-and-memory"}, {"path": "projects/cc-skills/claude-harness/.wt/guard", "worktree_of": "projects/cc-skills/claude-harness", "branch": "feat/6-guard-main-target"}, {"path": ".claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory"}, {"path": ".claude/projects/-Users-andrey-epifancev-projects-cc-skills-claude-harness/memory"}, {"path": ".claude/hooks"}, {"path": ".claude/jobs"}]} +{"session": "0d5648e2", "ts": "2026-10-06T13:14:14.950Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cat /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/jetstream-check-later.md /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c001", "why": "not-a-block"} +{"session": "0e94130b", "ts": "2026-10-08T14:17:06.073Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "docker compose -f ../docs/dev/docker-compose.yml exec -T postgres sh -c 'psql -U \"$POSTGRES_USER\" -d \"$POSTGRES_DB\" -tAc \"set search_path=dkb; select (select count(*) from consolidation_version), (select count(*) from stored_file), (select count(*) from company_group), (select count(*) from app_user)\"'; T=$(curl -s -X POST localhost:8000/auth/dev-login -H 'content-type: application/json' -d '{\"email\":\"analyst@armka.local\"}' 2>/dev/null | python3 -c 'import sys,json;print(json.load(sys.stdin).get(\"access_token\",\"\"))' 2>/dev/null); echo \"token: curl -s -o /dev/null -w '%{http_code}\\n' localhost:3000/", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (docker compose -f ../docs/dev/docker-compose.yml exec -T postgres sh -c 'psql -U). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c002", "why": "unparseable"} +{"session": "13081199", "ts": "2026-10-08T09:00:56.719Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca; grep -n \"DEBUG\" arm-core/api/src/core/config.py scan-gateway/api/src/core/config.py tabular-parser/api/src/core/config.py; echo ---; sed -n 36,70p scan-gateway/api/src/app.py; echo; grep -n \"get_settings()\\|def create_app\\|^app = \\|FastAPI(\" scan-gateway/api/src/app.py arm-core/api/src/app.py; grep -rn \"setup_logger\\|diagnose\" tabular-parser/api/tests | head; cat ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c003", "why": "not-a-block"} +{"session": "1d7198e6", "ts": "2026-10-09T18:35:23.443Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory && cat arm-web-repo.md arm-web-demo-prototype.md pdfjs-legacy-yandex.md contour-back-dev-login.md snapshot-nested-75.md workspace-hooks-and-worktrees.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c004", "why": "not-a-block"} +{"session": "26b1e4e5", "ts": "2026-10-07T18:06:54.767Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core/api", "branch": "HEAD", "tool": "Bash", "command": "git -C .. log --oneline -8 main; ls migrations/versions 2>/dev/null || find . -path ./.venv -prune -o -name \"versions\" -type d -print; ", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (git -C .. log --oneline -8 main; ls migrations/versions 2>/dev/null || find . -p). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c005", "why": "devnull"} +{"session": "26b1e4e5", "ts": "2026-10-07T18:07:01.108Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core/api", "branch": "HEAD", "tool": "Bash", "command": "grep -rln \"legal_entity\" /Users/andrey.epifancev/projects/awp-ca/arm-core/api/src/db/migrations/versions /Users/andrey.epifancev/projects/awp-ca/arm-core/api/migrations/versions 2>/dev/null | xargs ls -t 2>/dev/null | head -5", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (grep -rln \"legal_entity\" /Users/andrey.epifancev/projects/awp-ca/arm-core/api/sr). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c006", "why": "devnull"} +{"session": "30bc3fca", "ts": "2026-10-08T18:08:27.859Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca/arm-core/api; grep -rn \"404\" src/api/dependencies src/api/endpoints/me.py src/core/exception_handlers.py | head -20; grep -n \"root_path\\|ROOT_PATH\\|mount\\|web_dir\" src/app.py src/core/config.py | head -20; grep -rn \"ROOT_PATH\\|API_URL\" ../web/rsbuild.config.ts ../web/.env* ../Dockerfile* ../web/Dockerfile* 2>/dev/null | head", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca/arm-core/api; grep -rn \"404\" src/api/). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c007", "why": "devnull"} +{"session": "30bc3fca", "ts": "2026-10-08T18:14:42.003Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca/arm-core; grep -rhoE \"[\\`'](/(admin|me|directories|documents|files|consolidations|company-groups|legal-entities|dev|users|statements|osv)[^\\`'?]*)\" web/src | sed -E \"s/^[\\`']//\" | sort -u > $CLAUDE_JOB_DIR/tmp/web_paths.txt; cat $CLAUDE_JOB_DIR/tmp/web_paths.txt; echo ----; cd api && .venv/bin/python -c \"\nfrom src.app import create_app\nfrom src.core.config import settings\napp=create_app(settings)\nfor r in app.routes:\n p=getattr(r,'path',None); m=getattr(r,'methods',None)\n if p and p.startswith('/api'): print(sorted(m or []), p)\n\" 2>&1 | sort -k2", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca/arm-core; grep -rhoE \"[\\`'](/(admin|m). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c008", "why": "var-target"} +{"session": "34b466b5", "ts": "2026-10-09T16:18:39.133Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "ls; grep -rn \"dev/token\\|AUTH_DEV_ISSUER\\|auth_dev_issuer\" --include='*.py' . 2>/dev/null | grep -v \"\\.wt\\|tests/\" | head -15; echo ---GATE; cat web/src/auth/AuthGate.tsx", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (ls; grep -rn \"dev/token\\|AUTH_DEV_ISSUER\\|auth_dev_issuer\" --include='*.py' . 2>). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c009", "why": "devnull"} +{"session": "34b466b5", "ts": "2026-10-09T16:24:03.128Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cat ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md; cat ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/arm-web-repo.md | head -30", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c010", "why": "not-a-block"} +{"session": "433a59c7", "ts": "2026-10-06T12:45:57.070Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cat \"/Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md\"; cd /Users/andrey.epifancev/projects/awp-ca && git -C docs status --short | head && git -C docs branch --show-current && git -C docs worktree list && ls .wt 2>/dev/null", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c011", "why": "not-a-block"} +{"session": "4cb93cc7", "ts": "2026-10-09T15:04:10.690Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core/api", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca && grep -rn \"CONTENT_TYPES\" arm-core/api/src --include=*.py | head -3; grep -n \"CONTENT_TYPES\" -A12 arm-core/api/src/domain/file_types.py; echo \"=== usage SourcePreview ===\"; grep -rn \"SourcePreview\\|fileId=\" arm-web/src --include=*.tsx | grep -v \"SourcePreview.tsx\" | head; echo \"=== miniopy pin ===\"; grep -n \"miniopy\\|aiohttp\" arm-core/api/pyproject.toml arm-core/api/requirements*.txt arm-core/api/uv.lock 2>/dev/null | head; echo \"=== S3 settings ===\"; grep -n \"S3\\|MINIO\\|OPENBAO\" arm-core/api/src/core/config.py | head -40; echo \"=== контур docs ===\"; grep -rln \"контур\" docs/dev/*.md | head", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca && grep -rn \"CONTENT_TYPES\" arm-core/). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c012", "why": "devnull"} +{"session": "4cb93cc7", "ts": "2026-10-09T15:04:23.215Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core/api", "branch": "HEAD", "tool": "Bash", "command": "cat ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/feedback-no-harness-bypass.md ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c013", "why": "not-a-block"} +{"session": "5bbf3523", "ts": "2026-10-08T12:51:16.774Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory; cat workspace-hooks-and-worktrees.md contour-transfer-queue.md logger-diagnose-state.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c014", "why": "not-a-block"} +{"session": "6cc6d498", "ts": "2026-10-08T19:39:34.332Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca; cat ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/contour-transfer-queue.md; ls .wt; ls docs/scripts; ls docs/tests 2>/dev/null; grep -rn \"prepare-contour\" docs --include=*.md --include=Makefile -l", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c015", "why": "not-a-block"} +{"session": "75185712", "ts": "2026-10-11T12:13:03.949Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca; f=~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/75185712-0f28-4c91-8f7c-a8dc174a367b/tool-results/bb63026cq.txt; grep -n -- '---HOOKS' $f; sed -n \"$(grep -n -- '---HOOKS' $f | cut -d: -f1),\\$p\" $f | head -400", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c016", "why": "not-a-block"} +{"session": "75185712", "ts": "2026-10-11T12:29:14.057Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd ~/projects/cc-skills/claude-harness; cat README.md | head -80; echo ---INSTALL; cat install.sh; echo ---TREE; find . -type f | grep -v examples | head -40; echo ---GLOBAL; ls global; echo ---CHANGELOG; head -30 CHANGELOG.md", "file_path": null, "hook": "other", "message": "# Ожидается BLOCKED и предложение создать worktree.", "expected": "allow", "id": "c017", "why": "not-a-block"} +{"session": "75185712", "ts": "2026-10-11T12:46:03.638Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /private/tmp/claude-501/-Users-andrey-epifancev-projects-awp-ca/75185712-0f28-4c91-8f7c-a8dc174a367b/scratchpad && python3 -I retro_stats.py ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/ > retro_stats.out 2>&1; wc -l retro_stats.out; cat retro_stats.out", "file_path": null, "hook": "guard-main(Bash)", "message": "[0d5648e2,13081199] BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main испо", "expected": "allow", "id": "c018", "why": "not-a-block"} +{"session": "75185712", "ts": "2026-10-11T12:46:25.920Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cat ~/.claude/hooks/guard-main.sh; echo ---WS; cat /Users/andrey.epifancev/projects/awp-ca/.claude/hooks/guard-main.sh; echo ---RP; cat ~/.claude/hooks/require-plan.sh", "file_path": null, "hook": "guard-main(Bash)", "message": " echo \"BLOCKED: shell write on $label ($(printf '%s' \"$cmd\" | head -c 80)). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly.\" >&2", "expected": "allow", "id": "c019", "why": "not-a-block"} +{"session": "7580b8ad", "ts": "2026-10-10T06:19:51.216Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "ls -la && echo ---WT--- && ls -la .wt 2>/dev/null && echo ---MEM--- && cat /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c020", "why": "not-a-block"} +{"session": "776ede53", "ts": "2026-10-11T20:43:57.614Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "ls; ls -d .codegraph; grep -rln \"consolidated\\|сводн\" --include='*.py' . 2>/dev/null | grep -v node_modules | grep -v tests | head -20", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (ls; ls -d .codegraph; grep -rln \"consolidated\\|сводн\" --include='*.py' . 2>). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directl", "expected": "allow", "id": "c021", "why": "devnull"} +{"session": "776ede53", "ts": "2026-10-12T05:30:13.767Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "grep -n \"@router\\.\\(post\\|put\\|delete\\)\" api/src/api/endpoints/directories/related_groups.py api/src/api/endpoints/directories/company_groups.py api/src/api/endpoints/directories/legal_entities.py | head -20; grep -n \"NATS_ENABLED\\|DEBUG\\|AUTH_DEV\\|S3\\|OPENBAO__\\|DB_SCHEMA\" .wt 2>/dev/null; grep -v '^#' /Users/andrey.epifancev/projects/awp-ca/.wt/web-proto/arm-core/api/.env | grep -v '^$' | sed 's/\\(SECRET\\|PASSWORD\\|KEY\\)=.*/\\1=***/'", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (grep -n \"@router\\.\\(post\\|put\\|delete\\)\" api/src/api/endpoints/directories/relat). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c022", "why": "devnull"} +{"session": "776ede53", "ts": "2026-10-12T05:31:39.187Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/tabular-parser", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca/.wt/web-proto/arm-core/api && (lsof -nP -iTCP:8000 -sTCP:LISTEN || true) && nohup .venv/bin/python run_server.py > /private/tmp/claude-501/-Users-andrey-epifancev-projects-awp-ca/776ede53-781a-412c-a70e-2dce0962b8fe/scratchpad/arm-core.log 2>&1 &\nsleep 8; curl -s -m 5 localhost:8000/healthcheck; echo; curl -s -m 5 localhost:8001/healthcheck", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca/.wt/web-proto/arm-core/api && (lsof -). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c023", "why": "outside-repo"} +{"session": "776ede53", "ts": "2026-10-12T05:36:58.887Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Write", "command": null, "file_path": "/private/tmp/claude-501/-Users-andrey-epifancev-projects-awp-ca/776ede53-781a-412c-a70e-2dce0962b8fe/scratchpad/synth/run.py", "hook": "guard-main(Edit/Write)", "message": "PreToolUse:Write hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: you are on main. Create an isolated worktree first (superpowers:using-git-worktrees), never edit main directly.", "expected": "allow", "id": "c024", "why": "outside-repo"} +{"session": "778bb3cc", "ts": "2026-10-07T14:40:20.704Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/ && cat workspace-hooks-and-worktrees.md frontend-auth-guide-state.md core-42-front-gaps.md contour-transfer-queue.md audit-until-osv.md qa-local-stand.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c025", "why": "not-a-block"} +{"session": "778bb3cc", "ts": "2026-10-07T14:41:37.739Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core/api", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/.claude/jobs/0d5648e2/tmp && cat > dump_openapi.py <<'EOF'\nimport json, sys, os\nsys.path.insert(0, \"/Users/andrey.epifancev/projects/awp-ca/arm-core/api\")\nos.chdir(\"/Users/andrey.epifancev/projects/awp-ca/arm-core/api\")\nfrom src import app\nspec = app.openapi()\njson.dump(spec, open(sys.argv[1], \"w\"), ensure_ascii=False, indent=1)\nfor p, ops in spec[\"paths\"].items():\n for m, op in ops.items():\n print(m.upper(), p, \"|\", op.get(\"summary\",\"\"))\nEOF\n/Users/andrey.epifancev/projects/awp-ca/.wt/kpp/arm-core/api/.venv/bin/python -B dump_openapi.py /Users/andrey.epifancev/.claude/jobs/0d5648e2/tmp/openapi.json 2>&1 | tail -120", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/.claude/jobs/0d5648e2/tmp && cat > dump_openapi.py <<). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c026", "why": "outside-repo"} +{"session": "8105dccb", "ts": "2026-10-08T07:39:44.661Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "ls dkb_24.09.26 | head -30; grep -rl \"Тип контрагента\" dkb_24.09.26 2>/dev/null | head", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (ls dkb_24.09.26 | head -30; grep -rl \"Тип контрагента\" dkb_24.09.2). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly.", "expected": "allow", "id": "c027", "why": "devnull"} +{"session": "8105dccb", "ts": "2026-10-08T07:52:37.175Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cat /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/arm-web-repo.md; ls -la /Users/andrey.epifancev/projects/awp-ca/.wt/web-proto/", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c028", "why": "not-a-block"} +{"session": "8105dccb", "ts": "2026-10-08T08:41:22.920Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca/.wt/ocr-q1/fixtures && python3 -I - README.md <<'PY'\nimport sys\np=sys.argv[1]; s=open(p,encoding='utf-8').read()\na=\"\"\" aep_6.pdf → aep_6.json (баланс + ОФР, успех); aep_5, aep_8 — ответы-ошибки\n\"\"\"\nb=\"\"\" aep_6.pdf → aep_6.json (баланс + ОФР, успех); aep_5, aep_8 — ответы-ошибки\n ББ/ОФР группы РБ за 1 кв. 2025 (ответы настоящей ИС, прогон тестировщика 08.10.2026; PDF —\n копии из statements/, эмулятор ищет пары только в ocr/): «Бухгалтерская отчетность 1 кв.25»\n (АО РБ) и «БО 1 кв 2025» (Счастливочная) — .pdf/.PDF + .json, баланс + ОФР, needs_check;\n ИС склеила строки 1500 и 1700 в одну ячейку — нормализатор их пока пропускает (см. эталоны)\n\"\"\"\nassert s.count(a)==1; s=s.replace(a,b); open(p,'w',encoding='utf-8').write(s)\nPY\ngit add README.md \"ocr/БО 1 кв 2025.PDF\" \"ocr/БО 1 кв 2025.json\" \"ocr/Бухгалтерская отчетность 1 кв.25.pdf\" \"ocr/Бухгалтерская отчетность 1 кв.25.json\" \"ocr/expected_normalize/БО 1 кв 2025.json\" \"ocr/expected_normalize/Бухгалтерская отчетность 1 кв.25.json\" && git commit -q -F - <<'EOF'\nfixtures(ocr): ответы ИС Сканирование по ББ/ОФР РБ и Счастливочной за 1 кв. 2025 (#15)\n\nПары для эмулятора (поиск по sha256 только в ocr/): PDF скопированы из statements/,\nJSON — элемент files[] ответа настоящей ИС (прогон тестировщика 08.10.2026), file_uid =\nимя набора, как в aep_6.json. Эталоны нормализатора — make golden-update в scan-gateway\n(main 2026-10-08): оба набора needs_check, по 2 документа (BALANCE + INCOME); старые\nэталоны не изменились.\n\nИзвестное ограничение, зафиксированное эталоном: ИС склеила строки 1500 и 1700\nв одну ячейку («1500\\n1700», суммы через \\n) — нормализатор пропускает строку как\n«нечисловой код», в документах нет 1500 и 1700.\n\nCo-Authored-By: Claude Opus 5.5 \nEOF\ngit log --oneline -1; git push -q -u origin feat/15-ocr-2025-q1 2>&1 | tail -1", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on detached HEAD (cd /Users/andrey.epifancev/projects/awp-ca/.wt/ocr-q1/fixtures && python3 -I - R). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land o", "expected": "allow", "id": "c029", "why": "worktree"} +{"session": "8105dccb", "ts": "2026-10-08T08:49:24.280Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "git switch -q -c feat/16-split-merged-codes && touch .bounded && grep -n bounded .gitignore; codegraph init 2>&1 | tail -1; git status -sb | head -3", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on detached HEAD (git switch -q -c feat/16-split-merged-codes && touch .bounded && grep -n bounded). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land o", "expected": "allow", "id": "c030", "why": "outside-repo"} +{"session": "8105dccb", "ts": "2026-10-08T14:10:53.578Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "git add openspec && git status --short | grep -v \"^[RAMD] *openspec/\" ; git commit -q -F - <<'EOF'\ndocs(openspec): архив auto-confirm-clean-documents — спеки osv-result-mapping, primary-files, audit-trail (#64)\n\nCo-Authored-By: Claude Opus 5.5 \nEOF\ngit log --oneline -1 && git push -q origin main 2>&1 | tail -1", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (git add openspec && git status --short | grep -v \"^[RAMD] *openspec/\" ; git comm). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "block", "id": "c031", "why": "main-git"} +{"session": "8105dccb", "ts": "2026-10-08T14:11:08.484Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "cat ~/.claude/hooks/guard-main.sh | head -120", "file_path": null, "hook": "guard-main(Bash)", "message": " echo \"BLOCKED: shell write on $label ($(printf '%s' \"$cmd\" | head -c 80)). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly.\" >&2", "expected": "allow", "id": "c032", "why": "not-a-block"} +{"session": "8782a981", "ts": "2026-10-09T10:54:58.961Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/scan-gateway/api", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca && grep -rniE \"smb|register_session\" reference-old-arm --include=*.py --include=*.yml --include=*.yaml --include=*.env* 2>/dev/null | grep -v node_modules | head -30; echo ----; grep -rniE \"SMB_LOGIN|OPENBAO__SMB|smb\" docs --include=*.md -l | head; ", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca && grep -rniE \"smb|register_session\" ). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c033", "why": "devnull"} +{"session": "8782a981", "ts": "2026-10-09T12:18:48.416Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/scan-gateway", "branch": "HEAD", "tool": "Bash", "command": "cat /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/feedback-execution-mode-from-claude-md.md /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/logger-diagnose-state.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c034", "why": "not-a-block"} +{"session": "8782a981", "ts": "2026-10-09T14:19:05.748Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/scan-gateway/api", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca && grep -n \"smbprotocol\" scan-gateway/api/requirements.txt; P=$(find / -path \"*/site-packages/smbclient/_pool.py\" 2>/dev/null | head -1); echo \"$P\"; D=$(dirname $(dirname \"$P\")); ls $D | grep -i \"^smbprotocol-\"; grep -rn \"STATUS_NOT_FOUND\" $D/smbclient $D/smbprotocol | head -20", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca && grep -n \"smbprotocol\" scan-gateway). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c035", "why": "devnull"} +{"session": "9057cf5f", "ts": "2026-10-07T20:00:38.444Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "ls; cat ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/frontend-api-guide-state.md; tea repos ls --login home 2>/dev/null | head -20", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c036", "why": "not-a-block"} +{"session": "9057cf5f", "ts": "2026-10-07T20:00:49.740Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-web", "branch": "HEAD", "tool": "Bash", "command": "git log --stat --format='--- %h %s (%ad)' --date=iso | head -120; cat package.json rsbuild.config.ts; ls src/conf; cat src/conf/* 2>/dev/null | head -40; find src -type f | xargs wc -l | sort -n | tail -70", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (git log --stat --format='--- %h %s (%ad)' --date=iso | head -120; cat package.js). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c037", "why": "devnull"} +{"session": "925f14ee", "ts": "2026-10-09T15:58:02.871Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/scan-gateway", "branch": "HEAD", "tool": "Bash", "command": "grep -rn -B2 -A8 \"D-6\\|D-7\" openspec/specs/*/spec.md openspec/specs/*.md 2>/dev/null | head -80; echo ----RULES; grep -n -A12 \"^labels\" api/rules/*.yaml 2>/dev/null || grep -rn -A10 \"labels:\" api/src/normalizer/rules/*.yaml api/rules 2>/dev/null | head -40; echo ----RAW; find /Users/andrey.epifancev/projects/awp-ca -path '*f37d677c*' 2>/dev/null | head", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (grep -rn -B2 -A8 \"D-6\\|D-7\" openspec/specs/*/spec.md openspec/specs/*.md 2>/dev/). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c038", "why": "devnull"} +{"session": "925f14ee", "ts": "2026-10-09T16:02:27.157Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/scan-gateway", "branch": "HEAD", "tool": "Bash", "command": "cat /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md; echo ----; cd /Users/andrey.epifancev/projects/awp-ca/scan-gateway && git worktree list && git status --short | head && git log --oneline -3", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c039", "why": "not-a-block"} +{"session": "925f14ee", "ts": "2026-10-09T16:02:31.733Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/scan-gateway", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca && git -C scan-gateway fetch -q origin && git -C scan-gateway status -sb | head -2 && mkdir -p .wt/type-by-codes && git -C scan-gateway worktree add .wt/../.wt/type-by-codes/scan-gateway -b tmp/type-by-codes origin/main 2>&1 | tail -2; ls .wt/type-by-codes", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca && git -C scan-gateway fetch -q origi). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c040", "why": "worktree-setup"} +{"session": "a0a8c17e", "ts": "2026-10-09T08:54:06.683Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/.wt/stmt-template/arm-web", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/ && cat auto-confirm-64.md jetstream-check-later.md workspace-hooks-and-worktrees.md logger-diagnose-state.md | grep -v '^\\s*$'", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c041", "why": "not-a-block"} +{"session": "a22dee1e", "ts": "2026-10-08T07:39:44.661Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/docs/mockup", "branch": "HEAD", "tool": "Bash", "command": "ls dkb_24.09.26 | head -30; grep -rl \"Тип контрагента\" dkb_24.09.26 2>/dev/null | head", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (ls dkb_24.09.26 | head -30; grep -rl \"Тип контрагента\" dkb_24.09.2). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly.", "expected": "allow", "id": "c042", "why": "devnull"} +{"session": "a22dee1e", "ts": "2026-10-08T07:52:37.175Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/.wt/web-proto/arm-web", "branch": "HEAD", "tool": "Bash", "command": "cat /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/arm-web-repo.md; ls -la /Users/andrey.epifancev/projects/awp-ca/.wt/web-proto/", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c043", "why": "not-a-block"} +{"session": "a22dee1e", "ts": "2026-10-08T08:41:22.920Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/.wt/ocr-q1/scan-gateway/api", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca/.wt/ocr-q1/fixtures && python3 -I - README.md <<'PY'\nimport sys\np=sys.argv[1]; s=open(p,encoding='utf-8').read()\na=\"\"\" aep_6.pdf → aep_6.json (баланс + ОФР, успех); aep_5, aep_8 — ответы-ошибки\n\"\"\"\nb=\"\"\" aep_6.pdf → aep_6.json (баланс + ОФР, успех); aep_5, aep_8 — ответы-ошибки\n ББ/ОФР группы РБ за 1 кв. 2025 (ответы настоящей ИС, прогон тестировщика 08.10.2026; PDF —\n копии из statements/, эмулятор ищет пары только в ocr/): «Бухгалтерская отчетность 1 кв.25»\n (АО РБ) и «БО 1 кв 2025» (Счастливочная) — .pdf/.PDF + .json, баланс + ОФР, needs_check;\n ИС склеила строки 1500 и 1700 в одну ячейку — нормализатор их пока пропускает (см. эталоны)\n\"\"\"\nassert s.count(a)==1; s=s.replace(a,b); open(p,'w',encoding='utf-8').write(s)\nPY\ngit add README.md \"ocr/БО 1 кв 2025.PDF\" \"ocr/БО 1 кв 2025.json\" \"ocr/Бухгалтерская отчетность 1 кв.25.pdf\" \"ocr/Бухгалтерская отчетность 1 кв.25.json\" \"ocr/expected_normalize/БО 1 кв 2025.json\" \"ocr/expected_normalize/Бухгалтерская отчетность 1 кв.25.json\" && git commit -q -F - <<'EOF'\nfixtures(ocr): ответы ИС Сканирование по ББ/ОФР РБ и Счастливочной за 1 кв. 2025 (#15)\n\nПары для эмулятора (поиск по sha256 только в ocr/): PDF скопированы из statements/,\nJSON — элемент files[] ответа настоящей ИС (прогон тестировщика 08.10.2026), file_uid =\nимя набора, как в aep_6.json. Эталоны нормализатора — make golden-update в scan-gateway\n(main 2026-10-08): оба набора needs_check, по 2 документа (BALANCE + INCOME); старые\nэталоны не изменились.\n\nИзвестное ограничение, зафиксированное эталоном: ИС склеила строки 1500 и 1700\nв одну ячейку («1500\\n1700», суммы через \\n) — нормализатор пропускает строку как\n«нечисловой код», в документах нет 1500 и 1700.\n\nCo-Authored-By: Claude Opus 5.5 \nEOF\ngit log --oneline -1; git push -q -u origin feat/15-ocr-2025-q1 2>&1 | tail -1", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on detached HEAD (cd /Users/andrey.epifancev/projects/awp-ca/.wt/ocr-q1/fixtures && python3 -I - R). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land o", "expected": "allow", "id": "c044", "why": "worktree"} +{"session": "a22dee1e", "ts": "2026-10-08T08:49:24.280Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/.wt/ocr-q1/scan-gateway", "branch": "HEAD", "tool": "Bash", "command": "git switch -q -c feat/16-split-merged-codes && touch .bounded && grep -n bounded .gitignore; codegraph init 2>&1 | tail -1; git status -sb | head -3", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on detached HEAD (git switch -q -c feat/16-split-merged-codes && touch .bounded && grep -n bounded). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land o", "expected": "allow", "id": "c045", "why": "worktree"} +{"session": "adba5129", "ts": "2026-10-11T16:41:25.025Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "ls arm-core/openspec/changes/archive | tail -3; D=$(ls -d arm-core/openspec/changes/archive/* | tail -1); ls -R \"$D\"; echo ----; head -40 \"$D/proposal.md\"; echo ----; head -60 \"$D\"/specs/*/spec.md | head -80; echo ----; grep -n \"^## \" \"$D/design.md\"; echo ----; cat .wt 2>/dev/null; grep -rn \"wt\" jira-hub/.gitignore; cat ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md | head -40", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c046", "why": "not-a-block"} +{"session": "b02ed030", "ts": "2026-10-08T19:24:09.355Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/reference-old-arm", "branch": "HEAD", "tool": "Bash", "command": "grep -rnE \"img_uploaded|img_parsed|tabular_uploaded|tabular_parsed|subject\" . 2>/dev/null | grep -v -E \"node_modules|\\.git/\" | cut -c1-250 | head -30", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (grep -rnE \"img_uploaded|img_parsed|tabular_uploaded|tabular_parsed|subject\" . 2>). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c047", "why": "devnull"} +{"session": "b02ed030", "ts": "2026-10-08T19:28:34.941Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory && cat workspace-hooks-and-worktrees.md logger-diagnose-state.md feedback-no-harness-bypass.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c048", "why": "not-a-block"} +{"session": "bce8483e", "ts": "2026-10-10T14:44:11.172Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/docs/context", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca && echo \"=== arm-web branches/log ===\"; git -C arm-web branch -a | head; git -C arm-web log --oneline -15 --all --date=short --format=\"%ad %s\" | head -20; echo \"=== arm-web routes ===\"; grep -rhoE \"path: *['\\\"][^'\\\"]+['\\\"]|]*path=['\\\"][^'\\\"]+['\\\"]\" arm-web/src 2>/dev/null | sort -u | head -40; echo \"=== arm-web pages ===\"; ls arm-web/src/pages 2>/dev/null; ls arm-web/src/app 2>/dev/null | head; echo \"=== memory arm-web ===\"; cat ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/arm-web-repo.md ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/arm-web-demo-prototype.md 2>/dev/null | grep -v \"^---\" | head -80", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca && echo \"=== arm-web branches/log ===). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c049", "why": "devnull"} +{"session": "bce8483e", "ts": "2026-10-10T14:44:19.830Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/docs/context", "branch": "HEAD", "tool": "Bash", "command": "ls /Users/andrey.epifancev/projects/awp-ca/arm-web/src/pages /Users/andrey.epifancev/projects/awp-ca/arm-web/src 2>/dev/null; grep -rhoE \"path=['\\\"][^'\\\"]+['\\\"]\" /Users/andrey.epifancev/projects/awp-ca/arm-web/src 2>/dev/null | sort -u | head -40", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (ls /Users/andrey.epifancev/projects/awp-ca/arm-web/src/pages /Users/andrey.epifa). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c050", "why": "devnull"} +{"session": "bce8483e", "ts": "2026-10-10T20:57:19.615Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-web", "branch": "HEAD", "tool": "Bash", "command": "echo \"--- чек-лист/замена/удаление/ЕЦП/история/состав где-либо\"; grep -rln -i \"чек-лист\\|checklist\" src | grep -v test; grep -rn -i \"заменить\\|удалить\\|replace\\|ЕЦП\\|История изменений\\|состав\" src --include=*.tsx 2>/dev/null | grep -v test | cut -c1-140 | head -12; echo \"--- CompanyGroupsTable / ConsoleLegalEntities\"; ls src/widgets/CompanyGroupsTable src/widgets/ConsoleLegalEntities; grep -rn -i \"Modal\\|Drawer\\|Tabs\\|onRow\\|navigate\" src/widgets/CompanyGroupsTable/*.tsx src/widgets/ConsoleLegalEntities/*.tsx | cut -c1-140 | head -10", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (echo \"--- чек-лист/замена/удаление/ЕЦП/история/с). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly.", "expected": "allow", "id": "c051", "why": "devnull"} +{"session": "bce8483e", "ts": "2026-10-10T20:57:22.616Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-web", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca && grep -rn -i \"reprocess\\|повторн\\|retry\\|attempt\" arm-core/api/src/api --include=*.py 2>/dev/null | grep -v test | head -5; grep -rln -i \"reprocess\\|повторная обработка\" arm-core/api/src | head -3; echo \"--- gitea open issues arm-web\"; tea issues list --repo RSHB/arm-web --state open --fields index,title 2>&1 | head -15; echo \"--- arm-core open\"; tea issues list --repo RSHB/arm-core --state open --fields index,title 2>&1 | head -15", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca && grep -rn -i \"reprocess\\|повто). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directl", "expected": "allow", "id": "c052", "why": "devnull"} +{"session": "bce8483e", "ts": "2026-10-10T20:57:48.631Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-web", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca/arm-web && grep -rn \"path=\" src/App.tsx src/app/*.tsx src/routes*.tsx 2>/dev/null | cut -c1-140 | head -20; ls src; ls src/pages 2>/dev/null || ls src/app 2>/dev/null", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca/arm-web && grep -rn \"path=\" src/App.t). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c053", "why": "devnull"} +{"session": "bfc350d7", "ts": "2026-10-08T06:54:33.003Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/ && cat qa-local-stand.md arm-web-repo.md arm-web-demo-prototype.md workspace-hooks-and-worktrees.md feedback-no-harness-bypass.md feedback-compare-with-mockup.md frontend-api-guide-state.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c054", "why": "not-a-block"} +{"session": "bfc350d7", "ts": "2026-10-08T07:14:14.008Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/fixtures/osv", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca/.wt/qa-guide-web/docs && cat > /Users/andrey.epifancev/.claude/jobs/0d5648e2/tmp/sec8.md <<'EOF'\n## 8. Сквозной сценарий в UI (arm-web)\n\nОсновной путь аналитика по макету: группа компаний → новая консолидация → файлы ЮЛ → проверка ОСВ и форм ББ/ОФР →\nсводная ОСВ. Номера в квадратных скобках — экраны макета `docs/mockup/screens/-*.png` (открывать по одному через\n`docs/mockup/INDEX.md`): если не ясно, «так ли должно выглядеть», сравнивайте с ними при окне 1600×1000. Права, коды\nошибок и граничные случаи, которых нет в UI, — в разделах 6–7 (там же в каждом разделе указано, какая часть доступна в UI).\n\n**Предусловия.** Стенд по разделу 4: ядро с `NATS_ENABLED=true`, tabular-parser на `:8001`, `make seed-dev` (ГК «Русский\nБисквит» с ГСК из РБ и Счастливочной), фронт — `make start`. Для 8.5 (PDF) — ещё шлюз и эмулятор ИС Сканирование:\n\n```bash\ndocker compose -f ../docs/dev/docker-compose.yml --profile services up -d --build --no-deps scan-gateway scan-emulator\n```\n\nПользователь — `ka_go@armka.local`. Начинайте со свежего стенда (раздел 10): тогда цифры совпадут с 7.2 без поправок.\nПодписи кнопок, колонок и сообщений ниже — дословно как в UI.\n\n### 8.1 Навигация и группы компаний [03]\n\n| № | Шаги | Ожидание |\n|---|---|---|\n| W1 | Войти `ka_go`, посмотреть боковое меню | логотип «АРМ КА», кнопка «+ Новая консолидация»; разделы «Отчётность» (Группы компаний, Монозаемщики), «Справочники» (ГК и организации, ЮЛ вне ГК), «Документы» (Единое хранилище), «Система» (Журнал действий, Пользователи и роли); кнопка «‹» сворачивает меню до иконок. Всё, кроме «Группы компаний» и «Пользователи и роли», — заглушки |\n| W2 | «Группы компаний» | крошки «Отчётность › Группы компаний», подзаголовок «4 групп компаний» (после `make seed-dev`: Русский Бисквит, Унагранде, Новороссийский прокатный завод, Агрофирма Межениновская); колонки «Группа компаний», «CRM ID», «Основной РФ», «Организаций» |\n| W3 | Поиск «бисквит»; затем фильтр в заголовке «Группа компаний» (сортировка, отметки, «OK») | остаётся «Русский Бисквит»; появляется «Сбросить фильтры»; «Настройка таблицы» скрывает и показывает колонки |\n| W4 | В строке «Русский Бисквит» — кнопка «→ Консолидации» | страница группы [34]: «Русский Бисквит», «20 организаций», блок «Консолидации 0 шт.»; блок «Организации» пуст, пока версия не выбрана; «← Назад» возвращает к списку |\n\n### 8.2 Новая консолидация [42]\n\n| № | Шаги | Ожидание |\n|---|---|---|\n| W5 | «+ Новая консолидация» в меню | модалка «Новая консолидация»: «Группа компаний», «ГСК» (неактивна до выбора ГК), «Год» (текущий и три предыдущих), «Квартал»; «Создать» неактивна |\n| W6 | ГК «Русский Бисквит», ГСК «ГСК Русский Бисквит», 2024, «3 квартал» | блок «ЮЛ консолидации»: все открытые ЮЛ ГК, РБ (3528052718) и Счастливочная (3528104476) отмечены и стоят первыми, счётчик «Выбрано: 2 / 20»; поиск «Поиск ЮЛ...» фильтрует список |\n| W7 | «Создать» | уведомление «Создана консолидация 2024, 3 квартал»; открывается страница группы с выбранной версией (`?version=` в адресе): строка «2024 · К3 · 1 · Инициация», «Дата создания» — сегодня по Москве, «ГСК» и «Дата верификации» — «—» (так в прототипе); в «Организации» — 2 строки, «Бух.баланс» и «ОФР» — «Отсутствует», «Ответственный: Dev: Аналитик ГО» |\n| W8 | Повторить W5–W7 с тем же периодом | уведомление «Консолидация ГК за этот период уже создана» (409 `CONSOLIDATION_EXISTS`), модалка остаётся открытой |\n| W9 | В модалке оставить отмеченным одно ЮЛ; снять все | одно ЮЛ — уведомление с текстом сервера (422 `PERIMETER_TOO_SMALL`, K2); ни одного — «Создать» неактивна |\n\n### 8.3 Организации, чек-лист, загрузка файлов [34], [39], [40]\n\n| № | Шаги | Ожидание |\n|---|---|---|\n| W10 | Кликнуть строку версии в «Консолидации» | строка подсвечена зелёным, адрес `?version=`; блок «Организации»: колонки «Наименование», «Документы», «ОФР», «Бух.баланс», «ОСВ», «ИНН», «Тип»; меню «⋮» строки версии: «Добавить версию» → «Будет позже», «История изменений» → заглушка журнала |\n| W11 | «Скачать чек-лист» | файл `checklist-Русский Бисквит-2024-Q3.csv`; содержимое — как в F17 (BOM, разделитель `;`, открывается в Excel) |\n| W12 | Клик по строке «ПЕКАРНЯ СЧАСТЛИВОЧНАЯ, ООО» (или кнопке «Документы») | справа панель «ПЕРВИЧНЫЕ ДОКУМЕНТЫ» [39]: имя ЮЛ, ИНН 3528104476, кнопка «Загрузить документы», «Документов нет»; крестик справа закрывает панель |\n| W13 | «Загрузить документы» → «Выбрать документы» → пять файлов `../fixtures/osv/schastl/2024-Q3/*` → «Загрузить» | модалка «Загрузка документов» [40]: зелёный блок «Принято: 5» с именами, кнопка «Загрузить» сменилась на «Готово». В панели файлы сначала «Обработка», через 5–15 с у каждого документ: «ОСВ 60 · XLS · <дата загрузки>», период «2024 К3», проверка «Период —», «Контрагент ✓», статус «Требуется проверка». Панель опрашивает сервер раз в 5 с, пока есть файлы в обработке |\n| W14 | Загрузить ещё раз любой из этих файлов | красный блок «Отклонено: 1» со строкой «<имя> — файл «<имя>» уже загружен для этого ЮЛ» (`DUPLICATE_FILE`), уведомление «Ни один файл не принят» |\n| W15 | Закрыть панель, обновить страницу (F5) | статус версии в таблице — «Обработка документов» (K8) |\n\n### 8.4 Просмотр ОСВ-документа\n\n| № | Шаги | Ожидание |\n|---|---|---|\n| W16 | В панели Счастливочной клик по «ОСВ 60» | экран «Просмотр ОСВ»: крошки «Отчётность › ПЕКАРНЯ СЧАСТЛИВОЧНАЯ, ООО › Проверка документа», подзаголовок «<имя файла> · XLS · <дата>», справа статус «Требуется проверка»; надпись «Суммы в рублях; строки ОСВ только для чтения»; дерево счёт → субсчёт → контрагент, колонки «Счёт / контрагент», «ИНН» и шесть сумм. Корневая строка `60`: «—», «27 923 002,09», «107 554 149,33», «99 188 100,32», «—», «19 556 953,08» (сальдо нач. Кт + обороты Кт − обороты Дт = сальдо кон. Кт) |\n| W17 | «✓ Подтвердить» | уведомление «Документ подтверждён», статус «Загружен», кнопка неактивна; в панели документ — «Загружен» |\n| W18 | Остальные файлы РБ и Счастливочной 2024 Q1–Q3 (22, список — 7.1) — через «Загрузить документы» каждому ЮЛ и подтверждение каждого ОСВ, либо скриптом из `arm-web`: `scripts/demo-prepare.sh ` (id — из адреса `?version=`) | скрипт печатает по строке на файл (пять файлов из W13 — «уже загружен», это нормально), ждёт распознавания (до 5 мин), подтверждает все ОСВ и в конце печатает `строк: 1385 документов: 22 несопоставленных: 1381 пробелов: 7`. После F5 статус версии — «Коррекция данных» (K9), в «Организации» в колонке «ОСВ» — счета ЮЛ |\n\n`demo-prepare.sh` берёт пользователя вторым аргументом (по умолчанию `ka_go@armka.local`), адрес ядра — из `API`\n(по умолчанию `localhost:8000/api/v1`), каталог фикстур — из `FIXTURES_DIR` (по умолчанию `../fixtures` рядом с `arm-web`).\n\n### 8.5 Проверка формы ББ/ОФР из PDF [41], [41-02]\n\nPDF из `fixtures/ocr` — отчётность других компаний (`aep_6.pdf` — ООО «Дорстроймонтаж», на 30.06.2025), поэтому ячейки\nББ/ОФР чек-листа версии 2024 Q3 они не заполняют; для проверки экрана подойдёт любое ЮЛ версии. Распознаются только PDF,\nдля которых в эмуляторе есть ответ (раздел 6.8).\n\n| № | Шаги | Ожидание |\n|---|---|---|\n| W19 | В панели Счастливочной загрузить `../fixtures/ocr/aep_6.pdf` | через 10–60 с два документа: «Бух. баланс · PDF» и «ОФР · PDF», статус «Требуется проверка», «Контрагент ✗» (в скане другая организация) |\n| W20 | Открыть «Бух. баланс» | экран «Проверка распознавания»: слева «ИСХОДНЫЙ ДОКУМЕНТ» — страницы PDF «Страница 1…8», масштаб «−», «100%», «+» и сброс; справа «РЕЗУЛЬТАТ РАСПОЗНАВАНИЯ»: шапка «ИНН —», «Дата отчёта —» (оранжевый фон — не распознано), «Единица измерения тыс. руб.»; заголовок «Бухгалтерский баланс»; колонки «На 30 июня 2025 г», «На 31 декабря 2024 г.», «На 31 декабря 2023 г.»; разделы «АКТИВ», «I. Внеоборотные активы» … «ПАССИВ»; итоги разделов жирным; суммы без копеек, пустые — «-»; строка 1600 «БАЛАНС» — 293 709 / 179 791 / 189 847. Ячейки с уверенностью распознавания < 100 % — оранжевые, подсказка «Уверенность: N%» |\n| W21 | «✎ Редактировать» → в строке 1600 первой колонки прибавить 100 → «✓ Сохранить» | уведомление «Изменения сохранены»; ячейка голубая, подсказка «Исходное: 293 709»; блок «Нарушения (N)» со строками «Колонка N: <соотношение> — ожидалось X, получено Y» (F3) |\n| W22 | «✓ Подтвердить данные» | красный блок в правой части с текстом сервера и списком нарушений (409 `CHECKS_NOT_PASSED`, F4); документ не подтверждён |\n| W23 | «Принять с комментарием» → «Принять нарушения»: пустой комментарий, затем «округление в форме клиента» → «Принять» | с пустым «Принять» неактивна; после — синий блок «Нарушения приняты» с датой и комментарием (F5) |\n| W24 | «✎ Редактировать», ввести в ячейку `abc` или `1.234` → «✓ Сохранить» | ячейка в красной рамке, подсказка «Неверный формат суммы»; уведомление «Исправьте суммы с неверным форматом», запрос не уходит. «Отменить» — правки сброшены |\n| W25 | Вернуть в 1600 исходную сумму, сохранить | нарушений нет («Проверки пройдены»), принятие сброшено правкой (F6) |\n| W26 | В шапке «Дата отчёта» ввести `2025-09-15` → «✓ Сохранить» → «✓ Подтвердить данные» | красный блок «<текст сервера>. Исправьте дату отчёта в шапке и сохраните.» (409 `PERIOD_REQUIRED`: дата не на конец квартала, F9). Дата вводится строго `ГГГГ-ММ-ДД` |\n| W27 | Дата `2025-06-30` → «✓ Сохранить» → «✓ Подтвердить данные» | уведомление «Документ подтверждён», статус «Загружен», «Редактировать» и «Подтвердить данные» неактивны |\n| W28 | Открыть «ОФР» того же файла [41-02] | заголовок «Отчёт о финансовых результатах», колонки периодов ОФР; остальное — как W20 |\n\nФормы из Excel (`fixtures/statements-xlsx`, раздел 6.9) открываются тем же экраном, но слева вместо скана — «Превью\nнедоступно для этого формата» и кнопка «Скачать исходник». Документы «Прочий документ» (расшифровки) в панели не\nоткрываются.\n\n### 8.6 Сводная ОСВ [30]\n\nПредусловие — W18: все 22 ОСВ загружены и подтверждены.\n\n| № | Шаги | Ожидание |\n|---|---|---|\n| W29 | В строке версии «К отчётности →» | экран «Русский Бисквит — 2024 К3», крошки «Отчётность › Русский Бисквит › 2024 · К3», справа «Статус отчёта» «Коррекция данных»; активна вкладка «ОСВ», «Шахматка», «Таблица корректировки», «Гудвилл», «КФО», «КФО Общий», «Подтверждение КФО клиента» неактивны (подсказка «Будет в следующих срезах») |\n| W30 | Сводка | под таблицей «Строк: 1385» (= `rows_total`, 7.2); жёлтый баннер «Требуется указать соответствия … Осталось: 1381.» (= `unmapped_total`); свёрнутый блок «Пробелы: 7 — по этим счетам нет ОСВ за часть кварталов, их строк в своде нет», раскрыть — 7 строк «<ЮЛ> — счёт <код> — нет кварталов: Q1, Q2»: РБ 01, 58.03, 66, 67, 76; Счастливочная 01, 02 |\n| W31 | Колонка «Период» | у всех строк «9 мес. 2024 г.»; суммы с запятой и пробелами, ноль — «—»; таблица по 500 строк на страницу |\n| W32 | Фильтр «Компания ГК» (воронка в заголовке) = «ПЕКАРНЯ СЧАСТЛИВОЧНАЯ, ООО» → «Применить»; фильтр «Контрагенты» = `бисквит` → «Применить» | «Строк: 6» — `60.01`, `60.02`, `62.01`, `62.02`, `76.06`, `76.АВ`, контрагент «РУССКИЙ БИСКВИТ, АО», ИНН 3528052718. Суммы — как в 7.2, на экране с запятой: `60.01` — «—», «4 454 151,93», «128 058 143,65», «125 950 450,77», «—», «2 346 459,05»; `60.02` — «—» везде, кроме оборотов «9 745 019,62» / «9 745 019,62»; `62.01` — «40 211 938,00», «—», «398 018 012,46», «330 517 504,00», «107 712 446,46», «—»; `62.02` — «—», «24 100 000,00», «9 900 000,00», «—», «—», «14 200 000,00». Иконка активного фильтра зелёная |\n| W33 | Сбросить фильтры; «Счёт» = `62` | «Строк: 387» на свежем стенде; `60.01` — 0 строк (фильтр принимает только корень счёта, ограничение прототипа) |\n| W34 | «Показать ЮЛ без ИНН» | только несопоставленные строки, «Строк» = «Осталось»; кнопка сменилась на «Показать все строки» |\n| W35 | «Экспорт» (с фильтрами и без) | файл `svod-osv_Русский Бисквит_2024-Q3_v1.xlsx` по текущим фильтрам; содержимое — по 7.3 |\n\nЕсли на стенде раньше делали ручное сопоставление контрагентов (K14), «Осталось» меньше 1381: алиасы переживают версии.\nОстальные цифры от этого не меняются.\n\n### 8.7 Что в UI не считать багом (прототип)\n\n- Нет «Взять в работу», смены периметра, ручных переходов статусов, сопоставления контрагентов, экрана чек-листа,\n комментариев, удаления файлов, «Добавить версию» — проверяйте через API (разделы 6.6, 6.9, 7).\n- В своде не показываются «документов: 22», колонки «Признак контрагента» и «Способ сопоставления» (признак есть только\n внутри фильтра «Контрагенты»: ГСК / ГК / Внешний); фильтр «Счёт» принимает только корень (`60`, не `60.01`).\n- Таблица консолидаций и статус версии обновляются после F5: после создания консолидации на странице той же ГК новой\n строки может не быть до обновления; панель «Организации» перечитывается при закрытии панели документов.\n- «Распознан» и «Требуется проверка» в панели документов выглядят одинаково — «Требуется проверка».\n- «ГСК» и «Дата верификации» в таблице консолидаций — всегда «—»; «Утверждён в РФ», «Верифицирован в ГО» — «—».\n- Панель документов показывает первые 100 файлов ЮЛ; загрузка — только из панели ЮЛ внутри версии.\n- Шапку формы (ИНН, дату, единицу) нельзя очистить — только заменить; время принятия нарушений — в часовом поясе\n браузера, остальные даты — по Москве.\n- Ссылка `?version=` с несуществующим id «выбирает» пустую версию; при 404 на своде может прийти до трёх сообщений;\n после истечения токена кроме «Сессия истекла» бывают лишние уведомления.\n- Предупреждения antd в консоли браузера («Static function can not consume context…», deprecated) — не влияют.\n- Уведомления в `/admin` не появляются — дефект arm-web#7 (раздел 6.1).\n\nБаг UI — issue в `RSHB/arm-web` (раздел 11): номер сценария W, скриншот, вкладка Network с запросом и ответом.\n\nEOF\npython3 - <<'EOF'\np='dev/qa-guide.md'; s=open(p).read()\nsec=open('/Users/andrey.epifancev/.claude/jobs/0d5648e2/tmp/sec8.md').read()\nfor a,b in [(\"## 11. Типичные проблемы\",\"## 12. Типичные проблемы\"),(\"## 10. Как заводить баги\",\"## 11. Как заводить баги\"),(\"## 9. Сброс стенда\",\"## 10. Сброс стенда\"),(\"## 8. Известные ограничения\",\"## 9. Известные ограничения\")]:\n assert s.count(a)==1; s=s.replace(a,b)\na=\"## 9. Известные ограничения\"\ns=s.replace(a, sec+a)\nopen(p,'w').write(s)\nEOF\ngrep -n \"^## \" dev/qa-guide.md", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca/.wt/qa-guide-web/docs && cat > /Users). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c055", "why": "outside-repo"} +{"session": "c15a6f0c", "ts": "2026-10-06T13:14:14.950Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cat /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/jetstream-check-later.md /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c056", "why": "not-a-block"} +{"session": "c222e9bb", "ts": "2026-10-06T09:44:42.939Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core/api", "branch": "HEAD", "tool": "Bash", "command": "echo \"=== enums with perm\"; grep -n -i \"permission\\|role\" src/models/enums.py | head -40; echo \"=== seed perms\"; ls seed; grep -rn -i \"permission\" seed/*.yaml seed/*.json seed/*.csv 2>/dev/null | head -5; echo \"=== seed files content heads\"; for f in seed/*; do echo \"--- $f\"; head -60 \"$f\"; done 2>/dev/null | head -250", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (echo \"=== enums with perm\"; grep -n -i \"permission\\|role\" src/models/enums.py | ). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c057", "why": "devnull"} +{"session": "c222e9bb", "ts": "2026-10-06T09:45:06.568Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core/api", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca/arm-core/web && ls src src/auth src/api; cat src/api/client.ts; cat src/auth/*.ts src/auth/*.tsx 2>/dev/null", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca/arm-core/web && ls src src/auth src/a). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c058", "why": "devnull"} +{"session": "c222e9bb", "ts": "2026-10-06T09:49:37.296Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core/web", "branch": "HEAD", "tool": "Write", "command": null, "file_path": "/private/tmp/claude-501/-Users-andrey-epifancev-projects-awp-ca/c222e9bb-473a-4a4b-be9b-5dc8cdb15b48/scratchpad/frontend-auth-guide.md", "hook": "guard-main(Edit/Write)", "message": "PreToolUse:Write hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: you are on main. Create an isolated worktree first (superpowers:using-git-worktrees), never edit main directly.", "expected": "allow", "id": "c059", "why": "outside-repo"} +{"session": "c222e9bb", "ts": "2026-10-06T09:52:47.722Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cat /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md; git -C /Users/andrey.epifancev/projects/awp-ca/docs worktree list; git -C /Users/andrey.epifancev/projects/awp-ca/docs remote -v | head -2; cat /Users/andrey.epifancev/projects/awp-ca/docs/.gitignore", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c060", "why": "not-a-block"} +{"session": "c3f1f3d1", "ts": "2026-10-02T21:55:49.335Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "echo \"=====openspec tree=====\" && find openspec -type f | sort && for f in $(find openspec -maxdepth 1 -type f) openspec/config.yaml openspec/AGENTS.md; do [ -f \"$f\" ] && { echo \"--- $f\"; cat \"$f\"; }; done 2>/dev/null; echo \"=====openspec specs/changes=====\" && find openspec/specs openspec/changes -type f 2>/dev/null | head -50; for f in $(find openspec/specs openspec/changes -type f 2>/dev/null | head -20); do echo \"--- $f\"; cat \"$f\"; done; echo \"=====setup=====\" && find setup -type f | sort && for f in $(find setup -type f | sort); do echo \"--- $f\"; cat \"$f\"; done", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (echo \"=====openspec tree=====\" && find openspec -type f | sort && for f in $(fin). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c061", "why": "devnull"} +{"session": "c3f1f3d1", "ts": "2026-10-02T21:56:21.933Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "cat \"$HOME/.claude/hooks/guard-main.sh\"; echo \"=====settings hooks=====\"; python3 -c \"import json;d=json.load(open('$HOME/.claude/settings.json'));print(json.dumps(d.get('hooks',{}),ensure_ascii=False,indent=1))\"; ls \"$HOME/.claude/hooks/\" \"$HOME/.claude/harness/\" 2>&1 | head -40", "file_path": null, "hook": "guard-main(Bash)", "message": " echo \"BLOCKED: shell write on $label ($(printf '%s' \"$cmd\" | head -c 80)). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly.\" >&2", "expected": "allow", "id": "c062", "why": "not-a-block"} +{"session": "c3f1f3d1", "ts": "2026-10-02T21:57:47.007Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cat \"$HOME/.claude/hooks/require-plan.sh\"; echo \"=====harness-check=====\"; cat \"$HOME/.claude/hooks/harness-check.sh\"; echo \"=====adopt-project=====\"; cat \"$HOME/.claude/harness/adopt-project.sh\"; echo \"=====project-template=====\"; find \"$HOME/.claude/harness/project-template\" -type f | head -30", "file_path": null, "hook": "require-plan", "message": "echo \"BLOCKED: editing $fpath without a plan. Run superpowers:brainstorming; for a bounded change create '$root/.bounded' first.\" >&2", "expected": "allow", "id": "c063", "why": "not-a-block"} +{"session": "c3f1f3d1", "ts": "2026-10-02T22:00:57.967Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca && WT=/Users/andrey.epifancev/projects/awp-ca/.wt/slice-0 && mkdir -p \"$WT\" && for r in docs arm-core tabular-parser scan-gateway; do git -C \"$r\" worktree add \"$WT/$r\" -b tmp/slice-0-skeleton 2>&1 | tail -1; done && ln -sfn ../../fixtures \"$WT/fixtures\" && ln -sfn ../../reference-old-arm \"$WT/reference-old-arm\" && ln -sfn ../../CLAUDE.md \"$WT/CLAUDE.md\" && ln -sfn ../../.claude \"$WT/.claude\" && ls -la \"$WT\" && for r in docs arm-core tabular-parser scan-gateway; do echo \"--- $r: $(git -C \"$WT/$r\" branch --show-current) @ $(git -C \"$WT/$r\" rev-parse --short HEAD)\"; done && git -C docs worktree list", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca && WT=/Users/andrey.epifancev/project). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c064", "why": "worktree-setup"} +{"session": "c48e5bdb", "ts": "2026-10-09T12:34:44.922Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory && cat workspace-hooks-and-worktrees.md qa-local-stand.md qa-guide-web-state.md arm-web-demo-prototype.md feedback-compare-with-mockup.md arm-web-repo.md feedback-no-harness-bypass.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c065", "why": "not-a-block"} +{"session": "c58a72aa", "ts": "2026-10-08T13:28:11.693Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/; cat arm-web-repo.md arm-web-demo-prototype.md workspace-hooks-and-worktrees.md feedback-compare-with-mockup.md feedback-no-harness-bypass.md; sed -n 1,200p qa-local-stand.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c066", "why": "not-a-block"} +{"session": "cedd7915", "ts": "2026-10-06T08:41:54.622Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory && cat slice-4-state.md dkb-package-state.md workspace-hooks-and-worktrees.md tech-debt-slice-1.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c067", "why": "not-a-block"} +{"session": "cedd7915", "ts": "2026-10-06T08:42:35.390Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "grep -rn \"Claude-Session\\|bounded\" ~/.claude/hooks/*.sh .claude/hooks/*.sh 2>/dev/null | head -20; ls ~/.claude/hooks .claude/hooks; git -C arm-core log -5 --format='%s%n%(trailers:key=Claude-Session)' | head -20; echo \"=== fixtures osv-dkb README\"; cat fixtures/osv-dkb/README.md | head -150", "file_path": null, "hook": "require-plan", "message": "/Users/andrey.epifancev/.claude/hooks/require-plan.sh:32:echo \"BLOCKED: editing $fpath without a plan. Run superpowers:brainstorming; for a bounded change create '$root/.bounded' first.\" >&2", "expected": "allow", "id": "c068", "why": "not-a-block"} +{"session": "cedd7915", "ts": "2026-10-06T18:39:59.376Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/tabular-parser", "branch": "HEAD", "tool": "Bash", "command": "openspec validate --specs --strict --no-interactive 2>&1 | tail -2; git add openspec && git commit -q -m \"$(cat <<'EOF'\nopenspec: архив change slice-2b-statement-parser (2026-10-06), спека statement-parsing, дельта tabular-recognition-service\n\nRefs #14\n\nCo-Authored-By: Claude Opus 5.5 \nClaude-Session: https://claude.ai/code/session_01EAKRDNzXtRrwmWz9z6PZu3\nEOF\n)\" && git push -q origin main 2>&1 | grep -v \"^remote:\"; git log --oneline -1 | cat; git status -sb | head -1", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (openspec validate --specs --strict --no-interactive 2>&1 | tail -2; git add open). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "block", "id": "c069", "why": "main-git"} +{"session": "cedd7915", "ts": "2026-10-06T18:40:06.664Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/tabular-parser", "branch": "HEAD", "tool": "Bash", "command": "git add openspec && git commit -q -m \"openspec: архив change slice-2b-statement-parser (2026-10-06), спека statement-parsing, дельта tabular-recognition-service\n\nRefs #14\n\nCo-Authored-By: Claude Opus 5.5 \nClaude-Session: https://claude.ai/code/session_01EAKRDNzXtRrwmWz9z6PZu3\" && git push -q origin main && git log --oneline -1 | cat", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (git add openspec && git commit -q -m \"openspec: архив change slice-2b-state). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directl", "expected": "block", "id": "c070", "why": "main-git"} +{"session": "cedd7915", "ts": "2026-10-06T18:40:09.776Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/tabular-parser", "branch": "HEAD", "tool": "Bash", "command": "grep -n -i \"archive\" ~/.claude/hooks/guard-main.sh", "file_path": null, "hook": "guard-main(Bash)", "message": "37: echo \"BLOCKED: shell write on $label ($(printf '%s' \"$cmd\" | head -c 80)). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly.\" >&2", "expected": "allow", "id": "c071", "why": "not-a-block"} +{"session": "cedd7915", "ts": "2026-10-06T18:40:32.919Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/tabular-parser", "branch": "HEAD", "tool": "Bash", "command": "git switch -q -c chore/archive-slice-2b-statement-parser && git add openspec && git commit -q -m \"openspec: архив change slice-2b-statement-parser (2026-10-06), спека statement-parsing, дельта tabular-recognition-service\n\nRefs #14\n\nCo-Authored-By: Claude Opus 5.5 \nClaude-Session: https://claude.ai/code/session_01EAKRDNzXtRrwmWz9z6PZu3\" && git push -q -u origin chore/archive-slice-2b-statement-parser && git log --oneline -1 | cat", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (git switch -q -c chore/archive-slice-2b-statement-parser && git add openspec && ). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c072", "why": "switch-then-commit"} +{"session": "cedd7915", "ts": "2026-10-06T19:00:15.482Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/tabular-parser/api", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca && W=$PWD; mkdir -p .wt/f20 && git -C tabular-parser worktree add -q -b fix/20-continuation-tolerance $W/.wt/f20/tabular-parser origin/main && git -C fixtures worktree add -q -b fix/20-golden $W/.wt/f20/fixtures origin/main && for l in docs .claude CLAUDE.md; do ln -s ../../$l $W/.wt/f20/$l; done; touch $W/.wt/f20/tabular-parser/.bounded; make -C $W/.wt/f20/tabular-parser venv 2>&1 | tail -1; T=$W/.wt/f20/tabular-parser/api; wc -l $T/src/parser/statements/*.py $T/src/parser/book.py; cat $T/rules/statement_checks.yaml | head -20", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca && W=$PWD; mkdir -p .wt/f20 && git -C). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c073", "why": "worktree-setup"} +{"session": "d31ab7be", "ts": "2026-10-07T12:01:20.115Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca; M=~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory; cat $M/slice-3b-state.md $M/slice-3a-state.md $M/core-33-golden-tests.md $M/slice-4-state.md $M/workspace-hooks-and-worktrees.md; ls; ls .wt 2>/dev/null", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c074", "why": "not-a-block"} +{"session": "d60ad490", "ts": "2026-10-07T10:13:06.399Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "grep -rn -iE \"version_table_schema|search_path|schema *=|create schema|\\\"armka\\\"|'armka'|_SCHEMA\" --include='*.py' --include='*.ini' --include='*.toml' --include='*.yml' --include='*.yaml' --include='.env*' . 2>/dev/null | grep -v node_modules | head -40", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (grep -rn -iE \"version_table_schema|search_path|schema *=|create schema|\\\"armka\\\"). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c075", "why": "devnull"} +{"session": "d60ad490", "ts": "2026-10-07T10:13:36.529Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca/arm-core; ls api/alembic/versions; sed -n 1,40p api/src/db/db.py; sed -n 1,20p api/src/models/base.py; grep -rn \"DATABASE\" raisa_config.yml api/src/core/*.py 2>/dev/null | head", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca/arm-core; ls api/alembic/versions; se). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c076", "why": "devnull"} +{"session": "d60ad490", "ts": "2026-10-07T10:13:38.071Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca; git -C .wt/slice-3b/arm-core log --oneline -1 2>/dev/null || ls .wt/slice-3b; ls .wt/slice-3b/arm-core/api/alembic/versions 2>/dev/null || find .wt/slice-3b -path '*alembic/versions' -maxdepth 5", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca; git -C .wt/slice-3b/arm-core log --o). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c077", "why": "devnull"} +{"session": "d60ad490", "ts": "2026-10-07T10:14:06.972Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "cat >> /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/slice-3b-state.md <<'EOF'\n\n**После слияния 3b:** взять arm-core #38 (схема БД `dkb` зашита в models/base.py, db/db.py, alembic/env.py, миграциях 0001–0004 и conftest → сделать настройкой). Bounded. Отложено 07.10.2026, чтобы не конфликтовать с SDD-агентом 3b.\nEOF", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cat >> /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c078", "why": "outside-repo"} +{"session": "d60ad490", "ts": "2026-10-07T10:14:16.116Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Edit", "command": null, "file_path": "/Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/slice-3b-state.md", "hook": "guard-main(Edit/Write)", "message": "PreToolUse:Edit hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: you are on main. Create an isolated worktree first (superpowers:using-git-worktrees), never edit main directly.", "expected": "allow", "id": "c079", "why": "outside-repo"} +{"session": "d60ad490", "ts": "2026-10-07T14:07:41.379Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core/web", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca/arm-core && tea issues create --login home --repo RSHB/arm-core --title \"Зависимости под контур: PyJWT[crypto]==2.8.0, cryptography==43.0.3 (и правка JwksVerifier)\" --description \"$(cat <<'EOF'\n## Откуда\n\nЗапрос разработчика, который переносит сервисы в контур РАИСА: в arm-core должны стоять `pyjwt[crypto]==2.8.0` и `cryptography==43.0.3`. Сейчас `api/requirements.txt`: `PyJWT[crypto]==2.15.1`, `cryptography==50.0.2`; `api/requirements-dev.txt`: `PyJWT[crypto]>=2.9`.\n\n## Что проверено (07.10.2026, отдельный venv)\n\n- Вся `requirements.txt` с этими версиями разрешается без конфликтов (`uv pip compile`, кроме `raisa_*`).\n- Dev-аутентификация (`DevHs256Verifier`, `POST /api/v1/dev/token`) на 2.8.0 работает: encode/decode HS256, `require: [exp, sub]`, чужая подпись → `InvalidSignatureError`.\n- Параметры `PyJWKClient(cache_keys, cache_jwk_set, lifespan, timeout)` на 2.8.0 принимаются.\n- **Ломается `JwksVerifier`**: `src/core/auth.py`, `_signing_key` отдаёт в `jwt.decode` сам объект `PyJWK` — это поддерживается только с PyJWT 2.10. На 2.8.0 `TypeError: Expecting a PEM-formatted key` → перехватывается → 401 «недействительный токен» на **любой** токен из JWKS. Статический `AUTH_PUBLIC_KEY` работает.\n\n## Что сделать\n\n1. `api/requirements.txt`: `PyJWT[crypto]==2.8.0`, `cryptography==43.0.3`; `api/requirements-dev.txt`: снять `>=2.9` (согласовать с 2.8.0).\n2. `JwksVerifier._signing_key`: в `jwt.decode` передавать `signing_key.key`, а допустимый алгоритм выводить из типа ключа самим (RSA → RS256, EC P-256 → ES256 — как уже сделано для `AUTH_PUBLIC_KEY` в `_load_static_key`), чтобы сохранить поведение «ключ одного типа не принимает токен другого алгоритма» (Срез 0, решение #72).\n3. Тесты `api/tests/test_auth_core.py` (блок JwksVerifier, `_FakeJwkClient`) — зелёные на 2.8.0; добавить случай RSA-ключ + ES256-токен → 401 (а не 500). Проверить, что `PyJWKClientConnectionError` есть в 2.8.0 (импортируется в тестах).\n4. `make check-full` зелёный на новых версиях.\n\n## Контекст\n\nВ контуре пока разворачиваем **без платформенной аутентификации**, только dev-заглушка (`DEBUG=true`, `AUTH_DEV_ISSUER=true`, `AUTH_DEV_SECRET`) — она с 2.8.0 работает и без этой задачи. JWKS-путь понадобится, когда подключим SSO РАИСА (OQ-54: параметры Keycloak, `iss`/`aud`, claim с email; не выяснено, используется ли на платформе `raisa_fastapi_protected_api` — в коде он импортируется, но не подключён). Возможно, версии 2.8.0/43.0.3 — требование самого `raisa_fastapi_protected_api`; уточнить у разработчика.\n\nBounded-задача. Если `requirements.txt` трогает Срез 3b (#36) — начинать после его слияния.\nEOF\n)\" | tail -2", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca/arm-core && tea issues create --login). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c080", "why": "heredoc"} +{"session": "d60ad490", "ts": "2026-10-07T14:07:55.976Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core/web", "branch": "HEAD", "tool": "Bash", "command": "tea issues create --login home --repo RSHB/arm-core --title \"Зависимости под контур: PyJWT[crypto]==2.8.0, cryptography==43.0.3 (и правка JwksVerifier)\" --description \"$(cat <<'EOF'\n## Откуда\n\nЗапрос разработчика, который переносит сервисы в контур РАИСА: в arm-core должны стоять `pyjwt[crypto]==2.8.0` и `cryptography==43.0.3`. Сейчас `api/requirements.txt`: `PyJWT[crypto]==2.15.1`, `cryptography==50.0.2`; `api/requirements-dev.txt`: `PyJWT[crypto]>=2.9`.\n\n## Что проверено (07.10.2026, отдельный venv)\n\n- Вся `requirements.txt` с этими версиями разрешается без конфликтов (`uv pip compile`, кроме `raisa_*`).\n- Dev-аутентификация (`DevHs256Verifier`, `POST /api/v1/dev/token`) на 2.8.0 работает: encode/decode HS256, `require: [exp, sub]`, чужая подпись → `InvalidSignatureError`.\n- Параметры `PyJWKClient(cache_keys, cache_jwk_set, lifespan, timeout)` на 2.8.0 принимаются.\n- **Ломается `JwksVerifier`**: `src/core/auth.py`, `_signing_key` отдаёт в `jwt.decode` сам объект `PyJWK` — это поддерживается только с PyJWT 2.10. На 2.8.0 `TypeError: Expecting a PEM-formatted key` → перехватывается → 401 «недействительный токен» на **любой** токен из JWKS. Статический `AUTH_PUBLIC_KEY` работает.\n\n## Что сделать\n\n1. `api/requirements.txt`: `PyJWT[crypto]==2.8.0`, `cryptography==43.0.3`; `api/requirements-dev.txt`: снять `>=2.9` (согласовать с 2.8.0).\n2. `JwksVerifier._signing_key`: в `jwt.decode` передавать `signing_key.key`, а допустимый алгоритм выводить из типа ключа самим (RSA → RS256, EC P-256 → ES256 — как уже сделано для `AUTH_PUBLIC_KEY` в `_load_static_key`), чтобы сохранить поведение «ключ одного типа не принимает токен другого алгоритма» (Срез 0, решение #72).\n3. Тесты `api/tests/test_auth_core.py` (блок JwksVerifier, `_FakeJwkClient`) — зелёные на 2.8.0; добавить случай RSA-ключ + ES256-токен → 401 (а не 500). Проверить, что `PyJWKClientConnectionError` есть в 2.8.0 (импортируется в тестах).\n4. `make check-full` зелёный на новых версиях.\n\n## Контекст\n\nВ контуре пока разворачиваем **без платформенной аутентификации**, только dev-заглушка (`DEBUG=true`, `AUTH_DEV_ISSUER=true`, `AUTH_DEV_SECRET`) — она с 2.8.0 работает и без этой задачи. JWKS-путь понадобится, когда подключим SSO РАИСА (OQ-54: параметры Keycloak, `iss`/`aud`, claim с email; не выяснено, используется ли на платформе `raisa_fastapi_protected_api` — в коде он импортируется, но не подключён). Возможно, версии 2.8.0/43.0.3 — требование самого `raisa_fastapi_protected_api`; уточнить у разработчика.\n\nBounded-задача. Если `requirements.txt` трогает Срез 3b (#36) — начинать после его слияния.\nEOF\n)\"", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (tea issues create --login home --repo RSHB/arm-core --title \"Зависимос�). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly.", "expected": "allow", "id": "c081", "why": "heredoc"} +{"session": "d60ad490", "ts": "2026-10-07T14:08:08.999Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core/web", "branch": "HEAD", "tool": "Write", "command": null, "file_path": "/private/tmp/claude-501/-Users-andrey-epifancev-projects-awp-ca/d60ad490-3778-4c02-b9ba-0689e706df8f/scratchpad/issue-jwt.md", "hook": "guard-main(Edit/Write)", "message": "PreToolUse:Write hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: you are on main. Create an isolated worktree first (superpowers:using-git-worktrees), never edit main directly.", "expected": "allow", "id": "c082", "why": "outside-repo"} +{"session": "d60ad490", "ts": "2026-10-07T14:15:11.785Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core/api", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca && sed -i '' 's/делать \\*\\*после завершения агента Среза 3b\\*\\* (arm-core #36)/делать **после завершения агента arm-core #42** (front-api-gaps, worktree .wt\\/front-gaps; Срез 3b уже слит и заархивирован 07.10.2026)/' /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/contour-transfer-queue.md && grep -n \"после завершения\" /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/contour-transfer-queue.md", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca && sed -i '' 's/делать \\*\\*по). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly.", "expected": "allow", "id": "c083", "why": "outside-repo"} +{"session": "d60ad490", "ts": "2026-10-07T14:15:16.765Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core/api", "branch": "HEAD", "tool": "Edit", "command": null, "file_path": "/Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/contour-transfer-queue.md", "hook": "guard-main(Edit/Write)", "message": "PreToolUse:Edit hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: you are on main. Create an isolated worktree first (superpowers:using-git-worktrees), never edit main directly.", "expected": "allow", "id": "c084", "why": "outside-repo"} +{"session": "d60ad490", "ts": "2026-10-07T14:20:39.845Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core/api/src/schemas", "branch": "HEAD", "tool": "Edit", "command": null, "file_path": "/Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/contour-transfer-queue.md", "hook": "guard-main(Edit/Write)", "message": "PreToolUse:Edit hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: you are on main. Create an isolated worktree first (superpowers:using-git-worktrees), never edit main directly.", "expected": "allow", "id": "c085", "why": "outside-repo"} +{"session": "d60ad490", "ts": "2026-10-07T14:23:25.033Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cat /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md; ls /Users/andrey.epifancev/projects/awp-ca/.wt", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c086", "why": "not-a-block"} +{"session": "d60ad490", "ts": "2026-10-07T14:45:48.564Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/.wt/verify-main/arm-core/api", "branch": "HEAD", "tool": "Bash", "command": "P=$(ls -d .venv venv 2>/dev/null | head -1)/bin/python; echo $P; $P -c \"import jwt,cryptography;print(jwt.__version__,cryptography.__version__)\"; $P -m pytest -q -rs 2>&1 | grep SKIPPED | sed -E 's/.*SKIPPED \\[[0-9]+\\] //' | cut -c1-150 | sort | uniq -c | head", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on detached HEAD (P=$(ls -d .venv venv 2>/dev/null | head -1)/bin/python; echo $P; $P -c \"import j). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land o", "expected": "allow", "id": "c087", "why": "devnull"} +{"session": "dc1c37f7", "ts": "2026-10-09T18:32:56.639Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cat ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md; echo \"#### docs status\"; git -C docs status --short | head; git -C docs branch --show-current; ls .wt 2>/dev/null; echo \"#### openspec in docs\"; ls docs/openspec docs/openspec/changes 2>/dev/null | head -20", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c088", "why": "not-a-block"} +{"session": "e08f3f0b", "ts": "2026-10-09T06:07:31.177Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-core", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca && grep -rln \"1110\" tabular-parser/src tabular-parser/rules arm-core/api/rules scan-gateway/src docs/context 2>/dev/null | head; grep -n \"^## \\|^### \" docs/context/05-domain-rules.md | sed -n 1,200p | grep -n \"12\\|7\\.\\|11\" | head -30", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca && grep -rln \"1110\" tabular-parser/sr). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c089", "why": "devnull"} +{"session": "e08f3f0b", "ts": "2026-10-09T06:13:46.634Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/arm-web", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/ && cat workspace-hooks-and-worktrees.md feedback-no-harness-bypass.md; ls /Users/andrey.epifancev/projects/awp-ca/.wt/web-proto /Users/andrey.epifancev/projects/awp-ca/.wt/logger-diagnose", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c090", "why": "not-a-block"} +{"session": "e3c5dd5e", "ts": "2026-10-08T15:22:43.958Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/docs/mockup/dkb_24.09.26", "branch": "HEAD", "tool": "Bash", "command": "python3 - <<'EOF'\ns=open(\"dkb_24.09.26.html\",encoding=\"utf-8\").read()\ni=3350471\nseg=s[i-40000:i]\nimport re\nfor pat in [r'.{0,200}\\bN\\s*=\\s*(?:\\(|[A-Za-z_$]+=>|useCallback|function).{0,300}', r'.{0,300}Показать ЮЛ без ИНН', r'.{0,200}missingInn.{0,200}', r'.{0,200}counterpartyInn\\|\\|.{0,200}']:\n for m in re.finditer(pat,seg): print(\">>\",m.group(0)[:600]); print()\nEOF", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (python3 - <<'EOF'", "expected": "allow", "id": "c091", "why": "heredoc"} +{"session": "e3c5dd5e", "ts": "2026-10-08T15:22:52.982Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/docs/mockup/dkb_24.09.26", "branch": "HEAD", "tool": "Write", "command": null, "file_path": "/Users/andrey.epifancev/.claude/jobs/8105dccb/tmp/mock_n.py", "hook": "guard-main(Edit/Write)", "message": "PreToolUse:Write hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: you are on main. Create an isolated worktree first (superpowers:using-git-worktrees), never edit main directly.", "expected": "allow", "id": "c092", "why": "outside-repo"} +{"session": "e3c5dd5e", "ts": "2026-10-08T15:22:58.746Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/docs/mockup/dkb_24.09.26", "branch": "HEAD", "tool": "Bash", "command": "grep -o -E '.{0,150}Показать ЮЛ без ИНН' /Users/andrey.epifancev/projects/awp-ca/docs/mockup/dkb_24.09.26/dkb_24.09.26.html | head -3; grep -o -E '.{0,10}[^A-Za-z_$]N=[A-Za-z_$]+=>.{0,250}' /Users/andrey.epifancev/projects/awp-ca/docs/mockup/dkb_24.09.26/dkb_24.09.26.html | grep -i inn | head", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (grep -o -E '.{0,150}Показать ЮЛ без ИНН' /Users/andrey.epifancev). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly.", "expected": "allow", "id": "c093", "why": "read-only"} +{"session": "e3c5dd5e", "ts": "2026-10-08T15:26:58.431Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/docs/mockup/dkb_24.09.26", "branch": "HEAD", "tool": "Write", "command": null, "file_path": "/Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/osv-inn-mapping-rework.md", "hook": "guard-main(Edit/Write)", "message": "PreToolUse:Write hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: you are on main. Create an isolated worktree first (superpowers:using-git-worktrees), never edit main directly.", "expected": "allow", "id": "c094", "why": "outside-repo"} +{"session": "e3c5dd5e", "ts": "2026-10-08T15:29:21.316Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/docs/mockup/dkb_24.09.26", "branch": "HEAD", "tool": "Bash", "command": "cat /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/feedback-no-harness-bypass.md /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/le-group-name-state.md", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c095", "why": "not-a-block"} +{"session": "e44c4b65", "ts": "2026-10-02T21:55:49.335Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "echo \"=====openspec tree=====\" && find openspec -type f | sort && for f in $(find openspec -maxdepth 1 -type f) openspec/config.yaml openspec/AGENTS.md; do [ -f \"$f\" ] && { echo \"--- $f\"; cat \"$f\"; }; done 2>/dev/null; echo \"=====openspec specs/changes=====\" && find openspec/specs openspec/changes -type f 2>/dev/null | head -50; for f in $(find openspec/specs openspec/changes -type f 2>/dev/null | head -20); do echo \"--- $f\"; cat \"$f\"; done; echo \"=====setup=====\" && find setup -type f | sort && for f in $(find setup -type f | sort); do echo \"--- $f\"; cat \"$f\"; done", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (echo \"=====openspec tree=====\" && find openspec -type f | sort && for f in $(fin). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c096", "why": "devnull"} +{"session": "e44c4b65", "ts": "2026-10-02T21:56:21.933Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cat \"$HOME/.claude/hooks/guard-main.sh\"; echo \"=====settings hooks=====\"; python3 -c \"import json;d=json.load(open('$HOME/.claude/settings.json'));print(json.dumps(d.get('hooks',{}),ensure_ascii=False,indent=1))\"; ls \"$HOME/.claude/hooks/\" \"$HOME/.claude/harness/\" 2>&1 | head -40", "file_path": null, "hook": "guard-main(Bash)", "message": " echo \"BLOCKED: shell write on $label ($(printf '%s' \"$cmd\" | head -c 80)). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly.\" >&2", "expected": "allow", "id": "c097", "why": "not-a-block"} +{"session": "e44c4b65", "ts": "2026-10-02T21:57:47.007Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cat \"$HOME/.claude/hooks/require-plan.sh\"; echo \"=====harness-check=====\"; cat \"$HOME/.claude/hooks/harness-check.sh\"; echo \"=====adopt-project=====\"; cat \"$HOME/.claude/harness/adopt-project.sh\"; echo \"=====project-template=====\"; find \"$HOME/.claude/harness/project-template\" -type f | head -30", "file_path": null, "hook": "require-plan", "message": "echo \"BLOCKED: editing $fpath without a plan. Run superpowers:brainstorming; for a bounded change create '$root/.bounded' first.\" >&2", "expected": "allow", "id": "c098", "why": "not-a-block"} +{"session": "e44c4b65", "ts": "2026-10-02T22:00:57.967Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca && WT=/Users/andrey.epifancev/projects/awp-ca/.wt/slice-0 && mkdir -p \"$WT\" && for r in docs arm-core tabular-parser scan-gateway; do git -C \"$r\" worktree add \"$WT/$r\" -b tmp/slice-0-skeleton 2>&1 | tail -1; done && ln -sfn ../../fixtures \"$WT/fixtures\" && ln -sfn ../../reference-old-arm \"$WT/reference-old-arm\" && ln -sfn ../../CLAUDE.md \"$WT/CLAUDE.md\" && ln -sfn ../../.claude \"$WT/.claude\" && ls -la \"$WT\" && for r in docs arm-core tabular-parser scan-gateway; do echo \"--- $r: $(git -C \"$WT/$r\" branch --show-current) @ $(git -C \"$WT/$r\" rev-parse --short HEAD)\"; done && git -C docs worktree list", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca && WT=/Users/andrey.epifancev/project). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c099", "why": "worktree-setup"} +{"session": "e44c4b65", "ts": "2026-10-04T09:29:49.924Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/fixtures/ocr", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca && cat fixtures/.gitignore; git -C fixtures log --oneline -3; git -C fixtures worktree add .wt/fixtures-ocr -b feat/1-ocr-fixtures-aep3-aep6 2>&1 | tail -1 && for f in aep_3.json aep_3.pdf aep_3_ofr.json aep_6.json; do cp \"fixtures/ocr/$f\" \".wt/fixtures-ocr/ocr/$f\" && cmp \"fixtures/ocr/$f\" \".wt/fixtures-ocr/ocr/$f\" && echo \"ok $f\"; done; python3 -c \"\nimport json\nfor f in ['aep_3.json','aep_3_ofr.json','aep_6.json']:\n d=json.load(open('.wt/fixtures-ocr/ocr/'+f)); print(f,'valid json, error=',d['error'])\"", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca && cat fixtures/.gitignore; git -C fi). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c100", "why": "worktree-setup"} +{"session": "e44c4b65", "ts": "2026-10-04T09:31:33.866Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/reference-old-arm/api/src", "branch": "HEAD", "tool": "Write", "command": null, "file_path": "/Users/andrey.epifancev/projects/awp-ca/.wt/slice-0/scan-gateway/docs/superpowers/plans/2026-10-04-multi-file-response.md", "hook": "guard-main(Edit/Write)", "message": "PreToolUse:Write hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: you are on main. Create an isolated worktree first (superpowers:using-git-worktrees), never edit main directly.", "expected": "allow", "id": "c101", "why": "worktree"} +{"session": "e44c4b65", "ts": "2026-10-04T09:31:36.996Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/reference-old-arm/api/src", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca/.wt/slice-0/scan-gateway && git add docs/superpowers/plans/2026-10-04-multi-file-response.md && git commit -q -F - <<'EOF'\nchore: план доработки scan-gateway по реальным ответам ИС (несколько json на задание, служебный каталог)\n\nРешения человека 04.10.2026 (OQ-76/77/78/79) и факты о формате ответа по новым фикстурам aep_3 и aep_6:\ndata_ocr — объект, успех = error.code == \"\", таблиц в одном json несколько.\n\nCo-Authored-By: Claude Sonnet 5.5 \nClaude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2\nEOF\ngit log --oneline -1; git status --short | wc -l | tr -d ' '", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca/.wt/slice-0/scan-gateway && git add d). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c102", "why": "worktree"} +{"session": "e44c4b65", "ts": "2026-10-04T15:43:22.935Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/docs", "branch": "HEAD", "tool": "Bash", "command": "git add -A openspec && git diff --cached --name-only | grep -v '^openspec/' | wc -l | tr -d ' ' && git commit -q -F - <<'EOF'\nchore(openspec): архив change slice-0-skeleton (docs), спеки recognition-contract, dev-environment, quality-checks\n\nЗадачи 3.x, 5.1, 5.2 отмечены выполненными: исполнены в дочерних change'ах arm-core, tabular-parser, scan-gateway (PR #3 каждого).\n\nCo-Authored-By: Claude Sonnet 5.5 \nClaude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2\nEOF\ngit push -q origin main 2>&1 | tail -1; git status -sb | head -1; git log --oneline -1 | cut -c1-90", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (git add -A openspec && git diff --cached --name-only | grep -v '^openspec/' | wc). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "block", "id": "c103", "why": "main-git"} +{"session": "e44c4b65", "ts": "2026-10-04T15:44:07.480Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/docs", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca && for r in docs arm-core tabular-parser scan-gateway; do echo \"$r: dirty=$(git -C $r status --porcelain | wc -l | tr -d ' ') branch=$(git -C $r branch --show-current)\"; done; mkdir -p .wt/archive && for r in docs arm-core tabular-parser scan-gateway; do git -C $r worktree add ../.wt/archive/$r -b chore/archive-slice-0-skeleton 2>&1 | tail -1; done", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca && for r in docs arm-core tabular-par). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c104", "why": "worktree-setup"} +{"session": "e44c4b65", "ts": "2026-10-05T07:18:56.578Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/docs", "branch": "HEAD", "tool": "Bash", "command": "awk '/^## §3\\b|^## §3\\. /,0' context/06-test-data.md | head -120; echo '-----'; ls -la ../fixtures/osv ../fixtures/statements-xlsx ../fixtures/golden 2>/dev/null | head -60", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (awk '/^## §3\\b|^## §3\\. /,0' context/06-test-data.md | head -120; echo '-----'). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main dire", "expected": "allow", "id": "c105", "why": "devnull"} +{"session": "e44c4b65", "ts": "2026-10-06T08:16:10.000Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca/tabular-parser/api", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca && git -C fixtures fetch -q origin && git -C fixtures worktree add -q /Users/andrey.epifancev/projects/awp-ca/.wt/dkb/fixtures -b wip/dkb origin/main && git -C tabular-parser fetch -q origin && git -C tabular-parser worktree add -q /Users/andrey.epifancev/projects/awp-ca/.wt/dkb/tabular-parser -b wip/dkb-golden origin/main && cd /Users/andrey.epifancev/projects/awp-ca/.wt/dkb/fixtures && ID=$(tea issues create --login home --repo RSHB/arm-ka-fixtures --title \"Пакет ДКБ 06.10.2026: ОСВ шести новых организаций, ББ/ОФР в Excel, план счетов 1С\" --description \"Разложить присланный пакет «Документы по дкб в формате xls» + ББ/ОФР/план счетов: osv//<период>/ для АО Цимлянский, ООО НКЗ, ООО Новоросметалл, ООО НПЗ, ООО Приманыческий, АО Агрокомплекс им. Ткачева, Счастливочная 1 кв 2025 (6 недостающих счетов); отчёты «Анализ счета» отдельно; statements-xlsx/ (Унагранде Ко, Унагранде Групп, НПЗ, Агрофирма Межениновская, шаблоны Минфина); directories/ план счетов 1С (488 строк, OQ-113); golden-снимки парсера; README. Дубли РБ 3 кв 2025 и Счастливочной 01/60 1 кв 2025 не добавлять.\" 2>&1 | grep -oE \"issues/[0-9]+\" | grep -oE \"[0-9]+\") && git branch -m wip/dkb feat/$ID-dkb-package && echo \"fixtures issue $ID; branch $(git branch --show-current)\"; ls /Users/andrey.epifancev/projects/awp-ca/.wt/dkb/", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd /Users/andrey.epifancev/projects/awp-ca && git -C fixtures fetch -q origin &&). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c106", "why": "worktree-setup"} +{"session": "f44aee1b", "ts": "2026-10-07T19:04:37.820Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cat /Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md | head -40; for r in arm-core arm-ka-docs arm-tabular-parser arm-scan-gateway arm-ka-fixtures; do echo \"=== $r\"; tea issues list --login home --repo RSHB/$r --state open --output simple 2>&1 | head -30; done", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c107", "why": "not-a-block"} +{"session": "f66d8575", "ts": "2026-10-09T09:27:13.268Z", "cwd": "/Users/andrey.epifancev/projects/awp-ca", "branch": "HEAD", "tool": "Bash", "command": "cd /Users/andrey.epifancev/projects/awp-ca; cat ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/le-group-name-state.md ~/.claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory/workspace-hooks-and-worktrees.md; git -C arm-core status -sb | head; git -C arm-core worktree list; grep -rn \"alias\" arm-core/src --include=*.py -il | head -30", "file_path": null, "hook": "guard-main(Bash)", "message": "получают `BLOCKED: shell write on main` от guard-main и останавливаются. Для чтения файлов main использовать абсолютные", "expected": "allow", "id": "c108", "why": "not-a-block"} +{"session": "259e4a45", "ts": "2026-10-11T17:05:38.793Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness", "branch": "main", "tool": "Bash", "command": "cat README.md ARCHITECTURE.md CHANGELOG.md docs/HARNESS.md .gitignore; echo ===HOOKS; cat hooks/settings.hooks.json hooks/guard-main.sh hooks/require-plan.sh hooks/harness-check.sh; echo ===SCRIPTS; cat install.sh adopt-project.sh new-project.sh; echo ===TEMPLATE; cat project-template/CLAUDE.md project-template/CLAUDE.harness-section.md project-template/ARCHITECTURE.md project-template/gitignore; echo ===DOCTOR; cat skills/harness-doctor/SKILL.md skills/harness-doctor/scripts/doctor.sh; echo ===OPENSPEC; cat openspec/config.yaml; echo ===CAVEMAN; cat caveman/config.json; echo ===GLOBAL; head -5 global/CLAUDE.md; diff global/CLAUDE.md ~/.claude/CLAUDE.md && echo SAME_AS_HOME", "file_path": null, "hook": "other", "message": "# Ожидается BLOCKED и предложение создать worktree.", "expected": "allow", "id": "c109", "why": "not-a-block"} +{"session": "259e4a45", "ts": "2026-10-11T17:05:41.652Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness", "branch": "main", "tool": "Bash", "command": "cat hooks/settings.hooks.json hooks/guard-main.sh hooks/require-plan.sh hooks/harness-check.sh", "file_path": null, "hook": "guard-main(Bash)", "message": " echo \"BLOCKED: shell write on $label ($(printf '%s' \"$cmd\" | head -c 80)). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly.\" >&2", "expected": "allow", "id": "c110", "why": "not-a-block"} +{"session": "259e4a45", "ts": "2026-10-11T17:05:46.774Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness", "branch": "main", "tool": "Bash", "command": "cat install.sh adopt-project.sh new-project.sh; echo ===TEMPLATE; cat project-template/CLAUDE.harness-section.md project-template/gitignore; echo ===DOCTOR-SKILL; cat skills/harness-doctor/SKILL.md; echo ===DOCTOR-SH-HEAD; head -40 skills/harness-doctor/scripts/doctor.sh; echo ===OPENSPEC; cat openspec/config.yaml; echo ===CAVEMAN; cat caveman/config.json; echo ===CLAUDE-DIR; ls .claude; cat .claude/settings*.json 2>/dev/null", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cat install.sh adopt-project.sh new-project.sh; echo ===TEMPLATE; cat project-te). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c111", "why": "devnull"} +{"session": "259e4a45", "ts": "2026-10-11T17:06:14.575Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness/.wt/retro", "branch": "main", "tool": "Bash", "command": "cat docs/retro/2026-10-11.md", "file_path": null, "hook": "guard-main(Bash)", "message": "контроллер и всех его implementer-ов в состояние `BLOCKED: shell write on main`, и они останавливаются. Правило «не cd, пока", "expected": "allow", "id": "c112", "why": "not-a-block"} +{"session": "259e4a45", "ts": "2026-10-11T17:14:09.509Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness/.wt/retro", "branch": "retro/2026-10-11", "tool": "Bash", "command": "cat > CLAUDE.md <<'EOF'\n# claude-harness\n\n## Что это\nПакет обвязки для разработки с Claude Code: глобальный `CLAUDE.md` с процессом, три блокирующих хука, скрипты подключения проекта и skill диагностики. Для одного разработчика, который ведёт фичи через superpowers, хранит задачи в Gitea и требования в OpenSpec. Главный сценарий: `bash install.sh` ставит глобальную часть в `~/.claude`, `adopt-project.sh` подключает существующий проект, `new-project.sh` создаёт новый; дальше проект живёт по `docs/HARNESS.md`. Репозиторий — источник; установленные копии в `~/.claude` по месту не правятся.\n\n## Стек и ограничения\n- Bash (`set -euo pipefail`) и markdown. JSON из stdin хуков разбирается встроенным `python3`; других зависимостей времени выполнения нет. Внешние CLI (`tea`, `openspec`, `codegraph`) проверяются, но не требуются для установки.\n- Хук блокирует вызов только через `exit 2` с сообщением в stderr; `exit 0` — пропустить. Любой сбой разбора входа должен приводить к пропуску, не к блокировке.\n- `install.sh` идемпотентен: повторный запуск не ломает `settings.json` и делает бэкапы `CLAUDE.md` и `settings.json` с меткой времени.\n- Хуки работают из `~/.claude/hooks/`, пути в `settings.hooks.json` через `$HOME`; `CLAUDE_CONFIG_DIR` переопределяет `~/.claude`.\n- macOS и Linux; `python3` должен быть в PATH сессии Claude Code.\n\n## Архитектура\n- `hooks/` — поведение обвязки, единственный «код» с логикой. `guard-main.sh` (PreToolUse на Edit/Write/MultiEdit/Bash): запрет записи на main/master/detached HEAD, исключение для openspec-only коммитов. `require-plan.sh` (PreToolUse на Edit/Write/MultiEdit): правки в каталогах кода только при плане для ветки или файле `.bounded`. `harness-check.sh` (SessionStart): проверка, что проект подключён. `settings.hooks.json` — блок, который `install.sh` сливает в `~/.claude/settings.json`.\n- `install.sh`, `adopt-project.sh`, `new-project.sh` — установка и бутстрап. `install.sh` копирует `global/`, `hooks/`, `project-template/`, `adopt-project.sh`, `new-project.sh` и `skills/harness-doctor` в `~/.claude/{CLAUDE.md,hooks,harness,skills}`. `adopt-project.sh` работает на ветке `chore/harness-init` и вливает её ff в текущую ветку; это единственная разрешённая запись на main вне PR.\n- `global/CLAUDE.md` — глобальные правила процесса, ставятся в `~/.claude/CLAUDE.md`. `project-template/` — `CLAUDE.md`, `CLAUDE.harness-section.md` (секция «OpenSpec и superpowers», по ней `harness-check` узнаёт подключённый проект), `ARCHITECTURE.md`, `gitignore`.\n- `skills/harness-doctor/` — skill диагностики: `scripts/doctor.sh` снимает состояние машины и проекта и прогоняет самотест хуков; `SKILL.md` — чеклист прочтения отчёта.\n- `caveman/config.json` — выключенный по умолчанию caveman. `examples/` — реальный `CLAUDE.md` проекта-пилота.\n- `docs/HARNESS.md` — как устроен цикл фичи, SDD, маркировка, закрытие. `docs/PILOT-2026-09-27.md` — пилот. `docs/retro/` — ретроспектива 11.10.2026 и скрипты подсчёта по транскриптам. `docs/superpowers/specs/` — спеки фич обвязки, `docs/superpowers/plans/` — планы.\n- Связь слоёв: хуки читают JSON вызова из stdin и состояние git проекта; скрипты бутстрапа читают `project-template/`; `doctor.sh` читает установленные копии в `~/.claude`, не репозиторий.\n\n## Команды\n- `bash install.sh --check` — проверка окружения: бинарники, плагины, конфликты.\n- `bash install.sh` — установка глобальной части, идемпотентно.\n- `bash skills/harness-doctor/scripts/doctor.sh [проект]` — отчёт и самотест хуков. Только чтение.\n- `bash adopt-project.sh <папка> [--no-merge] [--push]` — подключить существующий проект. Из терминала, не из Claude Code.\n- `bash new-project.sh <папка> [ssh-url]` — каркас нового проекта.\n- `python3 docs/retro/scripts/retro_stats.py <каталог транскриптов>` и `retro_blocks.py` — статистика сессий и блокировок хуков для ретро.\n\n## Тесты\n- Отдельного набора тестов нет. Единственная автоматическая проверка — самотест хуков в `doctor.sh`: фиктивные вызовы Write/Bash на main и на ветке, `src/` с планом и без, `.bounded`. Прогоняется по установленным копиям, поэтому перед проверкой изменений в `hooks/` сначала `bash install.sh`.\n- Ручная проверка хука: на main в любом репозитории попросить агента создать файл, ожидается `BLOCKED`.\n- Регрессионные тесты хуков на реальных командах из транскриптов — бэклог ретро B2, пока не сделаны.\n\n## Текущая работа\nБэклог обвязки — `docs/retro/2026-10-11.md`, раздел 4 (B1–B12), решения владельца — раздел 5. Порядок: B1+B2 (guard-main по цели записи, тесты хуков), затем B5+B6 (роли и память, спека `docs/superpowers/specs/2026-10-11-roles-and-memory-design.md`), затем B3/B4/B10. Находки ретро маркируются `RT-n`.\n\n## OpenSpec и superpowers\n- Вход в любую задачу — superpowers:brainstorming. Skills openspec-propose и openspec-explore сами по себе не запускать.\n- Когда brainstorming классифицировал задачу как architectural, структуру change создать через `openspec new change ` (или `/opsx:propose `), а содержимое proposal.md, specs/ и design.md написать по итогам brainstorming. tasks.md из OpenSpec не заполнять — задачи живут в плане superpowers.\n- `/opsx:apply` не использовать: исполнение идёт через writing-plans и SDD/executing-plans.\n- `openspec archive ` — после merge PR, как часть чеклиста закрытия.\n- Хук require-plan защищает любой каталог `src/` на любой глубине. Если код лежит иначе — перечислить каталоги (префиксы от корня, по одному в строке) в `.claude/code-paths`. В этом репозитории код — `hooks/` и `skills/`, они перечислены в `.claude/code-paths`.\nEOF\nprintf 'hooks/\\nskills/\\n' > .claude/code-paths\necho '--- checks'; grep -c '{{' CLAUDE.md; grep -q '^## OpenSpec и superpowers' CLAUDE.md && echo section-ok; bash ~/.claude/skills/harness-doctor/scripts/doctor.sh . 2>&1 | sed -n '/## Проект/,$p'; echo '--- require-plan on hooks/ with .bounded'; printf '{\"cwd\":\"%s\",\"tool_input\":{\"file_path\":\"%s/hooks/x.sh\"}}' \"$PWD\" \"$PWD\" | bash ~/.claude/hooks/require-plan.sh; echo \"exit=$?\"; mv .bounded .bounded.off; printf '{\"cwd\":\"%s\",\"tool_input\":{\"file_path\":\"%s/hooks/x.sh\"}}' \"$PWD\" \"$PWD\" | bash ~/.claude/hooks/require-plan.sh; echo \"exit=$? (ожидалось 2)\"; mv .bounded.off .bounded; git status --short", "file_path": null, "hook": "require-plan", "message": "BLOCKED: editing /Users/andrey.epifancev/projects/cc-skills/claude-harness/.wt/retro/hooks/x.sh without a plan. Run superpowers:brainstorming; for a bounded change create '/Users/andrey.epifancev/projects/cc-skills/claude-harness/.wt/retro/.bounded' first.", "expected": "allow", "id": "c113", "why": "not-a-block"} +{"session": "daff409e", "ts": "2026-10-12T10:37:01.326Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness", "branch": "main", "tool": "Bash", "command": "cd ~ ; echo \"--- which -a\"; which -a claude; echo \"--- realpath\"; realpath \"$(which claude)\" 2>/dev/null; echo \"--- native\"; ls -la ~/.local/bin/claude 2>&1; echo \"--- npm prefix\"; P=$(npm -g config get prefix 2>/dev/null); echo \"$P\"; ls -d \"$P/lib/node_modules/@anthropic-ai/claude-code\" 2>&1; echo \"--- local\"; ls -d ~/.claude/local 2>&1; echo \"--- PATH has .local/bin?\"; echo \"$PATH\" | tr ':' '\\n' | grep -n \"\\.local/bin\"; echo \"--- version\"; claude --version; echo \"--- installMethod/autoUpdates\"; jq '{installMethod, autoUpdates, numStartups}' ~/.claude.json; echo \"--- env flags\"; env | grep -E '^(CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC|DISABLE_AUTOUPDATER|DISABLE_UPDATES)=' | cut -d= -f1", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (cd ~ ; echo \"--- which -a\"; which -a claude; echo \"--- realpath\"; realpath \"$(wh). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c114", "why": "devnull"} +{"session": "daff409e", "ts": "2026-10-12T10:37:13.083Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness", "branch": "main", "tool": "Bash", "command": "grep -nE 'npm|realpath|cd |write|BLOCKED|pattern|case' ~/.claude/hooks/guard-main.sh | head -40", "file_path": null, "hook": "guard-main(Bash)", "message": "37: echo \"BLOCKED: shell write on $label ($(printf '%s' \"$cmd\" | head -c 80)). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly.\" >&2", "expected": "allow", "id": "c115", "why": "not-a-block"} +{"session": "daff409e", "ts": "2026-10-12T10:37:30.378Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness", "branch": "main", "tool": "Bash", "command": "echo \"--- user skills\"; ls ~/.claude/skills 2>&1; echo \"--- project skills\"; ls .claude/skills 2>&1; echo \"--- agents\"; ls ~/.claude/agents .claude/agents 2>&1; echo \"--- plugins\"; jq -r '.plugins | keys[]' ~/.claude/plugins/installed_plugins.json 2>/dev/null; echo \"--- CLAUDE.md files\"; ls -la ~/.claude/CLAUDE.md CLAUDE.md CLAUDE.local.md .claude/CLAUDE.md 2>&1; ls .claude/rules 2>&1; find . -name CLAUDE.md -not -path './node_modules/*' -not -path './.git/*'; wc -c ~/.claude/CLAUDE.md CLAUDE.md; ls", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (echo \"--- user skills\"; ls ~/.claude/skills 2>&1; echo \"--- project skills\"; ls ). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c116", "why": "devnull"} +{"session": "daff409e", "ts": "2026-10-12T10:37:54.874Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness", "branch": "main", "tool": "Bash", "command": "ls ~/.claude/skills/synced | head; ls .claude/commands; ls .claude/commands/* | head; echo \"--- global drift\"; diff -q global/CLAUDE.md ~/.claude/CLAUDE.md; git check-ignore -v .wt; for d in ~/.claude/skills/*/ .claude/skills/*/; do f=\"$d/SKILL.md\"; [ -f \"$f\" ] || continue; awk 'NR==1&&$0!=\"---\"{print FILENAME\": no frontmatter\";exit} NR>1&&$0==\"---\"{exit} NR>1' \"$f\" > /dev/null; done", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (ls ~/.claude/skills/synced | head; ls .claude/commands; ls .claude/commands/* | ). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c117", "why": "read-only"} +{"session": "daff409e", "ts": "2026-10-12T10:38:16.273Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness", "branch": "main", "tool": "Write", "command": null, "file_path": "/private/tmp/claude-501/-Users-andrey-epifancev-projects-cc-skills-claude-harness/daff409e-f6ff-4ead-b651-64cb360470eb/scratchpad/scan.py", "hook": "guard-main(Edit/Write)", "message": "PreToolUse:Write hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: you are on main. Create an isolated worktree first (superpowers:using-git-worktrees), never edit main directly.", "expected": "allow", "id": "c118", "why": "outside-repo"} +{"session": "daff409e", "ts": "2026-10-12T10:38:39.879Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness", "branch": "main", "tool": "Bash", "command": "python3 -I - <<'EOF'\nimport json, os, glob, re, collections, statistics, time\nhome = os.path.expanduser(\"~\")\ndef fm_check(path):\n lines = open(path, encoding=\"utf-8\").read().split(\"\\n\")\n if not lines or lines[0].strip() != \"---\":\n return \"no frontmatter\"\n try:\n end = lines[1:].index(\"---\") + 1\n except ValueError:\n return \"unterminated\"\n block = \"\\n\".join(lines[1:end])\n try:\n import yaml\n except ImportError:\n bad = [l for l in block.split(\"\\n\") if l and not l.startswith((\" \", \"\\t\", \"-\", \"#\")) and \":\" not in l]\n return \"no-yaml-lib; suspicious lines=%d\" % len(bad)\n try:\n d = yaml.safe_load(block)\n if not isinstance(d, dict): return \"not a mapping\"\n miss = [k for k in (\"name\",\"description\") if k not in d]\n return \"ok\" + (\" missing \" + \",\".join(miss) if miss else \"\") + \" desc=%d\" % len(str(d.get(\"description\",\"\")))\n except Exception as e:\n return \"YAML ERROR: \" + str(e).split(\"\\n\")[0]\nprint(\"== skill frontmatter\")\nfor p in glob.glob(home + \"/.claude/skills/*/SKILL.md\") + glob.glob(\".claude/skills/*/SKILL.md\"):\n print(\" \", p.replace(home, \"~\"), \"=\", fm_check(p))\nfiles = sorted(glob.glob(home + \"/.claude/projects/*/*.jsonl\"), key=os.path.getmtime, reverse=True)[:50]\nmt = [os.path.getmtime(f) for f in files]\nprint(\"\\n== window: %d sessions, %.1f days (%s .. %s)\" % (len(files), (max(mt)-min(mt))/86400, time.strftime(\"%Y-%m-%d\", time.localtime(min(mt))), time.strftime(\"%Y-%m-%d\", time.localtime(max(mt)))))\nskills=collections.Counter(); slash=collections.Counter(); mcp=collections.Counter()\nhooks=collections.defaultdict(list); hto=collections.Counter()\nden=collections.Counter(); dk=collections.defaultdict(collections.Counter)\ncre=re.compile(r\"/?([^<]+)\")\nfor fp in files:\n tu={}; pend=[]\n for line in open(fp, encoding=\"utf-8\", errors=\"replace\"):\n try: o=json.loads(line)\n except Exception: continue\n t=o.get(\"type\"); msg=o.get(\"message\") or {}; c0=msg.get(\"content\") if isinstance(msg, dict) else None\n if t==\"assistant\" and isinstance(c0,list):\n for c in c0:\n if isinstance(c,dict) and c.get(\"type\")==\"tool_use\":\n n=c.get(\"name\",\"\"); inp=c.get(\"input\") or {}; tu[c.get(\"id\")]=(n,inp)\n if n==\"Skill\": skills[str(inp.get(\"skill\"))]+=1\n elif n.startswith(\"mcp__\"): mcp[n.split(\"__\")[1]]+=1\n elif t==\"user\":\n s=c0 if isinstance(c0,str) else json.dumps(c0) if c0 else \"\"\n for m in cre.findall(s): slash[m.strip()]+=1\n kind=o.get(\"toolDenialKind\")\n if isinstance(c0,list):\n for c in c0:\n if isinstance(c,dict) and c.get(\"type\")==\"tool_result\":\n if kind and kind not in (\"interrupted\",\"cancelled\"): pend.append((c.get(\"tool_use_id\"),kind,True))\n elif not kind and c.get(\"is_error\"):\n x=json.dumps(c.get(\"content\"))\n if \"want to proceed\" in x or \"Permission to use\" in x or \"Permission for this\" in x: pend.append((c.get(\"tool_use_id\"),\"fallback\",False))\n elif t==\"attachment\":\n a=o.get(\"attachment\") or {}\n if str(a.get(\"type\",\"\")).startswith(\"hook_\"):\n k=(a.get(\"hookEvent\"), (a.get(\"command\") or a.get(\"hookName\") or \"\")[-45:])\n if a.get(\"durationMs\") is not None: hooks[k].append(a[\"durationMs\"])\n if a.get(\"type\")==\"hook_cancelled\" and a.get(\"timedOut\"): hto[k]+=1\n for tid,kind,st in pend:\n n,inp=tu.get(tid,(\"?\",{}))\n if n.startswith(\"mcp__\") and not st: continue\n k=(\"Bash: \"+\" \".join(str(inp.get(\"command\",\"\")).split()[:2])[:50]) if n==\"Bash\" else n\n den[k]+=1; dk[k][kind]+=1\nprint(\"== Skill calls:\", dict(skills.most_common()))\nprint(\"== slash:\", dict(slash.most_common(40)))\nprint(\"== MCP by server:\", dict(mcp.most_common()))\nprint(\"\\n== hooks: n median max\")\nfor k,v in sorted(hooks.items(), key=lambda kv: -statistics.median(kv[1])): print(\" \",k,len(v),int(statistics.median(v)),max(v))\nprint(\"== hook timeouts:\", dict(hto))\nprint(\"\\n== denials\")\nfor k,n in den.most_common(25): print(\" \",k,n,dict(dk[k]))\nEOF", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (python3 -I - <<'EOF'", "expected": "allow", "id": "c119", "why": "heredoc"} +{"session": "e9960c90", "ts": "2026-10-12T10:59:13.940Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness", "branch": "main", "tool": "Bash", "command": "git fetch origin --prune 2>&1 | tail -3; git log --oneline -6 origin/main; echo ---; git branch -r; echo ---; git merge-base --is-ancestor origin/retro/2026-10-11 origin/main 2>/dev/null && echo \"retro is ancestor of main\" || echo \"retro NOT ancestor (squash/rebase?)\"; tea pulls edit --help 2>&1 | head -30", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (git fetch origin --prune 2>&1 | tail -3; git log --oneline -6 origin/main; echo ). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c120", "why": "devnull"} +{"session": "e9960c90", "ts": "2026-10-12T11:02:22.931Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness/.wt/roles", "branch": "main", "tool": "Bash", "command": "sed -n 1,45p ~/.claude/hooks/guard-main.sh", "file_path": null, "hook": "guard-main(Bash)", "message": " echo \"BLOCKED: shell write on $label ($(printf '%s' \"$cmd\" | head -c 80)). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly.\" >&2", "expected": "allow", "id": "c121", "why": "not-a-block"} +{"session": "e9960c90", "ts": "2026-10-12T11:03:38.383Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness", "branch": "main", "tool": "Write", "command": null, "file_path": "/Users/andrey.epifancev/.claude/projects/-Users-andrey-epifancev-projects-cc-skills-claude-harness/memory/roles-and-memory-prs.md", "hook": "guard-main(Edit/Write)", "message": "PreToolUse:Write hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: you are on main. Create an isolated worktree first (superpowers:using-git-worktrees), never edit main directly.", "expected": "allow", "id": "c122", "why": "outside-repo"} +{"session": "e9960c90", "ts": "2026-10-12T11:09:44.026Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness", "branch": "main", "tool": "Bash", "command": "grep -rniE 'guard-main' docs/retro/2026-10-11.md docs/superpowers/specs/*.md CHANGELOG.md 2>/dev/null | head -20; echo ----; tea issues list --state open 2>&1 | head -8; echo ----; ls hooks; grep -n 'file_path' hooks/guard-main.sh", "file_path": null, "hook": "guard-main(Bash)", "message": "PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: shell write on main (grep -rniE 'guard-main' docs/retro/2026-10-11.md docs/superpowers/specs/*.md CHA). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main di", "expected": "allow", "id": "c123", "why": "devnull"} +{"session": "e9960c90", "ts": "2026-10-12T11:25:08.186Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness/.wt/guard", "branch": "tmp/guard-main", "tool": "Bash", "command": "sed -n 38,75p docs/retro/2026-10-11.md; grep -nE '^\\*\\*RT-(6|7)' -A6 docs/retro/2026-10-11.md", "file_path": null, "hook": "guard-main(Bash)", "message": "контроллер и всех его implementer-ов в состояние `BLOCKED: shell write on main`, и они останавливаются. Правило «не cd, пока", "expected": "allow", "id": "c124", "why": "not-a-block"} +{"session": "e9960c90", "ts": "2026-10-12T11:25:14.192Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness/.wt/guard", "branch": "tmp/guard-main", "tool": "Bash", "command": "ls docs/retro/scripts; sed -n 1,200p docs/retro/scripts/retro_blocks.py", "file_path": null, "hook": "guard-main(Bash)", "message": " if \"BLOCKED\" in txt and (\"hook\" in txt.lower() or \"BLOCKED:\" in txt[:400]):", "expected": "allow", "id": "c125", "why": "not-a-block"} +{"session": "e9960c90", "ts": "2026-10-12T11:26:42.248Z", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness/.wt/guard", "branch": "feat/6-guard-main-target", "tool": "Bash", "command": "python3 docs/retro/scripts/hook_blocks_export.py /private/tmp/claude-501/-Users-andrey-epifancev-projects-cc-skills-claude-harness/e9960c90-6445-4fa5-b76f-0b371dfa85b2/scratchpad/fake-tr", "file_path": null, "hook": "guard-main(Bash)", "message": "{\"session\": \"s1.jsonl\", \"ts\": \"t1\", \"cwd\": \"/r\", \"branch\": \"main\", \"tool\": \"Bash\", \"command\": \"grep x 2>/dev/null; tea login add --token \"file_path\": null, \"hook\": \"guard-main(Bash)\", \"message\": \"PreToolUse:Bash hook error: BLOCKED: shell write on main (grep)\", \"expected\": null}", "expected": "allow", "id": "c126", "why": "read-only"} +{"id": "l001", "session": "live-1015", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness", "branch": "main", "tool": "Bash", "command": "grep -rn \"npm i\\|npm install\\|brew install\\|plugin install\\|go install\" --include=\"*.md\" --include=\"*.sh\" . | grep -v \"^./docs/retro/2026\" | head -20", "file_path": null, "hook": "guard-main(Bash)", "message": "", "expected": "allow", "why": "grep-pattern"} +{"id": "l002", "session": "live-1015", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness", "branch": "main", "tool": "Bash", "command": "grep -rn \"plugin add\\|marketplace\\|caveman@\\|superpowers@\" --include=\"*.md\" --include=\"*.sh\" README.md docs global install.sh 2>/dev/null | head -20", "file_path": null, "hook": "guard-main(Bash)", "message": "", "expected": "allow", "why": "devnull"} +{"id": "l003", "session": "live-1015", "cwd": "/Users/andrey.epifancev/projects/cc-skills/claude-harness", "branch": "main", "tool": "Bash", "command": "git show origin/feat/6-guard-main-target:docs/retro/fixtures/hook-blocks.jsonl | python3 -c '\nimport sys,json,re,collections\nrs=[json.loads(l) for l in sys.stdin if l.strip()]\nc=collections.Counter()\nfor r in rs:\n cmd=r.get(\"command\") or \"\"\n if re.search(r\"\\d>{1,2}\\s*/dev/null|\\d>&\\d\",cmd): c[\"bash: N>/dev/null или N>&M\"]+=1\n if re.search(r\"mkdir\\s+-p\\s+\\S*\\.wt/\",cmd): c[\"bash: mkdir -p .wt/\"]+=1\n if re.search(r\"\\bsed\\s+-i\",cmd): c[\"bash: sed -i\"]+=1\nfor k,v in c.most_common(): print(f\"{v:4} {k}\")'", "file_path": null, "hook": "guard-main(Bash)", "message": "", "expected": "allow", "why": "regex-text"} diff --git a/skills/harness-doctor/scripts/doctor.sh b/skills/harness-doctor/scripts/doctor.sh index 587acc2..8eb923d 100755 --- a/skills/harness-doctor/scripts/doctor.sh +++ b/skills/harness-doctor/scripts/doctor.sh @@ -22,6 +22,7 @@ for h in guard-main require-plan harness-check role-hint; do grep -q "$h.sh" "$CC/settings.json" 2>/dev/null && ok "hook $h.sh установлен и зарегистрирован" || warn "hook $h.sh есть, но не зарегистрирован в settings.json" else miss "hook $h.sh"; fi done +[ -f "$CC/hooks/harness_target.py" ] && ok "hooks/harness_target.py (ядро guard-main и require-plan)" || miss "hooks/harness_target.py (bash install.sh)" [ -x "$CC/harness/adopt-project.sh" ] && ok "~/.claude/harness/ (adopt/new-project)" || miss "~/.claude/harness/adopt-project.sh" [ -x "$CC/harness/link-roles.sh" ] && ok "~/.claude/harness/link-roles.sh" || miss "~/.claude/harness/link-roles.sh" rmiss=(); for r in dev admin analyst qa; do [ -f "$CC/roles/$r.md" ] || rmiss+=("$r"); done @@ -43,6 +44,9 @@ if [ -x "$CC/hooks/guard-main.sh" ]; then hk guard-main "{\"cwd\":\"$T\",\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$T/x\"}}" 2 "Write на main" hk guard-main "{\"cwd\":\"$T\",\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"echo hi > x\"}}" 2 "Bash echo > на main" hk guard-main "{\"cwd\":\"$T\",\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"git worktree add ../w -b f\"}}" 0 "git worktree на main" + mkdir -p "$T/openspec"; echo a > "$T/openspec/x.md"; git -C "$T" add openspec 2>/dev/null + hk guard-main "{\"cwd\":\"$T\",\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"git commit -m archive\"}}" 2 "git commit openspec-only на main" + git -C "$T" reset -q 2>/dev/null git -C "$T" switch -q -c feat/1-x 2>/dev/null hk guard-main "{\"cwd\":\"$T\",\"tool_name\":\"Write\",\"tool_input\":{\"file_path\":\"$T/x\"}}" 0 "Write на ветке" fi @@ -58,6 +62,12 @@ if [ -x "$CC/hooks/role-hint.sh" ]; then [ -z "$o" ] && ok "role-hint: без заголовка → тишина" || warn "role-hint: без заголовка → '$o' (ожидалась тишина)" fi rm -rf "$T" +HC="$CC/skills/harness-doctor/scripts/hook_corpus.py" +if [ -f "$HC" ] && [ -f "$CC/hooks/harness_target.py" ]; then + out=$(python3 "$HC" "$CC/hooks" --summary 2>&1); rc=$? + printf '%s\n' "$out" | grep '^MISMATCH' | sed 's/^MISMATCH / /' + [ "$rc" = 0 ] && ok "$(printf '%s\n' "$out" | tail -1)" || warn "$(printf '%s\n' "$out" | tail -1) — расхождения выше" +else miss "корпус хуков (skills/harness-doctor/scripts/hook_corpus.py, hooks/harness_target.py)"; fi echo "## Проект: $(cd "$DIR" 2>/dev/null && pwd)" if root=$(git -C "$DIR" rev-parse --show-toplevel 2>/dev/null); then diff --git a/skills/harness-doctor/scripts/hook_corpus.py b/skills/harness-doctor/scripts/hook_corpus.py new file mode 100644 index 0000000..5457ed5 --- /dev/null +++ b/skills/harness-doctor/scripts/hook_corpus.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""Регрессионный корпус хуков (B2): прогон fixtures/hook-cases.jsonl через guard-main и require-plan в песочнице. + +Запуск: python3 hook_corpus.py <каталог хуков> [--summary] [--fixtures ] +Песочница строится во временном каталоге по раскладке из первой строки корпуса; префикс домашнего каталога записей +заменяется на её home, HOME хуков подменяется туда же. Bash идёт через guard-main, Edit/Write — через guard-main и +require-plan (блок, если блокирует любой). +Вывод: строка на запись «idwhyожидалосьполученокоманда» и итог «total N/M»; +с --summary — только расхождения и «корпус хуков: N/M». Код выхода 0 — всё совпало, 1 — нет. +""" +import json +import os +import shutil +import subprocess +import sys +import tempfile + +HERE = os.path.dirname(os.path.abspath(__file__)) + + +def _git(*args): + subprocess.run(["git", "-c", "user.name=corpus", "-c", "user.email=corpus@example.invalid"] + list(args), + check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + +def build(home, layout): + for e in layout: + p = os.path.join(home, e["path"]) + if "worktree_of" in e: + _git("-C", os.path.join(home, e["worktree_of"]), "worktree", "add", "-q", p, "-b", e["branch"]) + elif "branch" in e: + os.makedirs(p, exist_ok=True) + _git("init", "-q", p) + _git("-C", p, "symbolic-ref", "HEAD", "refs/heads/" + e["branch"]) + _git("-C", p, "commit", "-q", "--allow-empty", "-m", "init") + else: + os.makedirs(p, exist_ok=True) + + +def verdict(hooks, case, prefix, home, env): + swap = lambda s: s.replace(prefix, home) if isinstance(s, str) else s + tool = case["tool"] + ti = {"command": swap(case.get("command"))} if tool == "Bash" else {"file_path": swap(case.get("file_path"))} + payload = json.dumps({"cwd": swap(case.get("cwd")), "tool_name": tool, "tool_input": ti}) + for name in ["guard-main"] + ([] if tool == "Bash" else ["require-plan"]): + r = subprocess.run(["bash", os.path.join(hooks, name + ".sh")], input=payload, universal_newlines=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, timeout=30) + if r.returncode == 2: + return "block" + if r.returncode != 0: + return "error" + return "allow" + + +def main(argv): + args = argv[1:] + summary = "--summary" in args + fx = os.path.join(HERE, "..", "fixtures", "hook-cases.jsonl") + if "--fixtures" in args: + fx = args[args.index("--fixtures") + 1] + pos = [a for k, a in enumerate(args) if not a.startswith("--") and (k == 0 or args[k - 1] != "--fixtures")] + if not pos: + print(__doc__.strip().splitlines()[2]) + return 1 + hooks = os.path.abspath(pos[0]) + lines = [json.loads(l) for l in open(fx, encoding="utf-8") if l.strip()] + head, cases = lines[0], lines[1:] + tmp = tempfile.mkdtemp(prefix="hook-corpus-") + try: + home = os.path.join(tmp, "home") + try: + build(home, head["layout"]) + except Exception as e: + print("корпус хуков: песочница не собрана (%s)" % e) + return 1 + env = {k: v for k, v in os.environ.items() if k not in ("GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE")} + env["HOME"] = home + ok = 0 + for c in cases: + got = verdict(hooks, c, head["home"], home, env) + ok += got == c["expected"] + body = (c.get("command") or c.get("file_path") or "").replace(head["home"], "~") + row = "\t".join([c["id"], c["why"], c["expected"], got, " ".join(body.split())[:70]]) + if not summary: + print(row) + elif got != c["expected"]: + print("MISMATCH " + row) + print(("корпус хуков: %d/%d" if summary else "total %d/%d") % (ok, len(cases))) + return 0 if ok == len(cases) else 1 + finally: + shutil.rmtree(tmp, ignore_errors=True) + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/tests/test-doctor.sh b/tests/test-doctor.sh index 5c03860..4f93bbd 100644 --- a/tests/test-doctor.sh +++ b/tests/test-doctor.sh @@ -1,7 +1,8 @@ # doctor.sh: новые проверки — файлы ролей, самотест role-hint, режим workspace, размеры. D="$ROOT/skills/harness-doctor/scripts/doctor.sh" CC="$TMP/cc"; mkdir -p "$CC/hooks" "$CC/roles" "$CC/skills/role" "$CC/agents" "$CC/harness" "$CC/projects" -cp "$ROOT"/hooks/*.sh "$CC/hooks/"; chmod +x "$CC/hooks/"*.sh +cp "$ROOT"/hooks/*.sh "$ROOT/hooks/harness_target.py" "$CC/hooks/"; chmod +x "$CC/hooks/"*.sh +mkdir -p "$CC/skills"; cp -R "$ROOT/skills/harness-doctor" "$CC/skills/" cp "$ROOT"/roles/*.md "$CC/roles/"; cp "$ROOT/skills/role/SKILL.md" "$CC/skills/role/"; cp "$ROOT/agents/briefing.md" "$CC/agents/" cp "$ROOT/link-roles.sh" "$ROOT/adopt-project.sh" "$CC/harness/"; cp "$ROOT/global/CLAUDE.md" "$CC/CLAUDE.md" printf '{"hooks":{"SessionStart":[{"hooks":[{"type":"command","command":"\\"$HOME/.claude/hooks/role-hint.sh\\""}]}]}}' > "$CC/settings.json" @@ -19,6 +20,12 @@ assert_grep "машина: агент briefing" 'ok +agent briefing' <(printf '% assert_grep "машина: link-roles.sh" 'ok +.*link-roles.sh' <(printf '%s' "$out") assert_grep "самотест: role-hint admin" 'ok +role-hint: заголовок admin' <(printf '%s' "$out") assert_grep "самотест: role-hint тишина" 'ok +role-hint: без заголовка' <(printf '%s' "$out") +assert_grep "машина: harness_target.py" 'ok +hooks/harness_target\.py' <(printf '%s' "$out") +assert_grep "самотест: openspec-only коммит на main — блок" 'ok +guard-main: git commit openspec-only на main → exit 2' <(printf '%s' "$out") +assert_grep "самотест: Write на main" 'ok +guard-main: Write на main → exit 2' <(printf '%s' "$out") +assert_grep "самотест: Write на ветке" 'ok +guard-main: Write на ветке → exit 0' <(printf '%s' "$out") +assert_grep "корпус хуков сходится" 'ok +корпус хуков: [0-9]+/[0-9]+' <(printf '%s' "$out") +assert_grep "заголовок раздела проекта (M-8)" '^## Проект: /' <(printf '%s' "$out") assert_grep "проект: режим workspace" 'workspace' <(printf '%s' "$out") assert_grep "проект: целая ссылка" 'ok +roles/admin.md' <(printf '%s' "$out") assert_grep "проект: битая ссылка" 'WARN +agents/none.md' <(printf '%s' "$out") diff --git a/tests/test-hook-corpus.sh b/tests/test-hook-corpus.sh new file mode 100644 index 0000000..b3d5d0b --- /dev/null +++ b/tests/test-hook-corpus.sh @@ -0,0 +1,10 @@ +# hook_corpus.py: регрессионный корпус хуков (B2) через источники hooks/ — assert на запись. +R="$ROOT/skills/harness-doctor/scripts/hook_corpus.py" +F="$ROOT/skills/harness-doctor/fixtures/hook-cases.jsonl" +assert_file "hook_corpus.py существует" "$R" +assert_file "корпус hook-cases.jsonl" "$F" +assert_eq "корпус совпадает со сборкой из выгрузки" "$(python3 "$ROOT/docs/retro/scripts/hook_cases_build.py" | cmp -s - "$F" && echo same)" "same" +while IFS=$'\t' read -r id why exp got cmd; do + case "$id" in total*) continue ;; esac + assert_eq "корпус $id ($why): $cmd" "$got" "$exp" +done < <(python3 "$R" "$ROOT/hooks") diff --git a/tests/test-hook-export.sh b/tests/test-hook-export.sh new file mode 100644 index 0000000..46a7caa --- /dev/null +++ b/tests/test-hook-export.sh @@ -0,0 +1,15 @@ +# hook_blocks_export.py: выгружаются только ошибки хуков, а не вывод команд со словом BLOCKED. +X="$ROOT/docs/retro/scripts/hook_blocks_export.py" +T="$TMP/transcripts"; mkdir -p "$T" +cat > "$T/s1.jsonl" <<'EOF' +{"type":"assistant","cwd":"/r","gitBranch":"main","timestamp":"t1","message":{"content":[{"type":"tool_use","id":"u1","name":"Bash","input":{"command":"echo x > f"}}]}} +{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"u1","is_error":true,"content":"PreToolUse:Bash hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED [guard-main]: запись в f — репозиторий /r на main."}]}} +{"type":"assistant","cwd":"/r","gitBranch":"main","timestamp":"t2","message":{"content":[{"type":"tool_use","id":"u2","name":"Bash","input":{"command":"cat hooks/require-plan.sh"}}]}} +{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"u2","content":"echo \"BLOCKED: editing $fpath without a plan. Run superpowers:brainstorming\" >&2"}]}} +{"type":"assistant","cwd":"/r","gitBranch":"main","timestamp":"t3","message":{"content":[{"type":"tool_use","id":"u3","name":"Edit","input":{"file_path":"/r/a.md"}}]}} +{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"u3","is_error":true,"content":[{"type":"text","text":"PreToolUse:Edit hook error: [\"$HOME/.claude/hooks/guard-main.sh\"]: BLOCKED: you are on main. Create an isolated worktree first"}]}]}} +EOF +out=$(python3 "$X" "$T") +assert_eq "две записи: вывод cat с BLOCKED не выгружен" "$(printf '%s\n' "$out" | grep -c .)" "2" +assert_eq "новый префикс → guard-main(Bash)" "$(printf '%s\n' "$out" | sed -n 1p | python3 -c 'import json,sys; r=json.load(sys.stdin); print(r["hook"], r["command"])')" "guard-main(Bash) echo x > f" +assert_eq "старая фраза → guard-main(Edit/Write)" "$(printf '%s\n' "$out" | sed -n 2p | python3 -c 'import json,sys; r=json.load(sys.stdin); print(r["hook"], r["file_path"])')" "guard-main(Edit/Write) /r/a.md" diff --git a/tests/test-hook-guard-main.sh b/tests/test-hook-guard-main.sh new file mode 100644 index 0000000..363e237 --- /dev/null +++ b/tests/test-hook-guard-main.sh @@ -0,0 +1,88 @@ +# guard-main.sh: решение по цели записи (спека hook-guards) в песочнице из настоящих git-репозиториев. +G="$ROOT/hooks/guard-main.sh" +S="$TMP/sb"; mkdir -p "$S/ws" "$HOME/.claude/projects/p/memory" +mk(){ git init -q "$1" && git -C "$1" symbolic-ref HEAD "refs/heads/${2:-main}" && git -C "$1" commit -q --allow-empty -m init; } +mk "$S/m" # репозиторий на main +mk "$S/f"; git -C "$S/f" worktree add -q "$S/wt" -b feat/6-x # основной checkout на main + worktree на ветке +mk "$S/ms" master # на master +mk "$S/d"; git -C "$S/d" checkout -q --detach # detached HEAD +j(){ python3 -c 'import json,sys; k="command" if sys.argv[2]=="Bash" else "file_path"; print(json.dumps({"cwd":sys.argv[1],"tool_name":sys.argv[2],"tool_input":{k:sys.argv[3]}}))' "$@"; } +rc(){ j "$@" | bash "$G" 2>/dev/null; echo $?; } +err(){ j "$@" | bash "$G" 2>&1 >/dev/null; } +# решение по цели, а не по cwd +assert_eq "чтение с 2>/dev/null на main" "$(rc "$S/m" Bash 'grep -rn "npm i\|npm install" . 2>/dev/null | head')" 0 +assert_eq "fetch 2>&1 | tail на main" "$(rc "$S/m" Bash 'git fetch origin --prune 2>&1 | tail -3')" 0 +assert_eq "Write вне репозитория при cwd на main" "$(rc "$S/m" Write "$HOME/.claude/projects/p/memory/x.md")" 0 +assert_eq "Edit в worktree при cwd на main" "$(rc "$S/f" Edit "$S/wt/a.md")" 0 +assert_eq "Edit в репозитории на main при cwd в worktree" "$(rc "$S/wt" Edit "$S/m/a.md")" 2 +assert_eq "Write на master" "$(rc "$S/ws" Write "$S/ms/a.md")" 2 +assert_eq "Write при detached HEAD" "$(rc "$S/ws" Write "$S/d/a.md")" 2 +# файловые цели Bash +assert_eq "echo > в main" "$(rc "$S/m" Bash 'echo x > notes.md')" 2 +assert_eq "make 2>build.log в main" "$(rc "$S/m" Bash 'make 2>build.log')" 2 +assert_eq "sed -i по файлу main" "$(rc "$S/ws" Bash "sed -i 's/a/b/' $S/m/docs/HARNESS.md")" 2 +assert_eq "sed -i '' по файлу памяти" "$(rc "$S/m" Bash "sed -i '' 's/a/b/' $HOME/.claude/projects/p/memory/x.md")" 0 +assert_eq "docker … sh -c > /tmp" "$(rc "$S/m" Bash "docker compose exec -T db sh -c 'psql -c \"select 1\" > /tmp/x'")" 0 +assert_eq "pip install на main" "$(rc "$S/m" Bash 'pip install requests')" 0 +assert_eq "npm install на main" "$(rc "$S/m" Bash 'npm install')" 2 +assert_eq "цель в переменной" "$(rc "$S/m" Bash 'echo x > "$OUT"')" 0 +assert_eq "цель через \$HOME в main" "$(HOME="$S" rc "$S/ws" Bash 'echo x > $HOME/m/f')" 2 +# каталог внутри команды +assert_eq "cd в worktree и запись" "$(rc "$S/f" Bash "cd $S/wt && echo x > f")" 0 +assert_eq "cd в main и touch" "$(rc "$S/wt" Bash "cd $S/m && touch f")" 2 +assert_eq "mkdir -p .wt/ в не-git workspace" "$(rc "$S/ws" Bash 'mkdir -p .wt/6-x')" 0 +assert_eq "bash -c с записью в main" "$(rc "$S/m" Bash "bash -c 'echo x > f'")" 2 +# git +assert_eq "add && commit в main" "$(rc "$S/m" Bash 'git add -A && git commit -m x')" 2 +assert_eq "commit -C worktree при cwd на main" "$(rc "$S/f" Bash "git -C $S/wt commit -m x")" 0 +assert_eq "worktree add -C из workspace" "$(rc "$S/ws" Bash "git -C $S/m worktree add ../.wt/x -b feat/x")" 0 +assert_eq "pull --ff-only на main" "$(rc "$S/m" Bash 'git pull --ff-only')" 0 +assert_eq "stash list на main" "$(rc "$S/m" Bash 'git stash list')" 0 +assert_eq "-C в переменной — блок" "$(rc "$S/ws" Bash 'git -C "$REPO" commit -m x')" 2 +assert_grep "-C в переменной — подсказка git -C" 'git -C <абсолютный путь>' <(err "$S/ws" Bash 'git -C "$REPO" commit -m x') +assert_eq "switch -c && commit на main" "$(rc "$S/m" Bash 'git switch -q -c chore/archive-x && git add openspec && git commit -m a')" 0 +assert_eq "switch -c && touch на main" "$(rc "$S/m" Bash 'git switch -q -c feat/16 && touch .bounded')" 0 +assert_eq "switch в переменную && commit — блок" "$(rc "$S/m" Bash 'git switch "$B" && git commit -m x')" 2 +assert_eq "switch в переменную && touch — пропуск" "$(rc "$S/m" Bash 'git switch "$B" && touch f')" 0 +assert_eq "commit вне репозитория" "$(rc "$S/ws" Bash 'git commit -m x')" 0 +# push +assert_eq "push с main" "$(rc "$S/m" Bash 'git push')" 2 +assert_eq "push --delete фичи с main" "$(rc "$S/m" Bash 'git push origin --delete feat/6-x')" 0 +assert_eq "push фичи с main" "$(rc "$S/m" Bash 'git push origin feat/6-x')" 0 +assert_eq "push HEAD:main из worktree" "$(rc "$S/wt" Bash 'git push origin HEAD:main')" 2 +assert_eq "push из worktree" "$(rc "$S/wt" Bash 'git push -u origin HEAD')" 0 +# без исключения для openspec (решение владельца 1) +mkdir -p "$S/m/openspec"; echo a > "$S/m/openspec/x.md"; git -C "$S/m" add openspec +assert_eq "commit только openspec/ в main" "$(rc "$S/m" Bash 'git commit -m archive')" 2 +git -C "$S/m" reset -q +assert_eq "openspec archive в main" "$(rc "$S/m" Bash 'openspec archive x')" 2 +# граничные случаи (Review Focus) +ln -s "$S/wt/CLAUDE.md" "$S/ws/CLAUDE.md"; ln -s "$S/m/CLAUDE.md" "$S/ws/MAIN.md" +assert_eq "запись через симлинк в worktree" "$(rc "$S/ws" Write "$S/ws/CLAUDE.md")" 0 +assert_eq "запись через симлинк в main" "$(rc "$S/ws" Write "$S/ws/MAIN.md")" 2 +assert_eq "пустой cwd и относительный путь — пропуск" "$(rc "" Write "a.md")" 0 +assert_eq "запись внутри .git/ — не рабочее дерево" "$(rc "$S/m" Bash 'echo x >> .git/info/exclude')" 0 +assert_eq "путь с пробелом в main" "$(rc "$S/m" Bash 'touch "a b.md"')" 2 +# финальное ревью: I-1, I-2, M-3, M-4, M-5 +assert_eq "checkout main && merge && push из worktree (I-1)" "$(rc "$S/wt" Bash 'git checkout main && git pull && git merge --no-ff feat/6-x && git push')" 2 +assert_eq "checkout master && commit (I-1)" "$(rc "$S/wt" Bash 'git checkout master && git commit -am x')" 2 +assert_eq "switch - && merge (I-1)" "$(rc "$S/wt" Bash 'git switch - && git merge x')" 2 +assert_eq "timeout 60 git push origin main (I-2)" "$(rc "$S/wt" Bash 'timeout 60 git push origin main')" 2 +assert_eq "nice -n 5 git commit на main (I-2)" "$(rc "$S/m" Bash 'nice -n 5 git commit -m x')" 2 +ln -s "$S/m" "$S/ws/mlink"; ln -s /etc/hosts "$S/m/LINK.md" +assert_eq "rm симлинка в workspace на main-репозиторий (M-3)" "$(rc "$S/ws" Bash "rm $S/ws/mlink")" 0 +assert_eq "ln -sfn поверх симлинка в workspace (M-3)" "$(rc "$S/ws" Bash "ln -sfn $S/m $S/ws/mlink")" 0 +assert_eq "rm симлинка внутри main, указывающего наружу (M-3)" "$(rc "$S/m" Bash 'rm LINK.md')" 2 +assert_grep "путь Edit с [скобками] не режется (M-4)" 'docs/\[x\]\.md' <(err "$S/ws" Write "$S/m/docs/[x].md") +assert_eq "push текущей ветки через подстановку из worktree (M-5)" "$(rc "$S/wt" Bash 'git push -u origin "$(git branch --show-current)"')" 0 +assert_eq "push --tags с main (M-5)" "$(rc "$S/m" Bash 'git push --tags')" 0 +# сбой разбора и сообщение +assert_eq "не JSON — пропуск" "$(printf 'not json' | bash "$G" 2>/dev/null; echo $?)" 0 +assert_eq "незакрытая кавычка — пропуск" "$(rc "$S/m" Bash 'echo "x > f')" 0 +mkdir -p "$TMP/nopy" +assert_eq "нет python3 — пропуск" "$(env PATH="$TMP/nopy" /bin/bash "$G" <<< "$(j "$S/m" Write "$S/m/a.md")" 2>/dev/null; echo $?)" 0 +m=$(err "$S/ws" Write "$S/m/docs/HARNESS.md") +assert_grep "сообщение: префикс" '^BLOCKED \[guard-main\]:' <(printf '%s' "$m") +assert_grep "сообщение: путь отн. корня" 'docs/HARNESS.md' <(printf '%s' "$m") +assert_grep "сообщение: корень и worktree" "$(basename "$S")/m на main.*worktree" <(printf '%s' "$m") +assert_grep "settings.hooks.json регистрирует guard-main" 'guard-main\.sh' "$ROOT/hooks/settings.hooks.json" diff --git a/tests/test-hook-parse.sh b/tests/test-hook-parse.sh new file mode 100644 index 0000000..2b69216 --- /dev/null +++ b/tests/test-hook-parse.sh @@ -0,0 +1,104 @@ +# harness_target.py targets: разбор команды Bash на цели записи, без файловой системы (спека hook-guards). +P="$ROOT/hooks/harness_target.py" +assert_file "harness_target.py существует" "$P" +t(){ python3 "$P" targets "$1" /r 2>&1; } +NL=$'\n' +# чтение и дескрипторы +assert_eq "grep с текстом «npm i» — целей нет" "$(t 'grep -rn "npm i\|npm install" docs | head')" "" +assert_eq "2>/dev/null и 2>&1 — не цели" "$(t 'ls x 2>/dev/null; git fetch origin --prune 2>&1 | tail -3')" "" +assert_eq ">&2 и 1>&- — не цели" "$(t 'echo x >&2; echo y 1>&-')" "" +assert_eq "регулярка с > в кавычках" "$(t "python3 -c 're.search(r\"\\d>{1,2}\", s); mkdir'")" "" +assert_eq "комментарий" "$(t 'echo x # > f')" "" +# редиректы +assert_eq "редирект" "$(t 'echo x > notes.md')" "file /r/notes.md >" +assert_eq "дозапись по абсолютному пути" "$(t 'echo x >> /tmp/a.log')" "file /tmp/a.log >>" +assert_eq "stderr в файл" "$(t 'make 2>build.log')" "file /r/build.log 2>" +assert_eq "echo 2 > f — 2 это аргумент" "$(t 'echo 2 > f')" "file /r/f >" +assert_eq "&> в файл" "$(t 'cmd &> out.log')" "file /r/out.log &>" +assert_eq "heredoc: цель — файл, тело не разбирается" "$(t "cat > CLAUDE.md <<'EOF'${NL}a > b${NL}rm -rf x${NL}EOF${NL}echo ok")" "file /r/CLAUDE.md >" +# команды записи +assert_eq "tee -a" "$(t 'echo x | tee -a a.log b.log')" "file /r/a.log tee${NL}file /r/b.log tee" +assert_eq "cp — назначение" "$(t 'cp a b dst/')" "file /r/dst cp" +assert_eq "cp -t" "$(t 'cp -t out a b')" "file /r/out cp" +assert_eq "mv — источник и назначение" "$(t 'mv a b')" "file /r/a mv${NL}file /r/b mv" +assert_eq "rm -rf" "$(t 'rm -rf build')" "file /r/build rm" +assert_eq "chmod — без режима" "$(t 'chmod +x run.sh')" "file /r/run.sh chmod" +assert_eq "mkdir -p .wt/" "$(t 'mkdir -p .wt/6-x')" "file /r/.wt/6-x mkdir" +assert_eq "glob — каталог до метасимвола" "$(t 'rm docs/*.md')" "file /r/docs rm" +assert_eq "sed -i" "$(t "sed -i 's/a/b/' x.md")" "file /r/x.md sed -i" +assert_eq "sed -i '' (BSD)" "$(t "sed -i '' 's/a/b/' x.md")" "file /r/x.md sed -i" +assert_eq "sed -i -e, два файла" "$(t 'sed -i -e s/a/b/ x.md y.md')" "file /r/x.md sed -i${NL}file /r/y.md sed -i" +assert_eq "sed -n — чтение" "$(t 'sed -n 1,5p x.md')" "" +assert_eq "perl -pi -e" "$(t "perl -pi -e 's/a/b/' x")" "file /r/x perl -i" +assert_eq "dd of=" "$(t 'dd if=a of=/r/b.img')" "file /r/b.img dd" +assert_eq "tar -x -C" "$(t 'tar -xzf a.tgz -C out')" "file /r/out tar -x" +assert_eq "tar c — не запись" "$(t 'tar czf a.tgz src')" "" +assert_eq "unzip -d" "$(t 'unzip a.zip -d out')" "file /r/out unzip" +assert_eq "unzip -l — чтение" "$(t 'unzip -l a.zip')" "" +assert_eq "npm install — текущий каталог" "$(t 'npm install')" "file /r npm install" +assert_eq "npm -g install — не репозиторий" "$(t 'npm -g install x')" "" +assert_eq "pip install — не репозиторий" "$(t 'pip install requests')" "" +assert_eq "openspec archive" "$(t 'openspec archive x')" "file /r openspec archive" +assert_eq "openspec validate — чтение" "$(t 'openspec validate x --strict')" "" +assert_eq "docker … sh -c — не локальная запись" "$(t "docker compose exec -T db sh -c 'psql > /tmp/x'")" "" +assert_eq "префиксы sudo/env/присваивания" "$(t 'sudo rm /etc/x; env A=1 touch f; A=1 B=2 touch g')" "file /etc/x rm${NL}file /r/f touch${NL}file /r/g touch" +assert_eq "xargs rm — цели неизвестны" "$(t 'xargs rm < list')" "file - rm" +assert_eq "if/then" "$(t 'if [ -f x ]; then rm x; fi')" "file /r/x rm" +# каталог +assert_eq "cd && запись" "$(t 'cd /w && echo x > f')" "file /w/f >" +assert_eq "cd относительный ;" "$(t 'cd sub; touch f')" "file /r/sub/f touch" +assert_eq "( cd ) не протекает" "$(t '(cd /w; touch a); touch b')" "file /w/a touch${NL}file /r/b touch" +assert_eq "cd в конвейере не протекает" "$(t 'cd /w | cat; touch b')" "file /r/b touch" +assert_eq "pushd/popd" "$(t 'pushd /w && touch a && popd && touch b')" "file /w/a touch${NL}file /r/b touch" +assert_eq "cd - — каталог неизвестен" "$(t 'cd - && touch a')" "file - touch" +assert_eq "bash -c разбирается" "$(t "bash -c 'echo x > f'")" "file /r/f >" +assert_eq "eval разбирается" "$(t 'eval "rm x"')" "file /r/x rm" +# переменные и HOME +assert_eq "цель в переменной" "$(t 'echo x > "$OUT"')" "file - >" +assert_eq "\$HOME раскрывается" "$(t 'echo x > $HOME/f')" "file $HOME/f >" +assert_eq "~ раскрывается" "$(t 'echo x > ~/f')" "file $HOME/f >" +assert_eq "ln в переменную" "$(t 'ln -sfn ../../fixtures "$WT/fixtures"')" "file - ln" +# git +assert_eq "git add && commit" "$(t 'git add -A && git commit -m x')" "git /r add${NL}git /r commit" +assert_eq "git -C" "$(t 'git -C /w commit -m x')" "git /w commit" +assert_eq "git -C в переменной" "$(t 'for r in a b; do git -C "$r" commit -m x; done')" "git - commit" +assert_eq "git --git-dir" "$(t 'git --git-dir=/x/.git commit -m x')" "git - commit" +assert_eq "worktree, pull, fetch, log — не мутации" "$(t 'git worktree add ../w -b f; git pull --ff-only; git fetch; git log')" "" +assert_eq "stash list/show — чтение" "$(t 'git stash list; git stash show')" "" +assert_eq "stash — мутация" "$(t 'git stash')" "git /r stash" +assert_eq "checkout -b — смена ветки" "$(t 'git checkout -b f')" "switch /r f" +assert_eq "checkout -- пути" "$(t 'git checkout -- a.txt')" "git /r checkout --" +assert_eq "checkout main — смена ветки (I-1)" "$(t 'git checkout main && git merge x')" "switch /r main${NL}git /r merge" +assert_eq "checkout ветки фичи — не событие" "$(t 'git checkout feat/x')" "" +assert_eq "switch - — ветка неизвестна (I-1)" "$(t 'git switch - && git merge x')" "switch /r -${NL}git /r merge" +assert_eq "timeout 60 прячет голову (I-2)" "$(t 'timeout 60 git push origin main')" "git /r push>main" +assert_eq "timeout -s KILL 5 (I-2)" "$(t 'timeout -s KILL 5 rm x')" "file /r/x rm" +assert_eq "nice -n 10 (I-2)" "$(t 'nice -n 10 git commit -m x')" "git /r commit" +assert_eq "sudo -u bob (I-2)" "$(t 'sudo -u bob touch f')" "file /r/f touch" +assert_eq "env -C меняет каталог команды (I-2)" "$(t 'env -C /w git commit -m x')" "git /w commit" +assert_eq "xargs -n 1 rm (I-2)" "$(t 'xargs -n 1 rm < list')" "file - rm" +assert_eq "push --tags — не ветка (M-5)" "$(t 'git push --tags')" "" +assert_eq "push текущей ветки через \$(git branch --show-current) (M-5)" "$(t 'git push -u origin "$(git branch --show-current)"')" "git /r push" +assert_eq "push текущей ветки через rev-parse (M-5)" "$(t 'git push origin $(git rev-parse --abbrev-ref HEAD)')" "git /r push" +assert_eq "switch -c && commit" "$(t 'git switch -q -c feat/x && git commit -m y')" "switch /r feat/x${NL}git /r commit" +assert_eq "apply --check — чтение" "$(t 'git apply --check p.diff')" "" +assert_eq "apply — мутация" "$(t 'git apply p.diff')" "git /r apply" +assert_eq "commit с heredoc в подстановке" "$(t "git commit -m \"\$(cat <<'EOF'${NL}fix: it's (done)${NL}EOF${NL})\"")" "git /r commit" +# push +assert_eq "push без refspec" "$(t 'git push')" "git /r push" +assert_eq "push origin HEAD" "$(t 'git push -u origin HEAD')" "git /r push" +assert_eq "push ветки фичи" "$(t 'git push -u origin feat/x')" "" +assert_eq "push --delete ветки фичи" "$(t 'git push origin --delete feat/x')" "" +assert_eq "push HEAD:main" "$(t 'git push origin HEAD:main')" "git /r push>main" +assert_eq "push --delete main" "$(t 'git push origin --delete main')" "git /r push>main" +assert_eq "push --all" "$(t 'git push --all')" "git /r push --all" +assert_eq "push в переменную" "$(t 'git push origin "$BR"')" "git /r push?" +assert_eq "push feat/\$ID — префикс фичи" "$(t 'git push origin feat/$ID-x')" "" +# граничные случаи (Review Focus) +assert_eq "путь с пробелом в кавычках" "$(t 'touch "a b.md"')" "file /r/a b.md touch" +assert_eq "арифметика \$((…)) не ломает разбор" "$(t 'echo $((1+2)) > f')" "file /r/f >" +assert_eq "cd ~/x" "$(t 'cd ~/x && touch f')" "file $HOME/x/f touch" +assert_eq "git -C относительный после cd" "$(t 'cd /w && git -C sub commit -m x')" "git /w/sub commit" +assert_eq "перенос строки обратным слешем" "$(t "git add a \\${NL} b && git commit -m x")" "git /r add${NL}git /r commit" +# сбой разбора +assert_grep "незакрытая кавычка — error" '^error' <(t 'echo "x') diff --git a/tests/test-hook-require-plan.sh b/tests/test-hook-require-plan.sh new file mode 100644 index 0000000..624db5f --- /dev/null +++ b/tests/test-hook-require-plan.sh @@ -0,0 +1,35 @@ +# require-plan.sh: корень, ветка, code-paths, .bounded и планы — из репозитория правимого файла (спека hook-guards). +H="$ROOT/hooks/require-plan.sh" +S="$TMP/rp"; mkdir -p "$S/out" +mk(){ git init -q "$1" && git -C "$1" symbolic-ref HEAD "refs/heads/${2:-main}" && git -C "$1" commit -q --allow-empty -m init; } +mk "$S/a"; touch "$S/a/.bounded" # A: есть .bounded +mk "$S/b" # B: ни плана, ни .bounded +mk "$S/f"; git -C "$S/f" worktree add -q "$S/wt" -b feat/6-x; touch "$S/wt/.bounded" +mk "$S/c"; mkdir -p "$S/c/.claude"; printf 'hooks/ # код\n\nskills/\n' > "$S/c/.claude/code-paths" +mk "$S/p2" feat/6-x; mkdir -p "$S/p2/docs/superpowers/plans" +echo "Ветка: feat/6-x" > "$S/p2/docs/superpowers/plans/2026-10-16-a.md"; echo "другое" > "$S/p2/docs/superpowers/plans/2026-10-11-b.md" +mk "$S/p0" feat/7-y; mkdir -p "$S/p0/docs/superpowers/plans" +echo "Ветка: feat/6-x" > "$S/p0/docs/superpowers/plans/a.md"; echo "другое" > "$S/p0/docs/superpowers/plans/b.md" +mk "$S/p1" feat/7-y; mkdir -p "$S/p1/docs/superpowers/plans"; echo "x" > "$S/p1/docs/superpowers/plans/a.md" +mk "$S/pd"; mkdir -p "$S/pd/docs/superpowers/plans"; echo a > "$S/pd/docs/superpowers/plans/a.md"; echo b > "$S/pd/docs/superpowers/plans/b.md" +git -C "$S/pd" checkout -q --detach +j(){ python3 -c 'import json,sys; print(json.dumps({"cwd":sys.argv[1],"tool_name":"Edit","tool_input":{"file_path":sys.argv[2]}}))' "$@"; } +rc(){ j "$@" | bash "$H" 2>/dev/null; echo $?; } +assert_eq "файл в B без плана при cwd в A с .bounded" "$(rc "$S/a" "$S/b/src/a.py")" 2 +assert_eq "файл в worktree с .bounded при cwd на main" "$(rc "$S/f" "$S/wt/src/a.py")" 0 +assert_eq "src на глубине без плана" "$(rc "$S/b" "$S/b/api/src/a.py")" 2 +assert_eq "имя файла с [скобками] в src без плана (M-4)" "$(rc "$S/b" "$S/b/src/[slug].ts")" 2 +assert_eq "не код — пропуск" "$(rc "$S/b" "$S/b/docs/a.md")" 0 +assert_eq "вне репозитория — пропуск" "$(rc "$S/b" "$S/out/src/a.py")" 0 +assert_eq "code-paths: hooks/ без плана" "$(rc "$S/a" "$S/c/hooks/x.sh")" 2 +assert_eq "code-paths: src/ не защищён" "$(rc "$S/c" "$S/c/src/a.py")" 0 +assert_eq "два плана, один с веткой" "$(rc "$S/p2" "$S/p2/src/a.py")" 0 +assert_eq "два плана без ветки" "$(rc "$S/p0" "$S/p0/src/a.py")" 2 +assert_eq "один план — пропуск" "$(rc "$S/p1" "$S/p1/src/a.py")" 0 +assert_eq "detached HEAD при планах — пропуск" "$(rc "$S/pd" "$S/pd/src/a.py")" 0 +assert_eq "не JSON — пропуск" "$(printf 'not json' | bash "$H" 2>/dev/null; echo $?)" 0 +assert_eq "без file_path — пропуск" "$(printf '{"cwd":"%s","tool_input":{}}' "$S/b" | bash "$H" 2>/dev/null; echo $?)" 0 +m=$(j "$S/a" "$S/b/src/a.py" | bash "$H" 2>&1 >/dev/null) +assert_grep "сообщение: префикс и путь" '^BLOCKED \[require-plan\]: правка src/a.py' <(printf '%s' "$m") +assert_grep "сообщение: .bounded репозитория файла" "$(basename "$S")/b/\.bounded" <(printf '%s' "$m") +assert_grep "settings.hooks.json регистрирует require-plan" 'require-plan\.sh' "$ROOT/hooks/settings.hooks.json" diff --git a/tests/test-install.sh b/tests/test-install.sh index aec06bb..8fa93dd 100644 --- a/tests/test-install.sh +++ b/tests/test-install.sh @@ -8,6 +8,9 @@ assert_file "skills/role/SKILL.md установлен" "$CC/skills/role/SKILL.m assert_file "agents/briefing.md установлен" "$CC/agents/briefing.md" assert_file "hooks/role-hint.sh установлен" "$CC/hooks/role-hint.sh" assert_eq "hooks/role-hint.sh исполняемый" "$([ -x "$CC/hooks/role-hint.sh" ] && echo x)" "x" +assert_file "hooks/harness_target.py установлен" "$CC/hooks/harness_target.py" +assert_file "корпус хуков установлен" "$CC/skills/harness-doctor/fixtures/hook-cases.jsonl" +assert_file "раннер корпуса установлен" "$CC/skills/harness-doctor/scripts/hook_corpus.py" assert_file "harness/link-roles.sh установлен" "$CC/harness/link-roles.sh" assert_file "harness/project-template/workspace" "$CC/harness/project-template/workspace/CLAUDE.md" assert_file "harness/project-template/role-repo" "$CC/harness/project-template/role-repo/ROLE.md" @@ -19,6 +22,7 @@ out=$(HOME="$TH" CLAUDE_CONFIG_DIR="$CLAUDE_CONFIG_DIR" bash "$ROOT/install.sh" assert_grep "--check: roles" 'roles/' <(printf '%s' "$out") assert_grep "--check: skill role" 'skills/role' <(printf '%s' "$out") assert_grep "--check: agent briefing" 'agents/briefing' <(printf '%s' "$out") +assert_grep "--check: harness_target.py" 'ok +hooks/harness_target\.py' <(printf '%s' "$out") rm "$CC/agents/briefing.md" out=$(HOME="$TH" CLAUDE_CONFIG_DIR="$CLAUDE_CONFIG_DIR" bash "$ROOT/install.sh" --check 2>&1) assert_grep "--check: MISS briefing" 'MISS +.*briefing' <(printf '%s' "$out")