diff --git a/CHANGELOG.md b/CHANGELOG.md index 66b0832..be0b921 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.5.0 — 2026-10-23 +- **`install.sh` ставит allow-правила цикла фичи** (#24; пересмотр решения владельца 2). Список — `global/permissions.json`: `git worktree`, `git branch -m/-D`, `git push -u origin feat/…`/`chore/…`, `git push origin --delete`, `tea issues`, `tea pulls create/list/edit`, `tea comments`. Merge и push в main в список не входят. Правила только дописываются в `permissions.allow` файла `~/.claude/settings.json`, чужие не трогаются; копия списка — `~/.claude/harness/permissions.json`. `--no-permissions` пропускает шаг. `--check` и `doctor.sh` показывают `permissions обвязки: N/M` и недостающие правила. +- `adopt-project.sh` и `new-project.sh` печатают строку `push --delete` для установки с `--no-permissions`. WARN проверки в `doctor.sh` подсказывает `bash install.sh`. +- `EnterWorktree` в список не входит. Вход в worktree вне `/.claude/worktrees/` спрашивает подтверждение при любых allow-правилах (Claude Code ≥ 2.1.206); перенос worktree — #25. +- harness-doctor: агент чинит `install.sh --no-permissions`, права ставит владелец. + ## 0.4.2 — 2026-10-23 - Удаление ветки фичи на сервере (чеклист закрытия, п. 4; B3, RT-3, решение владельца 2): `doctor.sh` в разделе «Проект» ищет allow-правило для `git push origin --delete` в `.claude/settings.json`, `settings.local.json` проекта и в глобальном `settings.json` — `ok` с файлом или `WARN` с готовой строкой `"Bash(git push origin --delete:*)"`. Строку печатают `adopt-project.sh` и `new-project.sh` во всех режимах (раньше — только `adopt --workspace`); файл настроек они не пишут, правило добавляет владелец (#22). - `HARNESS.md`: шаг 10 — чеклист из 8 пунктов в `roles/dev.md` и рецепт п. 4. `roles/dev.md`, п. 4: `git worktree remove` раньше `git branch -D` (ветку, выведенную в worktree, git не удаляет). diff --git a/CLAUDE.md b/CLAUDE.md index 7cf488b..fd9e41b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,14 +6,14 @@ ## Стек и ограничения - 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` с меткой времени. +- `install.sh` идемпотентен: повторный запуск не ломает `settings.json` и делает бэкапы `CLAUDE.md` и `settings.json` с меткой времени. В `settings.json` он сливает `hooks` и только дописывает `permissions.allow` из `global/permissions.json`; чужие правила не трогает. Агент запускает его только с `--no-permissions`: права ставит владелец. - Хуки работают из `~/.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 по цели записи (файл, цели команды 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`. +- `global/CLAUDE.md` — глобальные правила процесса, ставятся в `~/.claude/CLAUDE.md`. `global/permissions.json` — allow-правила цикла фичи (без merge и push в main), `install.sh` дописывает их в `~/.claude/settings.json` и кладёт копию в `~/.claude/harness/` для `doctor.sh`. `project-template/` — `CLAUDE.md`, `CLAUDE.harness-section.md` (секция «OpenSpec и superpowers», по ней `harness-check` узнаёт подключённый проект), `ARCHITECTURE.md`, `gitignore`. - `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`. @@ -23,8 +23,8 @@ ## Команды - `bash tests/run.sh [имя]` — весь набор тестов. Зелёным считается только весь набор. -- `bash install.sh --check` — проверка окружения: бинарники, плагины, конфликты. -- `bash install.sh` — установка глобальной части, идемпотентно. +- `bash install.sh --check` — проверка окружения: бинарники, плагины, конфликты, allow-правила обвязки. +- `bash install.sh [--no-permissions]` — установка глобальной части, идемпотентно; `--no-permissions` — без allow-правил. - `bash skills/harness-doctor/scripts/doctor.sh [проект]` — отчёт и самотест хуков. Только чтение. - `bash adopt-project.sh <папка> [--no-merge] [--push]` — подключить существующий проект. Из терминала, не из Claude Code. - `bash new-project.sh <папка> [ssh-url]` — каркас нового проекта. @@ -42,7 +42,8 @@ - Сделано: B5+B6 часть 1 — слой ролей (PR #4, 0.3.0). Часть 2 (awp-ca) — issue #2, отложенные находки — issue #5. - Сделано: B1+B2 и отмена исключения openspec-only для guard-main — хуки по цели записи и корпус хуков (issue #6, PR #12, 0.4.0), спека — `openspec/specs/hook-guards/`. Отложенные находки — issue #13. - Сделано: B3 — allow-правило `git push origin --delete` печатают adopt/new-project, проверяет doctor (issue #22, 0.4.2). B4 закрыт в #6 (решение владельца 1). B10 — workspace-хук awp-ca, в части 2 issue #2. -- Дальше: B12. +- Сделано: allow-правила цикла фичи ставит `install.sh` (issue #24, 0.5.0; пересмотр решения владельца 2). +- Дальше: worktree в `.claude/worktrees/` (issue #25), затем B12. ## OpenSpec и superpowers - Вход в любую задачу — superpowers:brainstorming. Skills openspec-propose и openspec-explore сами по себе не запускать. diff --git a/README.md b/README.md index 314e540..5743a3d 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,8 @@ hooks/settings.hooks.json блок PreToolUse для ~/.claude/settings.json caveman/config.json defaultMode: off → ~/.config/caveman/config.json project-template/ CLAUDE.md, ARCHITECTURE.md, gitignore для нового проекта examples/ реальный CLAUDE.md проекта-пилота -install.sh установка глобальной части, идемпотентно; --check проверяет окружение +global/permissions.json allow-правила цикла фичи (worktree, tea, push фичевых веток, удаление веток) → ~/.claude/settings.json +install.sh установка глобальной части, идемпотентно; --check проверяет окружение; --no-permissions — без allow-правил new-project.sh каркас нового проекта: git, CLAUDE.md, openspec init, codegraph init, remote roles/.md общий слой ролей dev/admin/analyst/qa → ~/.claude/roles/ skills/role/SKILL.md /role : загрузить роль, память, запустить briefing → ~/.claude/skills/role/ @@ -82,7 +83,9 @@ claude mcp add-json -s user codegraph '{"type":"stdio","command":"codegraph","ar # 3. Глобальная часть bash install.sh --check -bash install.sh +bash install.sh # дописывает в ~/.claude/settings.json allow-правила из global/permissions.json; + # чужие правила не трогает. Без них: bash install.sh --no-permissions. + # Нет tea pulls merge и push в main: merge — по вашей команде. # 4. Проверка хука: в любом репозитории на main запустить claude и попросить создать файл. # Ожидается BLOCKED и предложение создать worktree. diff --git a/adopt-project.sh b/adopt-project.sh index bc2dcae..5c76215 100755 --- a/adopt-project.sh +++ b/adopt-project.sh @@ -17,7 +17,7 @@ if [ "${1:-}" = "--workspace" ]; then Workspace $NAME подключён. Дальше: 1. Заполни таблицу репозиториев в CLAUDE.md и перезапусти: bash $HERE/link-roles.sh $(pwd) - 2. Разреши агенту удалять ветки на сервере (чеклист закрытия, п. 4) — строка в .claude/settings.json → permissions.allow: + 2. Если обвязка ставилась с --no-permissions: разреши агенту удалять ветки на сервере (чеклист закрытия, п. 4) — строка в .claude/settings.json → permissions.allow: "Bash(git push origin --delete:*)" 3. Сессии: claude -n "dev …" | "admin …" | "qa …" | "analyst …" из этого каталога. MSG @@ -73,7 +73,7 @@ fi cat <` из worktree фичи, затем из основного checkout `git worktree remove <путь>` и `git branch -D <ветка>`. guard-main с 0.4.0 эту команду не блокирует, но её может отклонить классификатор auto mode. Поэтому в проекте нужно allow-правило: строка `"Bash(git push origin --delete:*)"` в `.claude/settings.json` → `permissions.allow`. Строку добавляете вы (решение владельца 2); `adopt-project.sh` и `new-project.sh` её печатают, `doctor.sh` показывает, найдена ли она. +**Удаление ветки фичи (п. 4 закрытия).** `git push origin --delete <ветка>` из worktree фичи, затем из основного checkout `git worktree remove <путь>` и `git branch -D <ветка>`. guard-main с 0.4.0 эту команду не блокирует, но её может отклонить классификатор auto mode. Поэтому нужно allow-правило `"Bash(git push origin --delete:*)"`. Его вместе с остальными правилами цикла фичи (`global/permissions.json`) ставит `bash install.sh` в `~/.claude/settings.json`. Запускаете его вы: агент себе права не выдаёт. Если обвязка стоит с `--no-permissions`, строку добавляете в `.claude/settings.json` проекта сами: её печатают `adopt-project.sh` и `new-project.sh`, а `doctor.sh` показывает, найдена ли она. + +**Подтверждения.** В обычном режиме Claude Code спрашивает подтверждение на всё, что не покрыто allow-правилами. В auto mode решает классификатор: он пропускает или запрещает, но вас не спрашивает. Allow-правила проверяются раньше него. Вход в worktree вне `/.claude/worktrees/` спрашивает подтверждение при любых правилах, перенос worktree — #25. **Bounded-задача** (флаг, фикс) идёт короче: дизайн в чате, файл `.bounded` в корне worktree (пропускает хук require-plan), TDD, коммит, PR. Без OpenSpec change и без плана. diff --git a/docs/retro/2026-10-11.md b/docs/retro/2026-10-11.md index 392db16..54ebbee 100644 --- a/docs/retro/2026-10-11.md +++ b/docs/retro/2026-10-11.md @@ -173,6 +173,8 @@ requesting-code-review 0 раз. Закрытие и ревью идут по ч «в main только через PR» становится без исключений. (B4 → переформулирован.) 2. **Удаление серверных веток — allow-правило в настройках проекта.** Строку правила владелец добавляет сам; обвязка даёт готовую строку в `adopt-project.sh` и в отчёте `doctor.sh`. Чеклист закрытия п. 4 остаётся за агентом. (B3.) + *Пересмотрено 23.10.2026 (#24): правило вместе с остальными allow-правилами цикла фичи ставит `install.sh` + в `~/.claude/settings.json`; отказ — `--no-permissions`, тогда строку по-прежнему печатают adopt/new-project и doctor.* 3. **codegraph — оставить и замерить.** Один срез с обязательным codegraph и подсчётом вызовов `codegraph_explore` против Bash/Read в тех же задачах; решение после замера. Метрика входит в `/harness-retro`. (B8 → замер, не вывод из потока.) 4. **Память — политика «состояние задачи в Gitea, в память только уроки, фидбек и знания роли» и одноразовая чистка diff --git a/global/permissions.json b/global/permissions.json new file mode 100644 index 0000000..15b8fe7 --- /dev/null +++ b/global/permissions.json @@ -0,0 +1,17 @@ +{ + "permissions": { + "allow": [ + "Bash(git worktree:*)", + "Bash(git branch -m:*)", + "Bash(git branch -D:*)", + "Bash(git push -u origin feat/:*)", + "Bash(git push -u origin chore/:*)", + "Bash(git push origin --delete:*)", + "Bash(tea issues:*)", + "Bash(tea pulls create:*)", + "Bash(tea pulls list:*)", + "Bash(tea pulls edit:*)", + "Bash(tea comments:*)" + ] + } +} diff --git a/install.sh b/install.sh index 17f0228..a7cbea5 100755 --- a/install.sh +++ b/install.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash # claude-harness: установка глобальной части (CLAUDE.md, хуки, конфиг caveman, слияние settings.json). -# Идемпотентно. Запуск: bash install.sh | проверка окружения: bash install.sh --check +# Идемпотентно. Запуск: bash install.sh [--no-permissions] | проверка окружения: bash install.sh --check +# --no-permissions — не дописывать allow-правила цикла фичи (global/permissions.json) в settings.json. set -euo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" CC="${CLAUDE_CONFIG_DIR:-$HOME/.claude}" @@ -24,6 +25,23 @@ check() { if [ -f "$CC/settings.json" ] && grep -q 'codegraph prompt-hook' "$CC/settings.json"; then echo " WARN codegraph prompt-hook в settings.json — рекомендуется отключить на время пилота" fi + local pr; pr=$(python3 - "$CC/settings.json" "$HERE/global/permissions.json" <<'PY' 2>/dev/null +import json, sys +try: + s = json.load(open(sys.argv[1], encoding="utf-8")) +except Exception: + s = {} +have = (s.get("permissions") or {}).get("allow") or [] +need = json.load(open(sys.argv[2], encoding="utf-8"))["permissions"]["allow"] +miss = [r for r in need if r not in have] +print("%d/%d %s" % (len(need) - len(miss), len(need), " ".join(miss))) +PY +) || true + case "$pr" in + "") echo " WARN permissions обвязки: не прочитаны ($CC/settings.json)" ;; + *" ") echo " ok permissions обвязки: ${pr% }" ;; + *) echo " WARN permissions обвязки: ${pr%% *}, нет: ${pr#* } — bash install.sh (или осознанно --no-permissions)" ;; + esac echo "== Установленная часть обвязки ($CC)" 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 \ @@ -33,7 +51,14 @@ check() { return $ok } -if [ "${1:-}" = "--check" ]; then check; exit $?; fi +PERMS=1 +for a in "$@"; do + case "$a" in + --check) check; exit $? ;; + --no-permissions) PERMS=0 ;; + *) echo "ERR: неизвестный аргумент $a (есть --check, --no-permissions)"; exit 1 ;; + esac +done mkdir -p "$CC/hooks" "$HOME/.config/caveman" stamp=$(date +%Y%m%d-%H%M%S) @@ -50,7 +75,7 @@ cp "$HERE/hooks/guard-main.sh" "$HERE/hooks/require-plan.sh" "$HERE/hooks/harnes # 2a. Скрипты бутстрапа проекта — агент запускает их из сессии mkdir -p "$CC/harness" rm -rf "$CC/harness/project-template" -cp -R "$HERE/adopt-project.sh" "$HERE/new-project.sh" "$HERE/link-roles.sh" "$HERE/project-template" "$CC/harness/" +cp -R "$HERE/adopt-project.sh" "$HERE/new-project.sh" "$HERE/link-roles.sh" "$HERE/project-template" "$HERE/global/permissions.json" "$CC/harness/" chmod +x "$CC/harness/"*.sh # 2b. Skill диагностики mkdir -p "$CC/skills" @@ -94,6 +119,22 @@ json.dump(s, open(sp,"w"), ensure_ascii=False, indent=2) print("settings.json: hooks merged") PY +# 5. settings.json: allow-правила цикла фичи — только дописываются, чужие не трогаются +if [ "$PERMS" = 1 ]; then + python3 - "$SETTINGS" "$HERE/global/permissions.json" <<'PY' +import json, sys +sp, pp = sys.argv[1], sys.argv[2] +s = json.load(open(sp)); need = json.load(open(pp))["permissions"]["allow"] +allow = s.setdefault("permissions", {}).setdefault("allow", []) +new = [r for r in need if r not in allow] +allow.extend(new) +json.dump(s, open(sp, "w"), ensure_ascii=False, indent=2) +print("settings.json: permissions +%d%s" % (len(new), (" (" + ", ".join(new) + ")") if new else " (все на месте)")) +PY +else + echo "settings.json: permissions пропущены (--no-permissions)" +fi + 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/new-project.sh b/new-project.sh index a8e80c4..1d534bd 100755 --- a/new-project.sh +++ b/new-project.sh @@ -32,7 +32,7 @@ if [ "${1:-}" = "--workspace" ]; then Workspace $NAME готов. Дальше: 1. Заполни {{...}} в CLAUDE.md и в .claude/roles/dev.md; в репозиториях направлений — .claude/roles/.md. 2. Заведи репозитории направлений в Gitea и добавь remote (tea repos create …; git remote add origin …). - 3. Разреши агенту удалять ветки на сервере (чеклист закрытия, п. 4) — строка в .claude/settings.json → permissions.allow: + 3. Если обвязка ставилась с --no-permissions: разреши агенту удалять ветки на сервере (чеклист закрытия, п. 4) — строка в .claude/settings.json → permissions.allow: "Bash(git push origin --delete:*)" 4. cd $(pwd) && claude -n "dev …" MSG @@ -70,7 +70,7 @@ cat <` — всегда отдельным PR (ветка `chore/archive-` из main, `tea pulls create`, merge по команде человека). Прямых коммитов в main нет, исключений нет (решение владельца 11.10.2026). 2. Issue закрывается (`tea issues close `, комментарий со ссылкой на PR) только если все её пункты выполнены. Если PR закрывает часть пунктов — issue остаётся открытой, в неё пишется комментарий: какие маркеры закрыты, каким PR и коммитом. 3. Удалить SDD workspace (`.superpowers/sdd//`). - 4. Удалить ветку фичи на сервере и локально: `git push origin --delete <ветка>` выполняется из worktree на ветке фичи (не из main-checkout), разрешён allow-правилом проекта (`Bash(git push origin --delete:*)` в `.claude/settings.json`; строку добавляет владелец, её печатают `adopt-project.sh` и `new-project.sh`, есть ли она — показывает `doctor.sh`); затем из основного checkout `git worktree remove`, потом `git branch -D`. Остальные worktree не трогать. Обходы через API Gitea запрещены. + 4. Удалить ветку фичи на сервере и локально: `git push origin --delete <ветка>` выполняется из worktree на ветке фичи (не из main-checkout), разрешён allow-правилом `Bash(git push origin --delete:*)` (его ставит `install.sh` владельца; при `--no-permissions` — строка в `.claude/settings.json` проекта, её печатают `adopt-project.sh` и `new-project.sh`, есть ли она — показывает `doctor.sh`; агент права себе не добавляет); затем из основного checkout `git worktree remove`, потом `git branch -D`. Остальные worktree не трогать. Обходы через API Gitea запрещены. 5. Обновить основной checkout: дерево чистое — `git pull --ff-only`; есть незакоммиченные изменения — не трогать, перечислить их в итоговом сообщении. 6. Все находки со статусом `deferred` собрать в одну issue `Follow-ups from #`: маркер, одна строка, откуда взялось. Нет отложенного — issue не создавать. 7. Файл auto-memory задачи, если был, свернуть в строку `registry-closed.md` (issue, PR, follow-up, дата); уроки из него перенести в `notes/` направления. diff --git a/skills/harness-doctor/SKILL.md b/skills/harness-doctor/SKILL.md index 357fe01..92130b6 100644 --- a/skills/harness-doctor/SKILL.md +++ b/skills/harness-doctor/SKILL.md @@ -27,6 +27,7 @@ description: Диагностика обвязки Claude Code (claude-harness) - `WARN в окружении есть *_API_KEY` → ключ виден всем сабагентам и делает интеграционные тесты платными при каждом `npm test`. Убрать из профиля shell, передавать только в строке запуска. Это находка для пользователя, агент сам профиль не правит. - Глобальный CLAUDE.md без «маркировка: да» или «projectPath: да» → старая версия, `install.sh`. - `MISS roles/.md`, `MISS skill role`, `MISS agent briefing`, `MISS link-roles.sh` → `bash <пакет>/install.sh` (версия ≥ 0.3.0). +- `WARN permissions обвязки: N/M, нет: …` или `MISS permissions обвязки` → allow-правила цикла фичи (`global/permissions.json`) не стоят в `~/.claude/settings.json`. Это находка владельцу: `bash <пакет>/install.sh` запускает он сам. Агент себе права не выдаёт: так он обошёл бы отказ классификатора auto mode. Если обвязка ставилась с `--no-permissions` осознанно, WARN ожидаем. - `WARN глобальный CLAUDE.md > 3 КБ` → в `~/.claude/CLAUDE.md` дописано лишнее; процесс роли живёт в `~/.claude/roles/.md`, перенести туда. **Самотест хуков** — любое `WARN … (ожидалось N)` означает, что хук установлен, но не делает того, что должен. Причины по убыванию частоты: правлен руками; `python3` не в PATH сессии; settings.json зовёт другой путь. Чинится `install.sh`; если и после него WARN — приложить строку к находке, это дефект пакета. @@ -42,13 +43,13 @@ description: Диагностика обвязки Claude Code (claude-harness) - «code-paths: умолчание» при коде вне `src/` → создать `.claude/code-paths`. - «SDD workspace: …» при отсутствии активной фичи → хвост незакрытой фичи; проверить чеклист закрытия. - `WARN .bounded не в .gitignore` → добавить строку, иначе уедет в main. -- `WARN нет allow-правила для git push origin --delete` → находка владельцу: готовую строку из WARN он сам добавляет в `.claude/settings.json` проекта (решение владельца 2, чеклист закрытия п. 4); агент `settings.json` не правит. `ok allow …` называет файл, где правило найдено: проектный `settings.json`, `settings.local.json` или глобальный. +- `WARN нет allow-правила для git push origin --delete` → находка владельцу (чеклист закрытия п. 4). Правило ставит `bash install.sh`. При `--no-permissions` готовую строку из WARN владелец сам добавляет в `.claude/settings.json` проекта. Агент `settings.json` не правит. `ok allow …` называет файл, где правило найдено: проектный `settings.json`, `settings.local.json` или глобальный. - `WARN tea не отвечает` → `tea logins list`; токен нужен со scopes read:user, write:repository, write:issue. - «workspace (не git)» → режим нескольких репозиториев. `WARN … битая ссылка` → репозиторий направления переименован или файл удалён: `bash ~/.claude/harness/link-roles.sh `. `WARN нет /.claude/roles/.md` → создать проектный файл роли из `~/.claude/harness/project-template/role-repo/ROLE.md`. - `WARN индекс auto-memory > 3 КБ` → индекс снова стал журналом состояния; свернуть закрытое в `registry-closed.md`, уроки — в `notes/` направления (политика памяти в глобальном CLAUDE.md). ## Чинится само (без вопроса) -`install.sh`, `link-roles.sh` в workspace, `codegraph init` в worktree, `.claude/code-paths`, строка в `.gitignore` на ветке. Всё остальное — находка пользователю: профиль shell, плагины, токены, удаление чего-либо. +`install.sh --no-permissions` (агент так обновляет хуки и файлы; права ставит владелец своим `bash install.sh`), `link-roles.sh` в workspace, `codegraph init` в worktree, `.claude/code-paths`, строка в `.gitignore` на ветке. Всё остальное — находка пользователю: профиль shell, плагины, токены, права, удаление чего-либо. ## Чего не делать Не «улучшать» хуки по месту: их источник — пакет claude-harness, правка идёт туда и ставится install.sh. Не отключать хук ради того, чтобы «прошло». diff --git a/skills/harness-doctor/scripts/doctor.sh b/skills/harness-doctor/scripts/doctor.sh index dc11409..d19c0c3 100755 --- a/skills/harness-doctor/scripts/doctor.sh +++ b/skills/harness-doctor/scripts/doctor.sh @@ -34,7 +34,7 @@ PY ) while IFS= read -r line; do case "$line" in "bad "*) info "${line#bad } — не JSON, allow-правила не прочитаны" ;; esac; done <<< "$r" case "$r" in *"ok "*) ok "allow git push origin --delete (${r##*ok })" ;; - *) warn "нет allow-правила для git push origin --delete (закрытие, п. 4) — строка в .claude/settings.json → permissions.allow: \"Bash(git push origin --delete:*)\"" ;; esac + *) warn "нет allow-правила для git push origin --delete (закрытие, п. 4) — bash install.sh или строка в .claude/settings.json → permissions.allow: \"Bash(git push origin --delete:*)\"" ;; esac } echo "## harness-doctor $(date +%F' '%T)" @@ -56,6 +56,25 @@ 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" +if [ -f "$CC/harness/permissions.json" ]; then # allow-правила цикла фичи, которые ставит install.sh + pr=$(python3 - "$CC/settings.json" "$CC/harness/permissions.json" <<'PY' 2>/dev/null +import json, sys +try: + s = json.load(open(sys.argv[1], encoding="utf-8")) +except Exception: + s = {} +have = (s.get("permissions") or {}).get("allow") or [] if isinstance(s, dict) else [] +need = json.load(open(sys.argv[2], encoding="utf-8"))["permissions"]["allow"] +miss = [r for r in need if r not in have] +print("%d/%d %s" % (len(need) - len(miss), len(need), " ".join(miss))) +PY +) + case "$pr" in + "") warn "permissions обвязки: не прочитаны ($CC/harness/permissions.json)" ;; + *" ") ok "permissions обвязки: ${pr% }" ;; + *) warn "permissions обвязки: ${pr%% *}, нет: ${pr#* } — bash install.sh (или осознанно --no-permissions)" ;; + esac +else miss "permissions обвязки (harness/permissions.json — bash install.sh)"; fi rmiss=(); for r in dev admin analyst qa; do [ -f "$CC/roles/$r.md" ] || rmiss+=("$r"); done if [ ${#rmiss[@]} -eq 0 ]; then ok "roles/ (dev admin analyst qa)"; else for r in "${rmiss[@]}"; do miss "roles/$r.md"; done; fi [ -f "$CC/skills/role/SKILL.md" ] && ok "skill role" || miss "skill role (~/.claude/skills/role/SKILL.md)" diff --git a/tests/test-doctor.sh b/tests/test-doctor.sh index 3f23224..d1b75ff 100644 --- a/tests/test-doctor.sh +++ b/tests/test-doctor.sh @@ -4,7 +4,7 @@ CC="$TMP/cc"; mkdir -p "$CC/hooks" "$CC/roles" "$CC/skills/role" "$CC/agents" "$ 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" +cp "$ROOT/link-roles.sh" "$ROOT/adopt-project.sh" "$ROOT/global/permissions.json" "$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" # workspace с одной целой и одной битой ссылкой W="$TMP/ws"; mkdir -p "$W/admin/.claude/roles" "$W/.claude/roles" "$W/.claude/agents" @@ -26,6 +26,7 @@ assert_grep "самотест: Write на main" 'ok +guard-main: Write на main 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 "машина: правил обвязки нет — WARN с подсказкой install.sh (#24)" 'WARN +permissions обвязки: 0/[0-9]+.*bash install\.sh' <(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") @@ -53,6 +54,14 @@ printf '{"permissions":{"allow":["Bash(git push origin --delete:*)"]},"hooks":{} out=$(CLAUDE_CONFIG_DIR="$CC" HOME="$TMP/home" bash "$D" "$W" 2>&1) assert_grep "workspace: правило в глобальном settings.json — ok" 'ok +allow git push origin --delete \(.*/cc/settings\.json\)' <(printf '%s' "$out") cp "$TMP/settings.bak" "$CC/settings.json" +# правила обвязки (#24): все на месте — ok; нет harness/permissions.json — MISS +python3 -c 'import json,sys; s=json.load(open(sys.argv[1])); s["permissions"]=json.load(open(sys.argv[2]))["permissions"]; json.dump(s,open(sys.argv[1],"w"))' "$CC/settings.json" "$ROOT/global/permissions.json" +out=$(CLAUDE_CONFIG_DIR="$CC" HOME="$TMP/home" bash "$D" "$W" 2>&1) +assert_grep "машина: правила обвязки на месте — ok" 'ok +permissions обвязки: [0-9]+/[0-9]+' <(printf '%s' "$out") +rm "$CC/harness/permissions.json" +out=$(CLAUDE_CONFIG_DIR="$CC" HOME="$TMP/home" bash "$D" "$W" 2>&1) +assert_grep "машина: нет harness/permissions.json — MISS" 'MISS +permissions обвязки' <(printf '%s' "$out") +cp "$TMP/settings.bak" "$CC/settings.json" rm -f "$CC/roles/qa.md" out=$(CLAUDE_CONFIG_DIR="$CC" HOME="$TMP/home" bash "$D" "$W" 2>&1) assert_grep "машина: MISS roles/qa.md" 'MISS +roles/qa.md' <(printf '%s' "$out") diff --git a/tests/test-install.sh b/tests/test-install.sh index 8fa93dd..22ac560 100644 --- a/tests/test-install.sh +++ b/tests/test-install.sh @@ -1,6 +1,6 @@ # install.sh: новые файлы ставятся, повторный запуск не дублирует хуки, --check их видит. TH="$TMP/home"; CLAUDE_CONFIG_DIR="$TH/.claude"; mkdir -p "$CLAUDE_CONFIG_DIR" -echo '{"hooks":{"SessionStart":[{"matcher":"startup","hooks":[{"type":"command","command":"\"$HOME/.claude/hooks/harness-check.sh\""}]}]}}' > "$CLAUDE_CONFIG_DIR/settings.json" +echo '{"permissions":{"allow":["Bash(echo:*)"]},"hooks":{"SessionStart":[{"matcher":"startup","hooks":[{"type":"command","command":"\"$HOME/.claude/hooks/harness-check.sh\""}]}]}}' > "$CLAUDE_CONFIG_DIR/settings.json" HOME="$TH" CLAUDE_CONFIG_DIR="$CLAUDE_CONFIG_DIR" bash "$ROOT/install.sh" >/dev/null 2>&1; HOME="$TH" CLAUDE_CONFIG_DIR="$CLAUDE_CONFIG_DIR" bash "$ROOT/install.sh" >/dev/null 2>&1 CC="$CLAUDE_CONFIG_DIR" for r in dev admin analyst qa; do assert_file "roles/$r.md установлен" "$CC/roles/$r.md"; done @@ -26,3 +26,25 @@ assert_grep "--check: harness_target.py" 'ok +hooks/harness_target\.py' <(printf 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") +# allow-правила цикла фичи (#24): ставятся из global/permissions.json, чужие не трогаются, --no-permissions — отказ +P="$ROOT/global/permissions.json" +assert_file "global/permissions.json есть" "$P" +assert_file "harness/permissions.json установлен" "$CC/harness/permissions.json" +perm(){ python3 -c 'import json,sys; s=json.load(open(sys.argv[1])); a=s.get("permissions",{}).get("allow",[]); r=json.load(open(sys.argv[2]))["permissions"]["allow"]; print(sum(a.count(x)==1 for x in r), len(r), a.count("Bash(echo:*)"), len(a)-len(set(a)))' "$1" "$P"; } +read -r have total own dup <<< "$(perm "$CC/settings.json")" +assert_eq "список правил обвязки прочитан и не пуст" "$([ "${total:-0}" -gt 0 ] 2>/dev/null && echo да)" "да" +assert_eq "правила обвязки в settings.json — все, по одному разу после двух установок" "$have" "$total" +assert_eq "чужое правило сохранено" "$own" "1" +assert_eq "дублей в permissions.allow нет" "$dup" "0" +assert_eq "в списке нет merge и push в main" "$(grep -cE 'pulls merge|push origin main|"Bash\(git push:\*\)"|"Bash\(git:\*\)"|"Bash"' "$P")" "0" +assert_eq "EnterWorktree в списке нет: allow-правило не снимает запрос (#25)" "$(grep -c 'EnterWorktree' "$P")" "0" +out=$(HOME="$TH" CLAUDE_CONFIG_DIR="$CLAUDE_CONFIG_DIR" bash "$ROOT/install.sh" --check 2>&1) +assert_grep "--check: правила обвязки на месте" 'ok +permissions обвязки: [0-9]+/[0-9]+' <(printf '%s' "$out") +T2="$TMP/home2"; C2="$T2/.claude"; mkdir -p "$C2"; echo '{}' > "$C2/settings.json" +out=$(HOME="$T2" CLAUDE_CONFIG_DIR="$C2" bash "$ROOT/install.sh" --no-permissions 2>&1); rc=$? +assert_eq "--no-permissions: exit 0" "$rc" "0" +assert_grep "--no-permissions: говорит, что правила пропущены" 'permissions.*--no-permissions' <(printf '%s' "$out") +read -r have total own dup <<< "$(perm "$C2/settings.json")" +assert_eq "--no-permissions: правил обвязки не добавлено" "$have" "0" +assert_file "--no-permissions: хуки всё равно ставятся" "$C2/hooks/guard-main.sh" +assert_grep "--no-permissions: --check предупреждает" 'WARN +permissions обвязки: 0/' <(printf '%s' "$out") diff --git a/tests/test-workspace.sh b/tests/test-workspace.sh index 2904dca..868371f 100644 --- a/tests/test-workspace.sh +++ b/tests/test-workspace.sh @@ -49,4 +49,5 @@ A="$TMP/adopt"; git init -q -b main "$A" && git -C "$A" commit -q --allow-empty out=$(bash "$ROOT/adopt-project.sh" "$A" 2>&1); rc=$? assert_eq "adopt (старый режим): git-репозиторий → exit 0" "$rc" "0" assert_grep "adopt (старый режим): печатает allow-правило (B3)" '"Bash\(git push origin --delete:\*\)"' <(printf '%s' "$out") +assert_grep "adopt: строка — для установки с --no-permissions (#24)" 'no-permissions' <(printf '%s' "$out") assert_eq "adopt (старый режим): settings.json не создаёт" "$([ -e "$A/.claude/settings.json" ] && echo есть || echo нет)" "нет"