diff --git a/cspell-words.txt b/cspell-words.txt index f4211d3d..4cdc0e01 100644 --- a/cspell-words.txt +++ b/cspell-words.txt @@ -1863,3 +1863,6 @@ nohup сматчить тригернёт суппрессить +вокабуляр +Бypass +sess diff --git a/docs/observer/STATUS.md b/docs/observer/STATUS.md index 31218cc5..d58146ae 100644 --- a/docs/observer/STATUS.md +++ b/docs/observer/STATUS.md @@ -1,6 +1,6 @@ # Brain Status (auto-generated) -Last updated: 2026-05-29T01:44:49.392Z +Last updated: 2026-05-29T01:47:17.763Z | Контролёр | Состояние | Детали | |---|---|---| @@ -110,7 +110,7 @@ Episodes since last run: 542 / threshold: 10 | Фраза | За всё время | За сегодня | |---|---|---| | `recovery` | 901 | 4 | -| `ремонт инфраструктуры` | 186 | 1 | +| `ремонт инфраструктуры` | 192 | 7 ⚠️ | | `без скилов` | 185 | 7 ⚠️ | | `срочно` | 93 | 0 | | `memory dump` | 17 | 0 | diff --git a/docs/superpowers/specs/2026-05-28-router-gate-hard-wall-design.md b/docs/superpowers/specs/2026-05-28-router-gate-hard-wall-design.md new file mode 100644 index 00000000..bfa1f38a --- /dev/null +++ b/docs/superpowers/specs/2026-05-28-router-gate-hard-wall-design.md @@ -0,0 +1,333 @@ +# Router-gate hard wall — Дизайн-спецификация (Уровень 4) + +**Дата:** 2026-05-28 +**Автор:** Claude (controller Opus 4.7) под руководством заказчика Дмитрия +**Статус:** Approved by owner — готов к плану implementation +**Тип:** feature — enforcement architecture rewrite +**Предшественник:** [docs/superpowers/plans/2026-05-28-router-discipline-level-1-2.md](../plans/2026-05-28-router-discipline-level-1-2.md) (Уровни 1+2, merged ранее в день) + +--- + +## 1. Цель и контекст + +Закрыть все обходы (bypass paths) от controller'а (Claude) к роутеру (classifier-driven skill recommendations). После реализации controller физически не может обойти router-рекомендации без явного разрешения заказчика. + +### Триггер + +Brain-retro #10 trend analysis показал катастрофический рост override-events: **12 → 229 → 348 → 679** срабатываний за 4 дня (25-28.05.2026). Из них `recovery` 525 событий за один день. Это паттерн — controller использует override-вокабуляр как привычку, не аварийный клапан. Дисциплина без enforcement не держится. + +Self-retrospect #2 (28.05 вечер) подтвердил: 5 commitments утреннего self-retrospect сломаны 2 из 5 за 6 часов. Без enforcement-хука "привычку не удержать" — этот тезис теперь подтверждён двумя независимыми данными. + +### Что было до + +Архитектура enforcement до этого эпика: + +- 5 PreToolUse-хуков с per-rule override-фразами из общего vocab.json. +- 1 файл `tools/enforce-override-vocab.json` с 7 фразами обхода (`без скилов`, `direct ok`, `срочно`, `быстрый коммит`, `recovery`, `memory dump`, `ремонт инфраструктуры`). +- 1 хук `enforce-override-limit.mjs` со счётчиком per-day=5 на использование каждой фразы. +- Уровни 1+2 (merged этим утром) сузили scope `recovery` и `ремонт инфраструктуры`, добавили per-rate-window, понизили classifier-match threshold с 0.8 до 0.6, добавили inline router-skip с 50+ chars. + +Дырки которые Уровни 1+2 не закрыли: + +1. **Одна override-фраза снимает 5 PreToolUse хуков разом** — даже после сужения scope, фраза всё ещё мульти-suppressor. +2. **Router-skip как inline self-bypass** — controller сам себя освобождает от рекомендации (хоть и с 50+ chars обоснованием). +3. **Глобальный override-vocab по substring-match** — никакого user explicit consent required. + +### Что хочет заказчик + +«Hard wall. Никаких обходов. Разрешение на любой обход даёт только заказчик. Все router-рекомендации (одиночные и цепочки) — через явную санкцию заказчика. Любая неясная ситуация — controller ничего не делает сам, спрашивает заказчика с честными рекомендациями.» + +--- + +## 2. Принципы дизайна + +1. **Hard wall, no escape valves в коде.** Бypass возможен только через AskUserQuestion с явным выбором заказчика. Никаких inline phrases, substring-match overrides, self-bypass паттернов. + +2. **User approval everywhere для router output.** Каждая router-рекомендация (одиночный скил и цепочка) проходит через AskUserQuestion перед исполнением. Заказчик одобряет / выбирает альтернативу / останавливает. + +3. **Прямые поручения заказчика — без переспрашивания.** Когда заказчик в prompt'е явно указал скил (`/brain-retro`, «вызови writing-plans», «делай subagent-driven») — это direct invocation, не router-recommendation, без AskUserQuestion gate. + +4. **Read-only действия — всегда разрешены.** Read / Grep / Glob / LS / TodoWrite / read-only MCP / **AskUserQuestion** — безопасная база, никогда не блокируется. + +5. **Honest reasoning required.** Каждое AskUserQuestion включает мои честные рекомендации + объяснение причин. Никакого размытия, никаких рационализаций. + +6. **Recovery — explicit acknowledged risk.** Если gate ошибочно заблокирует и AskUserQuestion не сработает — единственный путь = ручная правка заказчиком файлов `.claude/settings.json` / `~/.claude/runtime/router-state-.json` / `~/.claude/runtime/chain-state-.json`. Заказчик соглашается быть recovery-каналом за хождение в hard wall. + +7. **All decisions logged.** Каждое срабатывание gate (allow / block / unlock) пишется в JSONL-файл для post-hoc анализа в brain-retro. + +--- + +## 3. Архитектура + +### Новый компонент: `tools/enforce-router-gate.mjs` + +Единственный PreToolUse-хук, регистрируется в `.claude/settings.json` с `matcher: ""` (все tools). Pure decision-функция + тонкая I/O обёртка. + +На каждый tool-call хук: + +1. Читает classifier-output последнего prompt'а из `~/.claude/runtime/router-state-.json` (уже существует — пишется при UserPromptSubmit роутером). +2. Читает chain-state из `~/.claude/runtime/chain-state-.json` (новый файл — какой шаг цепочки уже пройден across-turns). +3. Анализирует transcript текущего turn'а — был ли вызван AskUserQuestion или matching Skill/Task в этом ходе. +4. Резолвит набор «допустимых tools» по правилам Decision Flow (раздел 4). +5. Возвращает `decision: allow` (silent pass) или `decision: block` (с reason). +6. Пишет запись в `~/.claude/runtime/router-gate-decisions.jsonl`. + +### Удаляются как класс + +| Файл | Размер | Содержание | +|---|---|---| +| `tools/enforce-chain-recommendation.mjs` + test | ~370 lines | Chain enforcement, заменяется новым gate | +| `tools/enforce-classifier-match.mjs` + test | ~190 lines | Single-node enforcement, заменяется новым gate | +| `tools/enforce-graph-first.mjs` + test | ~150 lines | Graph-first rule, теперь через router recommendation | +| `tools/enforce-semgrep-security.mjs` + test | ~135 lines | Semgrep-security, теперь через router recommendation | +| `tools/enforce-override-limit.mjs` + test | ~150 lines | Override-счётчик, обходов больше нет | +| `tools/enforce-override-vocab.json` | ~80 lines | 7 фраз обходов | + +В `tools/enforce-hook-helpers.mjs` функции `findOverride`, `findOverrideAttempt`, `loadOverrideVocab` превращаются в stubs (возвращают null / null / `{phrases: []}`). Это сохраняет совместимость с 6 preserved хуками которые их вызывают, без правки их кода. Соответствующие unit-тесты переписываются под stub-поведение. + +Из `.claude/settings.json` снимаются регистрации удалённых хуков. + +### Сохраняются (с патчем на «без vocab») + +7 хуков с собственной семантикой (не про router-routing): + +| Хук | Семантика | Эффект удаления vocab | +|---|---|---| +| `enforce-tdd-gate` | Test перед production-code edit | Теряет escape-фразы `срочно` / `быстрый коммит` / `ремонт инфраструктуры`. Hard-lock — починка самого TDD-gate требует ручной правки settings.json заказчиком | +| `enforce-coverage-verify` | Stop-hook требует `coverage:` строку | Теряет `без скилов` / `direct ok`. Каждый ответ с tool_use требует coverage аннотацию | +| `enforce-memory-coverage` | Memory-write требует memory-sync coverage | Теряет `memory dump`. Каждое memory-редактирование требует явную аннотацию | +| `enforce-verify-before-push` | Свежий verify-sentinel перед commit/push | Теряет `срочно` / `быстрый коммит` / `ремонт инфраструктуры`. Hard-lock для починки verify-инфры | +| `enforce-rationalization-audit` | Soft detector, не блокирует | Не использует findOverride — нет эффекта | +| `enforce-prompt-injection` | Inject контекст-блок в UserPromptSubmit | Использует findOverride для surface активных overrides. После удаления — surface пустой | +| `enforce-branch-switch` | Block dangerous git-операции | Теряет `recovery`. Hard-lock для `git rebase` / `git reset --hard` — требует explicit `BRANCH-SWITCH-CONFIRMED` маркер в response каждый раз | + +Из 7 preserved хуков 6 используют `findOverride` (все кроме `enforce-rationalization-audit`). После удаления vocab.json эти вызовы будут возвращать `null` всегда — hooks работают корректно, никаких изменений в их коде не требуется. + +Это **explicit acknowledged risk** — заказчик соглашается что починка любого из 7 preserved хуков требует его ручного вмешательства. + +### Новый файл состояния + +`~/.claude/runtime/chain-state-.json`: + +``` +{ + "chain_active": ["#55", "#19", "#56"], + "chain_step": 1, + "initialized_at": "2026-05-28T20:00:00.000Z", + "last_step_at": "2026-05-28T20:15:30.000Z" +} +``` + +Инициализируется когда router выдаёт chain. Обновляется при matching skill invocation (chain_step++). Очищается когда `chain_step >= chain_active.length` или router даёт другую цепочку (обнуляется и переинициализируется). + +### Новый файл логирования + +`~/.claude/runtime/router-gate-decisions.jsonl`: + +``` +{"ts": "2026-05-28T20:00:01.123Z", "session_id": "abc", "tool_name": "Edit", "decision": "block", "reason": "recommendation #19 not invoked, AskUserQuestion not called yet", "state": {"rec_node": "#19", "rec_chain": [], "chain_step": 0, "askuser_called": false, "skill_invoked_matching": false, "is_direct_invocation": false}} +``` + +Append-only, ~1KB на запись × ~300 записей/день = ~300KB/день. Читается brain-retro и self-retrospect. + +--- + +## 4. Decision Flow (логика gate) + +Gate определяет одно из 4 поведений на основе state: + +### Поведение 1 — Прямое поручение заказчика (Direct invocation) + +Триггер: prompt начинается со slash-команды (`/brain-retro`, `/code-review`, etc), ИЛИ содержит явное `вызови X` / `делай subagent-driven` / `используй superpowers:writing-plans`. + +Детектор: regex поиск в `lastUserPromptText` для: + +- `^/[a-z0-9_-]+(\s|$)` — slash-command в начале prompt'а +- `(?:вызови|используй|делай|invoke)\s+(?:superpowers:)?[a-z0-9_-]+` — явное указание скила + +Если совпадение И tool-call матчит указанный скил → **allow** (без AskUserQuestion gate). +Если совпадение И tool-call НЕ матчит → **block** (заказчик указал X, а ты вызываешь Y). + +### Поведение 2 — Роутер дал одиночную рекомендацию + +Триггер: `rec_node !== null AND rec_chain.length === 0`. + +| Состояние turn'а | Tool | Решение | +|---|---|---| +| askuser_called=false, skill_invoked_matching=false | Read/Grep/Glob/LS/TodoWrite/AskUserQuestion/read-only MCP | allow (safe base) | +| askuser_called=false, skill_invoked_matching=false | Skill matching rec_node / Task subagent_type matching | allow + unlock turn | +| askuser_called=false, skill_invoked_matching=false | Edit/Write/MultiEdit/NotebookEdit/Bash/Skill non-matching/Task non-matching | block — «Router рекомендовал X, вызови AskUserQuestion с предложениями» | +| askuser_called=true, skill_invoked_matching=false | Любой tool | allow (заказчик уже ответил, выполняй выбранный путь) | +| skill_invoked_matching=true | Любой tool | allow (skill уже выполнен, продолжаем работу) | + +### Поведение 3 — Роутер дал цепочку + +Триггер: `rec_chain.length >= 1` (включая chain длины 1). + +Gate инициализирует chain-state (если не инициализирована) с `chain_active = rec_chain, chain_step = 0`. + +Если новый router-state даёт ДРУГОЙ rec_chain (не пустой и не равный текущему chain_active) — chain-state переинициализируется с новым chain. Если новый router-state даёт пустой rec_chain — текущая chain_state сохраняется (мы внутри уже идущей цепочки). + +`expected_node = chain_active[chain_step]`. + +| Состояние turn'а | Tool | Решение | +|---|---|---| +| askuser_called=false, expected_skill_invoked=false | Safe base | allow | +| askuser_called=false, expected_skill_invoked=false | Skill matching expected_node / Task subagent_type matching | allow + unlock turn + chain_step++ | +| askuser_called=false, expected_skill_invoked=false | Любой mutating tool / Skill non-matching | block — «Цепочка [...], сейчас ждём шаг N (X). Вызови AskUserQuestion для одобрения» | +| askuser_called=true | Любой tool | allow | +| expected_skill_invoked=true | Любой tool | allow + chain_step++ | +| chain_step >= chain_active.length | Любой tool | allow (chain complete, clear chain-state) | + +### Поведение 4 — Роутер молчит + +Триггер: `rec_node === null AND rec_chain.length === 0 AND chain-state empty`. + +| Состояние turn'а | Tool | Решение | +|---|---|---| +| askuser_called=false | Safe base | allow | +| askuser_called=false | Любой mutating tool (Edit/Write/Bash/Skill/Task) | block — «Роутер молчит. Вызови AskUserQuestion с 1/2+/0 форматом по количеству подходящих скилов» | +| askuser_called=true | Любой tool | allow (заказчик ответил, выполняй) | + +### AskUserQuestion-форматы для Поведения 4 + +| Сколько подходящих скилов вижу | Формат AskUserQuestion | +|---|---| +| 2 и больше | Перечисляю варианты, явно указываю который рекомендую + почему. Заказчик выбирает | +| Ровно 1 | Один вариант — спрашиваю «делать со скилом X или без него?» | +| Ни одного | Спрашиваю «продолжить direct без скила или остановиться?» | + +В **любом** AskUserQuestion (Поведения 2/3/4) controller обязан указать: + +- Что роутер рекомендовал (или что он молчит). +- Моя честная оценка — подходит ли рекомендация, если нет — почему. +- 3-4 варианта на выбор с цена каждого (плюсы / минусы). + +--- + +## 5. Безопасная база (всегда разрешено) + +Эти tools never blocked, любое поведение: + +- `Read` — просмотр файлов +- `Grep` — поиск по содержимому +- `Glob` — поиск по имени +- `LS` — список файлов +- `TodoWrite` — мой список задач (внутренний) +- `AskUserQuestion` — обращение к заказчику (ключевой канал) +- `ListMcpResourcesTool` — список MCP-ресурсов +- `ReadMcpResourceTool` — чтение MCP-ресурса +- Текстовый ответ (нет tool_use) + +--- + +## 6. Recovery при lockout + +### Базовый принцип + +Когда gate (или один из 7 preserved хуков) блокирует — controller **никогда** не принимает решение об обходе сам. Всегда один и тот же путь: + +1. Текстовый ответ — что заблокировано, какое правило, **честное** объяснение почему controller туда пришёл (без размытия — если controller сам нарушил правило, говорит прямо). +2. `AskUserQuestion` с 3-4 вариантами для заказчика: + - Дать explicit разрешение на конкретное действие. + - Указать другой путь. + - Остановить работу. +3. Ждать ответа. Не действовать дальше. + +### Если AskUserQuestion физически недоступен + +Три уровня заказчик-side intervention: + +**Уровень 1 — отключить конкретный хук.** В `.claude/settings.json` секции `PreToolUse` / `Stop` удалить запись про сломанный хук. Следующий ход — хук не сработает. + +**Уровень 2 — выключить ВСЕ хуки.** Переименовать `.claude/settings.json` → `.claude/settings.json.disabled`. + +**Уровень 3 — править router-state.** В `~/.claude/runtime/router-state-.json` заменить `recommended_node` и `recommended_chain` на null. Следующий tool-call gate увидит «молчание роутера» — пойдёт сценарий AskUserQuestion. + +--- + +## 7. Логирование и наблюдаемость + +### Новый источник данных + +`~/.claude/runtime/router-gate-decisions.jsonl` (формат — раздел 3). + +### Что теряется из brain-retro + +- **`override-usage.jsonl`** — пустеет. Hook enforce-override-limit удаляется. Таблица «Override events trend by day» исчезает. +- **`hook-outcomes.jsonl` Cut 11 в текущей форме** — теряется. Buckets (`blocked` / `passed-with-skill` / `passed-inline-override` / `passed-global-override` / `passed-short-chain` / `passed-no-mutating`) больше не пишутся. + +### Что искажается + +- **Brain-retro Table 5 «outcome × node_chosen group»** — bucket `direct_ignored_rec` становится 0 физически (gate блокирует). Интерпретация меняется: теперь это либо approved direct либо stop. +- **Factor matrix `decision_provenance`** — `autonomous` почти исчезает для mutating tools. Доминирует `user_chose_from_options`. Снижается аналитическая разнообразность. +- **Factor matrix `path_type`** — всё mutating становится `regulated`, `improvised` только для read-only. + +### Что добавляется в brain-retro + +**Table 11-new — Router-gate decision distribution.** Buckets: `allow-baseline` / `allow-after-skill-match` / `allow-after-askuser` / `allow-direct-invocation` / `block-no-rec-no-askuser` / `block-recommendation-bypass-attempt` / `block-chain-step-wrong` / `unlock-after-skill`. + +**Table 12-new — User approval patterns.** На AskUserQuestion в ответ на router-recommendation: `approved-as-recommended` / `approved-alternative-skill` / `approved-direct-no-skill` / `chose-stop`. Показывает — насколько часто я ловлю ошибку роутера vs насколько часто роутер прав. + +**Table 13-new — Lockout incidents.** Когда controller попал в lockout (gate заблокировал + AskUserQuestion → заказчик решил). Per incident: какой хук сработал, моя честная причина, решение заказчика. + +### Адаптация существующих таблиц и факторов + +**Table 5 переименовать в «outcome × approval-source».** Buckets: `skill-via-router-approved` / `skill-via-direct-invocation` / `direct-after-askuser-approval` / `read-only-no-gate`. + +**`decision_provenance` factor matrix** — добавить новые значения: + +- `user_approved_router_rec` (соглашение с роутером) +- `user_approved_alternative` (выбрал альтернативу) +- `user_approved_direct` (разрешил direct без скила) +- `user_directed` (явная команда заказчика) +- `autonomous` остаётся только для read-only и conversation + +**`path_type`** — добавить третье значение `gate_approved` (заказчик явно одобрил direct). + +### Migration of historical data + +`override-usage.jsonl` и `hook-outcomes.jsonl` остаются на диске как архив. Новый код их игнорирует. Brain-retro может опционально показывать «pre-migration baseline» как историческую справку. + +--- + +## 8. Этапы реализации (укрупнённо) + +Детальный план будет писаться отдельной сессией через `superpowers:writing-plans`. Канва: + +| Этап | Содержание | Часы | +|---|---|---| +| 1 — Подготовка инфраструктуры | Pure decision-модуль + I/O обёртка + chain-state persistence + decision logger + ~30 unit-тестов + smoke-тесты | 2-3 | +| 2 — Удаление 5 хуков + vocab | Удалить 5 .mjs + 5 .test.mjs + vocab.json (11 файлов). 3 helper-функции (findOverride / findOverrideAttempt / loadOverrideVocab) в enforce-hook-helpers.mjs оставить как stubs — возвращают null/empty всегда. Это сохраняет совместимость с 6 preserved хуками (tdd-gate / coverage-verify / memory-coverage / verify-before-push / prompt-injection / branch-switch) без правки их кода. Регрессия GREEN | 1-2 | +| 3 — Регистрация в settings.json | Добавить router-gate, снять регистрации удалённых хуков. Smoke-test полной сессии | 0.5 | +| 4 — Документация Recovery | Памятка для заказчика по 3 уровням recovery (отдельный doc) | 1 | +| 5 — Прогон в реальной работе | Несколько дней наблюдения, brain-retro #11 — проверить новые таблицы | (не таск) | +| 6 — Brain-retro adaptation | Обновить `brain-retro-analyzer.mjs` (parse router-gate-decisions, новые cut-функции). SKILL.md mandatory tables 11→13 | 1.5-2 | +| **Итого** | | **6-8.5** | + +### Риски миграции + +- **Этап 2 порядок-чувствительный** — сначала превращаем helper-функции в stubs (возвращают null/empty), потом удаляем vocab.json и тесты vocab. Если удалить vocab.json первым, тесты helpers упадут до stub-конверсии. +- **Этап 3 без отката** — если gate ломается, controller остаётся без всех PreToolUse-хуков (поставили новый, старые удалили). Mitigation — feature-branch + push до Этапа 3 для snapshot. + +--- + +## 9. Открытые вопросы (для плана implementation) + +Эти вопросы решаются на этапе writing-plans, не сейчас: + +- Точный формат AskUserQuestion message для каждого Поведения 2/3/4 — нужна шаблонная функция или жёсткие строки? +- Persistence chain-state при крэше Claude session — нужен периодический snapshot или достаточно текущего файла? +- Direct-invocation detection — точный regex для slash-команд и явных вызовов; покрытие угловых случаев («ну давай вызови ... что-то»). +- Логирование `direct_invocation` events — добавить отдельный bucket в decision log? +- Bash-команды как mutating — какой именно sub-pattern блокируем? Полное blacklist (`rm -rf` / `git push` / etc) или whitelist read-only (`git status` / `git log` / etc)? + +--- + +## 10. Cross-refs + +- Предшественник: [Уровни 1+2 план](../plans/2026-05-28-router-discipline-level-1-2.md) +- Brain-retro #10: [`docs/observer/notes/2026-05-28-brain-retro-10.md`](../../observer/notes/2026-05-28-brain-retro-10.md) +- Self-retrospect #2: [`docs/observer/notes/2026-05-28-self-retrospect-2.md`](../../observer/notes/2026-05-28-self-retrospect-2.md) +- Pravila §16 brain governance, §17 universal skill-coverage — будет обновляться отдельной задачей через `claude-md-management` после этого эпика. +- Текущая enforcement-архитектура: 5 PreToolUse хуков + vocab.json в `tools/`.