From 2fd2396c5cd18f2c03c644cfdfa7d564f6fb7f40 Mon Sep 17 00:00:00 2001 From: "andrey.epifantsev" Date: Fri, 16 Oct 2026 01:59:26 +0400 Subject: [PATCH] =?UTF-8?q?feat:=20install/doctor=20=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D0=B2=D1=8F=D1=82=20=D0=B8=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D1=8F=D1=8E=D1=82=20=D1=8F=D0=B4=D1=80=D0=BE=20=D0=B8=20?= =?UTF-8?q?=D0=BA=D0=BE=D1=80=D0=BF=D1=83=D1=81=20=D1=85=D1=83=D0=BA=D0=BE?= =?UTF-8?q?=D0=B2;=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=200.4.0=20(#6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 10 ++++++++++ CLAUDE.md | 12 +++++++----- README.md | 5 +++-- docs/HARNESS.md | 8 +++++--- install.sh | 7 ++++--- roles/dev.md | 2 +- skills/harness-doctor/SKILL.md | 2 ++ skills/harness-doctor/scripts/doctor.sh | 12 +++++++++++- tests/test-doctor.sh | 8 +++++++- tests/test-install.sh | 4 ++++ 10 files changed, 54 insertions(+), 16 deletions(-) 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 cc298a8..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,12 +28,14 @@ - `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` — статистика сессий и блокировок хуков для ретро; `hook_blocks_export.py <каталог> [...]` — выгрузка блокировок в JSONL (сырьё для тестов хуков). +- `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. Находки ретро маркируются `RT-n`. 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/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/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/scripts/doctor.sh b/skills/harness-doctor/scripts/doctor.sh index 587acc2..d4ce09d 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,8 +62,14 @@ 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)" +echo "## Проект:$(cd "$DIR" 2>/dev/null && pwd)" if root=$(git -C "$DIR" rev-parse --show-toplevel 2>/dev/null); then main=$(git -C "$root" rev-parse --git-common-dir 2>/dev/null); main=$(cd "$root" && cd "$(dirname "$main")" 2>/dev/null && pwd) info "git root: $root$( [ "$main" != "$root" ] && echo " (worktree; основной: $main)")" diff --git a/tests/test-doctor.sh b/tests/test-doctor.sh index 5c03860..4f3630e 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,11 @@ 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 "проект: режим 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-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")