# Router-gate hard wall — Дизайн-спецификация (plan-ready)

**Дата:** 2026-05-29
**Версия:** v3.5 condensed (plan-ready)
**Источник:** [2026-05-28-router-gate-hard-wall-design.md](2026-05-28-router-gate-hard-wall-design.md) v3.5 (audit-history + version-narrative вырезаны для plan writing)
**Автор:** Claude (controller Opus 4.7) под руководством заказчика Дмитрия
**Статус:** Approved by owner — готов к плану implementation через `superpowers:writing-plans`
**Предшественник:** [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 28.05.2026)

---

## TL;DR

**Цель:** Закрыть все обходы router-рекомендаций. Controller (Claude) физически не может действовать без явного разрешения заказчика.

**Решение:** Один PreToolUse-хук `tools/enforce-router-gate.mjs` заменяет 5 старых хуков и vocab.json. На каждый tool call gate резолвит «разрешено / заблокировано» по 4 поведениям:

1. **Direct invocation** заказчика (strict whitelist: slash-cmd / `Skill(X)` / `используй #N` / `делай <exact-name>`) → allow.
2. **Single router-рекомендация** → allow только matching Skill/Task + read-only baseline; mutating требует AskUserQuestion approval.
3. **Chain router-рекомендация** → allow только текущий шаг chain (state persists across turns, TTL 24h); progression after matching skill invoked.
4. **Silence** (router молчит) → mutating tools блокируются до AskUserQuestion с 1/2+/0-формат предложениями.

**Безопасная база (всегда разрешено):** Read / Grep / Glob / LS / TodoWrite / AskUserQuestion (с лимитом 2 per turn) / read-only MCP. Bash — отдельный whitelist (§5.1).

**Ключевые защиты:**

- AskUserQuestion НЕ unlock'ает turn автоматически — gate парсит ответ заказчика и unlock'ает только для approved action.
- Protected paths (`~/.claude/runtime/*`, `tools/enforce-*.mjs`, etc) — hard-deny независимо от unlock.
- Subagent inheritance через env vars (`CLAUDE_GATE_INHERIT=true`), не text-prefix.
- Subagent constraints: нет AskUser, нет recursive Task, max 3 parallel.
- Subagent BLOCKED-protocol через side-channel `subagent-block-<tool-use-id>.json` (subagent gate-process пишет, parent gate читает).
- Dangerous git операции (rebase / reset --hard / clean -f / push --force / etc) — через AskUserQuestion с git-pattern (§4.5); approval one-shot через `approve_git_operation` в askuser-decisions.jsonl.
- Atomic file writes + `proper-lockfile` для race-free state.
- Gate budget 2s + fail-CLOSE при таймаутах.
- Bash blocks sub-shells (`backticks`, `$()`, `<()`, `<<` heredocs) + file-watcher для script execution + static content scan.

**Цена:** 22.5-32 часов implementation в 6 этапов через `superpowers:subagent-driven-development`. Wall-clock с параллелизмом: 6-9 часов.

**Recovery:** заказчик соглашается быть recovery-каналом ручной правкой `.claude/settings.json` / state-файлов при ошибочном lockout.

**Удаляется:** 5 хуков (chain-recommendation / classifier-match / graph-first / semgrep-security / override-limit) + `enforce-override-vocab.json` (7 фраз обхода больше не работают).

**Сохраняется:** 7 preserved хуков (tdd-gate / coverage-verify / memory-coverage / verify-before-push / rationalization-audit / prompt-injection / branch-switch). 6 из них теряют свои findOverride escape-фразы. Branch-switch переписан под AskUserQuestion-gate (см. §3 таблица сохраняемых).

**Class controller-writable signals закрыт архитектурно** через side-channel writes в protected paths (S5: subagent-block-file; S8: askuser-decisions с git-pattern).

**Audit 29.05.2026** ([audit report](../audits/2026-05-29-router-gate-condensed-adversarial-audit.md)) выявил 51 finding, из них 8 MUST-FIX закрыты inline (commit поверх d4f7e681; см. §5.1 `<<<` here-string + `node` REPL + per-arg path-deny + `<` redirect; §3.1 UNC strip + 8.3 expand + `$VAR` fail-CLOSE; §4 Поведение 1 source restriction; §8 Этап 2.3 sequencing). 5 SHOULD-FIX перенесены в writing-plans (см. §9 «Audit findings deferred»). 9 DOS findings + 15 INFO residuals документированы в audit report.

---

## 1. Цель и контекст

Закрыть все обходы (bypass paths) от controller'а (Claude) к роутеру (classifier-driven skill recommendations). После реализации controller физически не может обойти router-рекомендации без явного разрешения заказчика.

### Триггер

Brain-retro #10 показал катастрофический рост override-events: 12 → 229 → 348 → 679 срабатываний за 4 дня (25-28.05.2026). Self-retrospect #2 подтвердил: 5 commitments утреннего self-retrospect сломаны 2 из 5 за 6 часов. Без enforcement-хука дисциплину не удержать — два независимых доказательства.

### Дыры, которые Уровни 1+2 не закрыли

1. Одна override-фраза снимает 5 PreToolUse хуков разом.
2. Router-skip как inline self-bypass — controller сам себя освобождает от рекомендации.
3. Глобальный override-vocab по substring-match — никакого user explicit consent.

### Что хочет заказчик

«Hard wall. Никаких обходов. Разрешение на любой обход даёт только заказчик. Все router-рекомендации (одиночные и цепочки) — через явную санкцию заказчика. Любая неясная ситуация — controller ничего не делает сам, спрашивает заказчика с честными рекомендациями.»

---

## 2. Принципы дизайна

1. **Hard wall, no escape valves в коде.** Bypass возможен только через AskUserQuestion с явным выбором заказчика. Никаких inline phrases, substring-match overrides, self-bypass паттернов.
2. **User approval everywhere для router output.** Каждая router-рекомендация (одиночный скил и цепочка) проходит через AskUserQuestion перед исполнением.
3. **Прямые поручения заказчика — без переспрашивания.** `/brain-retro`, «вызови writing-plans», «делай subagent-driven» — direct invocation, не router-recommendation.
4. **Read-only действия — всегда разрешены.** Read / Grep / Glob / LS / TodoWrite / read-only MCP / AskUserQuestion — безопасная база.
5. **Honest reasoning required.** Каждое AskUserQuestion включает мои честные рекомендации + объяснение причин.
6. **Recovery — explicit acknowledged risk.** Если gate ошибочно заблокирует и AskUserQuestion не сработает — единственный путь = ручная правка заказчиком файлов settings.json / state-файлов.
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-<sess>.json` (уже существует — пишется при UserPromptSubmit роутером).
2. Читает chain-state из `~/.claude/runtime/chain-state-<sess>.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 |
| `tools/enforce-classifier-match.mjs` + test | ~190 lines | Single-node enforcement |
| `tools/enforce-graph-first.mjs` + test | ~150 lines | Graph-first rule |
| `tools/enforce-semgrep-security.mjs` + test | ~135 lines | Semgrep-security |
| `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`. Каждый ответ требует coverage аннотацию |
| `enforce-memory-coverage` | Memory-write требует memory-sync coverage | Теряет `memory dump` |
| `enforce-verify-before-push` | Свежий verify-sentinel перед commit/push | Теряет `срочно` / `быстрый коммит` / `ремонт инфраструктуры` |
| `enforce-rationalization-audit` | Soft detector, не блокирует | Не использует findOverride — нет эффекта |
| `enforce-prompt-injection` | Inject контекст-блок в UserPromptSubmit | Использует findOverride для surface активных overrides. После удаления — surface пустой |
| `enforce-branch-switch` | Block dangerous git-операции (`git rebase` / `git reset --hard` / `git clean -f` / `git checkout --` / `git branch -D` / `git push --force` / `git stash drop` / `git cherry-pick` / `git revert`) | Теряет `recovery` override И controller-writable markers `BRANCH-SWITCH-CONFIRMED` / `RECOVERY-INTENT:`. Hard-lock: controller обязан AskUserQuestion с явным вопросом + 3 опциями; user approves через §4.5 answer parser (категория `approve_git_operation`); approval пишется в `askuser-decisions-<sess>.jsonl` (protected); hook читает exact-match + one-shot consume + 5-min time window |

Из 7 preserved хуков 6 используют `findOverride` (все кроме `enforce-rationalization-audit`). После удаления vocab.json эти вызовы возвращают `null` всегда — hooks работают корректно, изменений в их коде не требуется.

Это **explicit acknowledged risk** — заказчик соглашается что починка любого из 7 preserved хуков требует его ручного вмешательства.

### 3.1. Protected paths (hard-deny) + path normalization

Список путей **никогда** не разрешённых для Edit / Write / MultiEdit / NotebookEdit и Bash-команд rm / mv / cp / redirects (`>` / `>>`):

- `~/.claude/runtime/*` — все gate state-файлы: router-state, chain-state, askuser-decisions, gate-decisions, gate-errors, subagent-inheritance, coverage-hint, **`subagent-block-<tool-use-id>.json`** (subagent gate-process пишет block-events, parent gate читает после Task return; см. §3.4 + §10.2 schema), **`gate-config.json` тоже** (explicit, чтобы glob-интерпретация не разошлась).
- `.claude/settings.json` / `.claude/settings.local.json`
- `.claude/skills/**/SKILL.md` — skill definitions
- `tools/enforce-*.mjs` — все enforcement-хуки (включая `enforce-router-gate.mjs`)
- `tools/router-classifier.mjs` + dependents в `tools/router-*.mjs`
- `tools/router-gate-decide.mjs` + dependents (новые модули gate'а)
- `tools/enforce-hook-helpers.mjs` — содержит `sessionToolUses` / `findOverride` stubs / `logHookOutcome` — gate читает transcript через них. Если patched — falsify «askuser_called=true».
- `tools/subagent-prompt-prefix.mjs` — устанавливает env vars для субагента (§3.2). Если patched — субагент стартует без inheritance flags → undefined behavior.
- `tools/registry-load.mjs` — gate использует для alias resolution (skill ↔ #N ↔ slug). Если patched — direct-invocation strict whitelist матчит fake aliases.
- `docs/registry/nodes.yaml` — Single Source of Truth для skill/node names. Если patched — fake skill добавляется и `делай <fake>` проходит.
- `package.json` / `composer.json` — npm-scripts (`npm test`, `npm run lint:*`) и composer-команды в Bash-whitelist. Если patched — `"test": "rm -rf ..."` runs evil под whitelist.

Hard-deny check выполняется **ПЕРВЫМ** в decision flow, до любой recommendation logic. Никакой unlock (Skill match / AskUserQuestion / direct invocation) не отменяет hard-deny. Изменения этих файлов — только через заказчика снаружи Claude.

**Path normalization:** перед сравнением с deny-list gate **канонизирует** путь:

1. **Strip Windows UNC long-path prefix** (closes audit CRITICAL-4): если path начинается с `\\?\` или `\\.\` — strip prefix перед дальнейшей normalization. UNC форма `\\?\C:\Users\...\.claude\runtime\state.json` нормализуется к `C:\Users\...\.claude\runtime\state.json` для match с protected glob.
2. `path.resolve(target)` — разрешает относительные пути и `..`.
3. **Expand Windows 8.3 short names** (closes audit CRITICAL-5): на Windows если path содержит `~N` segment (tilde+digit, например `ADMINI~1`), вызвать `GetLongPathName` WinAPI через native binding (`node-ffi-napi` или PowerShell `[System.IO.Path]::GetFullPath` fallback). Без раскрытия `C:\Users\ADMINI~1\.claude\runtime\state.json` не match protected list `C:\Users\Administrator\.claude\runtime\*`. Если WinAPI fails (file не существует, permission) → fail-CLOSE.
4. `fs.realpathSync(resolved)` — разрешает symlinks (если файл существует; если нет — используется resolved).
5. Expand `~` через `os.homedir()` и env vars через `process.env`. **Unresolved `$VAR` / `%VAR%` после expansion → fail-CLOSE** (closes audit CRITICAL-3: `~/.claude/runtime/$UNDEFINED/state.json` literal `$UNDEFINED` segment не match glob, must block). Allowed env vars whitelist: `HOME` / `USERPROFILE` / `APPDATA` / `LOCALAPPDATA` — все остальные `$VAR` references → fail-CLOSE.
6. Case-fold на Windows (`.toLowerCase()` для сравнения; на Linux/Mac case-sensitive).

Если canonicalization падает (file ENOENT) — fail-CLOSE для unknown-state файлов, fail-OPEN для проверок прав на чтение существующих. EACCES при realpathSync → fail-CLOSE (assume protected).

### 3.2.0. Pre-flight smoke-tests — REQUIRED BEFORE §3.2 IMPLEMENTATION

**Pre-flight tasks Этапа 2.1 в плане implementation.** Три независимых smoke-теста, все обязательны. **User-run methodology:** smoke-тесты НЕ запускаются controller'ом (тот может подтасовать env в PreToolUse-handler'е до Task spawn → fake PASS). Заказчик запускает в чистой сессии где нет моих хуков.

**Smoke 1 — env propagation:**

Acceptance criteria: заказчик запускает в чистой Claude session тестовый prompt: «спавн субагента через Task с subagent_type=Explore, спроси что у него в `process.env.CLAUDE_TEST_PROBE`. Я перед prompt'ом руками поставлю env через PowerShell `$env:CLAUDE_TEST_PROBE='42'`». Субагент в первом tool_use возвращает значение.

- **Pass:** `42` → env propagation работает, §3.2 реализуется по env-based design.
- **Fail:** undefined / другое значение → §3.2 design не работоспособен, **stop эпик** и открыть отдельную задачу «subagent inheritance через file-based handshake» (Plan B: `subagent-prompt-prefix.mjs` пишет inheritance-файл по детерминированному имени `~/.claude/runtime/subagent-inheritance-<parent-sess>-LATEST.json` + субагент читает по convention с mtime check + flock для concurrent Task'ов).

Альтернатива если заказчик не может — independent 3-way verification: (a) `process.env` check; (b) `argv` check (env vars иногда передаются как `--env=`); (c) file-based fallback probe одновременно с env-probe — несовпадение результатов = controller-manipulation.

**Smoke 2 — PostToolUse semantics:**

Acceptance criteria: заказчик запускает prompt с явно-failing skill invocation (например `Skill(superpowers:writing-plans)` с заведомо невалидным arg). Hook `PostToolUse` зарегистрирован на этот tool. Acceptance:

- **Pass A:** PostToolUse fires **только** на success — на failed skill НЕ срабатывает → chain-state S3 fix корректен.
- **Pass B:** PostToolUse fires всегда, но содержит `status: "error"` поле → §3 handler фильтрует по этому полю, fix корректен.
- **Fail:** PostToolUse fires всегда без status differentiation → chain_step++ инкрементнет на упавшем skill. Нужен redesign — chain_step++ через **PreToolUse следующего turn'а** проверяя что предыдущий skill_invoke реально завершился (по transcript output length / artifact files).

**Smoke 3 — subagent block-file write:**

Acceptance criteria: заказчик запускает prompt: «спавн субагента через Task tool с заданием `Write tools/router-gate-decide.mjs` (path в §3.1 protected) — gate должен subagent'у заблокировать Write». После subagent return:

- **Pass:** в `~/.claude/runtime/` появляется `subagent-block-<tool-use-id>.json` с записью block-event на Write. Parent gate в next response обязан escalate AskUser (видно в transcript). Acceptance: file exists + parent escalation visible.
- **Fail:** file missing после blocked Task → subagent gate-process не пишет block-file (либо не запускается в subagent-режиме, либо path-derivation broken). Degraded fallback — weak heuristic (tool_use count + marker). Не блокер для эпика, но S5 остаётся в residual.

Budget: 0.5h × 3 smoke-tests + verification = **1.5h total**. Without these — три architectural assumption неверифицированы → суммарный риск 5-15h rework.

### 3.2. Subagent gate inheritance (env-based, NOT text-prefix)

При вызове Task tool gate (PreToolUse):

1. Пишет `~/.claude/runtime/subagent-inheritance-<task-id>.json`:

   ```json
   {
     "parent_session_id": "<parent-id>",
     "allowed_actions": ["Skill(matching)", "Task(subagent_type=matching)", ...],
     "created_at": "<iso-ts>"
   }
   ```

   **Path-injection защита:** поля `parent_router_state_path` / `parent_chain_state_path` **удалены** из schema. Субагент gate **derives** оба пути **hardcoded pattern** из `parent_session_id`: `` path.join(os.homedir(), '.claude', 'runtime', `router-state-${parent_session_id}.json`) `` и аналогично для chain-state. Принимать произвольные absolute paths из inheritance-файла нельзя — контроллер мог бы записать фейковый router-state в `/tmp/fake-state.json` и указать его → субагент читает фейк → bypass.

2. Хук `subagent-prompt-prefix.mjs` устанавливает env vars для процесса субагента:
   - `CLAUDE_PARENT_SESSION_ID=<parent-id>`
   - `CLAUDE_GATE_INHERIT=true`
   - `CLAUDE_INHERITANCE_FILE=~/.claude/runtime/subagent-inheritance-<task-id>.json`

3. Gate-хук **тот же** `enforce-router-gate.mjs` (тот же `.claude/settings.json` наследуется субагентом) при старте проверяет:
   - Если `CLAUDE_GATE_INHERIT=true` И inheritance-файл существует и валиден → derives parent state paths hardcoded из `parent_session_id` и читает parent's router-state, chain-state.
   - Свой собственный router-state субагента игнорируется (пустой при старте — silence-режим был бы deadlock).
   - **Если `CLAUDE_GATE_INHERIT` undefined / inheritance-файл missing / malformed → fail-CLOSE** (block all mutating tools, reason: «subagent inheritance missing — escalate to parent»). Symmetric с §3.3 «fail-CLOSE при missing state».
   - Decisions субагента логируются в **parent's** `router-gate-decisions.jsonl` с префиксом `[subagent:<task-id>]`.

4. Inheritance-файл удаляется когда Task tool возвращает результат родителю.

**Block-file write:** subagent gate ТАКЖЕ пишет `~/.claude/runtime/subagent-block-<tool-use-id>.json` каждый раз когда decide=block для любого tool subagent'а. См. §3.4 — единая subagent-gate logic.

### 3.3. Failure modes (fail-CLOSE)

Когда gate не может определить состояние — block all mutating tools:

| Ошибка | Поведение | Reason в ответе |
|---|---|---|
| `router-state-<sess>.json` отсутствует | Trigger Поведение 4 (silence) → AskUserQuestion required | "Router state missing — escalate" |
| `router-state` malformed (JSON parse error) | Block ALL mutating | "Router state corrupt — re-prompt required" |
| `chain-state` malformed | Reset to empty + log warning | "Chain state corrupt — restarting fresh" |
| Gate сам падает (uncaught exception) | Log в `~/.claude/runtime/gate-errors.jsonl` + block ALL mutating | "Gate internal error — see logs" |
| Hard-deny path не доступен для проверки | Block (assume protected) | "Path-check failed — defaulting to deny" |

Никаких fail-open ради «работает хоть как-то».

### 3.4. Subagent constraints + BLOCKED-protocol через side-channel

Субагент работает с **ограниченным** set'ом по сравнению с parent:

- **`AskUserQuestion` НЕ в baseline субагента.** При попытке вызвать — gate субагента блокирует с reason: «Subagents cannot escalate to user — return BLOCKED status to parent».
- **Recursive Task запрещён.** При попытке вызвать Task внутри Task — gate блокирует. Глубина max 1.
- **Limit parallel Task:** max 3 параллельных Task tool calls в одном turn'е родителя. 4-й и далее → block с reason «parallel subagent limit reached».
- При lockout субагент возвращает Task tool response: `{status: "BLOCKED", reason: "<why>", parent_should_askuser: true}`. Родитель видит → сам эскалирует к заказчику через свой AskUserQuestion.

**BLOCKED-protocol через out-of-band block-file (закрытие class controller-writable signals):**

Сигнал переносится в side-channel который контроллер не контролирует. Controller-written response text fundamentally fake-able; side-channel write через subagent gate-process в protected runtime-файл — не fake-able.

**Механизм:**

1. **Parent gate при PreToolUse(Task)** запоминает `tool_use_id` (Anthropic-assigned для этого Task tool call — controller НЕ контролирует generation; уже используется в §3.2 для `subagent-inheritance-<tool-use-id>.json`).
2. **Subagent gate при decide=`block`** (любой tool, любое Поведение) — atomic append в `~/.claude/runtime/subagent-block-<tool-use-id>.json` запись `{ts, tool_name, reason, decision_kind}` ДО возврата block-decision в subagent process. proper-lockfile (§3.5) для concurrent blocks.
3. **Parent gate при PreToolUse(next tool после Task return)** — читает block-file. Decision:

| Состояние block-file | tool_use count subagent'а | Parent decision |
|---|---|---|
| File exists, blocks непустой | любое | **Block ALL mutating** до AskUser-эскалации с reason из первого block. Логировать в decisions.jsonl как `subagent_blocked_escalate` |
| File missing | > 0 | **DONE** — subagent работал без блоков. Продолжаем normal flow |
| File missing | == 0 | **Assume BLOCKED** (defensive, gate-crash possible). Parent обязан AskUser |
| File read FS error | любое | fail-CLOSE per §3.3 |

1. **Cleanup:** parent gate удаляет block-file после чтения. Background cleanup script (cron / lefthook): orphans > 24h → delete (защита от parent gate crash после read но до delete).

**Anti-forgery (3 защитных слоя — почему controller не подделает):**

- **Path derived из `tool_use_id`** — Anthropic CLI assigns при Task spawn. Controller передаёт `subagent_type` и `prompt`, но **не** tool_use_id. Невозможно заранее знать какой ID будет, чтобы pre-emptively написать fake-файл.
- **File в `~/.claude/runtime/*`** → protected per §3.1. Edit/Write/MultiEdit blocked hard-deny. Bash `rm`/`mv`/`cp` в hard-blacklist. Controller не может delete/modify/create.
- **Пишет gate-процесс subagent'а**, не controller-written response text. Gate — отдельный node-процесс с тем же `enforce-router-gate.mjs` (env-inherited через §3.2). Когда gate решает `block`, он напрямую writes файл, controller text-response это никак не контролирует.

**Adversarial check:**

| Attack | Защита |
|---|---|
| Edit `enforce-router-gate.mjs` чтобы отменить write | Protected paths §3.1 — blocked |
| Delete block-file до parent read | `~/.claude/runtime/*` protected — rm/mv в hard-blacklist, blocked |
| Pre-emptive fake block-file с пустыми blocks | tool_use_id assign'ит harness, controller не знает заранее; Write на protected — blocked anyway |
| Subagent gate crash до first write | Defensive: file missing + tool_use count == 0 → assume BLOCKED |
| Subagent сделал read-only работу + был blocked на Write | Block-file существует с 1 блоком → parent escalates корректно |
| Race: 2 параллельных block writes от subagent | proper-lockfile (§3.5) — sequential consistency |

### 3.5. Atomic state writes / file locking

Все state-файлы (`router-state-*.json`, `chain-state-*.json`, `askuser-decisions-*.jsonl`, `router-gate-decisions.jsonl`, `subagent-inheritance-*.json`, `subagent-block-*.json`) пишутся atomically:

**JSON state-файлы (one-shot writes):**

- Запись через `write-to-tmp + rename` pattern: `fs.writeFileSync(target + '.tmp', data)` затем `fs.renameSync(target + '.tmp', target)`. POSIX `rename` атомарен; на Windows — близко к атомарному.

**JSONL append-only логи:**

- `fs.appendFileSync(target, line)` — атомарен для writes <`PIPE_BUF` (4096 bytes на POSIX) если flag `O_APPEND` установлен. Каждая строка <300 bytes — fits.
- На Windows — appendFileSync через `O_APPEND` тоже atomic для small writes.

**File locking при параллельных tool calls:**

- Gate использует `proper-lockfile` npm package (cross-platform file locks).
- Lock acquired на entry в decide function, released на exit.
- Lock timeout 1s — если другой gate-call держит lock дольше → fail-CLOSE (assume contention означает что-то странное).

### 3.6. Gate budget / timeout / state cache

Gate имеет жёсткий бюджет времени **2 секунды** на одну decision. Если превышен — fail-CLOSE (block с reason «gate budget exceeded»).

**Оптимизации в gate:**

- **In-memory state cache** (per gate-process) с TTL 5s: router-state и chain-state читаются раз, кэшируются. Следующий tool call в той же сессии в течение 5s — из кэша.
- **Lazy transcript parsing:** gate читает только last 5 turns transcript (не весь файл). Этого достаточно для проверки `ask_user_called_this_turn`, `skill_invoked_matching`, `is_direct_invocation`.
- **Configurable budget** через `~/.claude/runtime/gate-config.json`:

  ```json
  {
    "max_decision_time_ms": 2000,
    "state_cache_ttl_ms": 5000,
    "transcript_lookback_turns": 5
  }
  ```

### Chain-state файл

`~/.claude/runtime/chain-state-<sess>.json`:

```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. Обновляется PostToolUse-handler'ом при **success** matching skill invocation — chain_step++ (НЕ на PreToolUse `tool_input` match чтобы skill упавший mid-execution не прогрессировал chain без работы). Очищается когда `chain_step >= chain_active.length` или router даёт другую цепочку.

**Chain-state TTL.** Chain-state считается expired и удаляется если:

- `initialized_at + 24h < now` — цепочка протухла.
- **`transcript[-1].user.content`** (only — НЕ assistant turns; controller не может self-clear chain упоминанием маркера в собственном response) содержит явные explicit-reset фразы: `новая задача`, `сброс контекста`, `забудь предыдущее` (case-insensitive substring, точный match без частичных вхождений).

При expiry — chain-state удаляется, gate переходит в Поведение 4 (silence) если новый router-state не даёт recommendation.

**Chain progression semantics.** `chain_step++` инкрементируется НЕ при PreToolUse `tool_input` match, а при **PostToolUse success** matching skill/Task. Это требует регистрации gate ещё на PostToolUse event (lightweight handler — только инкремент chain_step при `tool_use_id` соответствующем skill, который PreToolUse разрешил по chain_step). Иначе skill упавший mid-execution прогрессирует chain без работы. Реализация: PostToolUse-handler читает `chain-state.json`, проверяет `tool_use_id` против `expected_node`, инкрементирует если success (PostToolUse fires только на success — verify через Smoke 2 §3.2.0).

**Chain SoT.** Для определения `chain_step` gate использует **`chain-state.json::chain_step` как Single Source of Truth**, НЕ transcript-lookback. Transcript-lookback (§3.6, 5 turns) используется только для current-turn флагов `askuser_called_this_turn` / `skill_invoked_matching` / `is_direct_invocation` — они per-turn, помещаются в окно. Это критично для длинных chain >5 turns (через transcript-only step-detection терялась бы).

### Logging

`~/.claude/runtime/router-gate-decisions.jsonl`:

```json
{"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)

**Детектор: strict whitelist (никаких fuzzy regex).** Direct invocation **только** через literal pattern match. Все regex case-insensitive Cyrillic-aware через `/.../iu` flags:

| Pattern | Пример | Что разрешает |
|---|---|---|
| `^/[a-z0-9_-]+($\|\s)` | `/brain-retro` / `/code-review --ultra` | Slash-команда в начале prompt'а |
| `(вызови\|примени)\s+Skill\([^)]+\)` | `вызови Skill(superpowers:writing-plans)` | Literal call-syntax |
| `использ(уй\|уйте)\s+#\d+` | `используй #19` / `Используйте #19` | Literal по реестру |
| `(делай\|сделай\|вызови\|примени\|используй)\s+<exact-skill-name>` | `делай subagent-driven-development` / `Сделай writing-plans` | Literal имя из реестра (точное совпадение с node `name` или `slug`). 5 morphology variants |

**НЕ детектится как direct invocation:**

- `продолжай`, `продолжи`, `делай дальше` — разговорное продолжение, не указание скила
- `делай как считаешь нужным` — передача решения мне
- `сделай Х`, `можешь сделать`, `давай делать` — без явного skill-name
- `вызови что-то` / `используй какой-нибудь скил` — без exact skill-name

**Если direct invocation детектирован:**

- Tool-call матчит указанный skill → **allow** без AskUserQuestion.
- Tool-call НЕ матчит → **block** с reason: «заказчик указал X через direct invocation, текущий tool — другое».
- Read-only baseline (Read/Grep/Glob/LS/TodoWrite/AskUserQuestion) разрешены всегда.

**Source restriction (closes audit CRITICAL-1):** detector патернов **проверяется ТОЛЬКО против organic root user prompt в turn** — первое user message турна. AskUserQuestion chosen_label / chosen_text НЕ проходят через Поведение 1 detector — обрабатываются эксклюзивно через §4.5 answer parser. Контроллер контролирует labels (option text создаётся controller'ом), без этой restriction он может сконструировать опцию `делай subagent-driven-development` (exact registry match) → user выбирает → label попадает в transcript как user content → Поведение 1 fake-triggers → allow любого mutating tool. Implementation: gate использует transcript metadata который различает `user_message_type: "prompt"` vs `user_message_type: "askuser_answer"`. Pattern match только на тип `prompt`.

**Stale registry — mandatory AskUser.**

Если префикс-pattern matched (например `делай foo-bar`), но `foo-bar` **не найден** в registry — gate **обязан** заблокировать любой mutating tool до **mandatory AskUserQuestion** с явным форматом: «Пользователь указал skill `foo-bar`, но он не найден в реестре (возможно опечатка или переименован). Router рекомендует `<rec_node>`. Что делать?» с опциями:

1. «Имел в виду `<closest-fuzzy-match>` — делать его» (если fuzzy-match similarity > 0.7).
2. «Делать `<rec_node>` по рекомендации router'а».
3. «Делать direct без скила».
4. «Остановиться — переформулирую prompt».

Read-only baseline разрешён всегда. Логируется в decisions.jsonl как `direct_invocation_unmatched_registry_askuser_required: <name>` для surface в brain-retro.

**Multiple direct invocations в одном prompt'е.**

Если в prompt'е несколько direct invocations (например `вызови writing-plans, потом делай subagent-driven`):

- **Первая** invocation выполняется без AskUserQuestion.
- **Вторая и далее** требуют AskUserQuestion после завершения первой (`Ты указал A, B, C — A выполнен, продолжать с B?`).
- Не разрешается chained execution всех direct invocations подряд без явного approval per step.

### Поведение 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++ происходит на PostToolUse success) |
| 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 уже инкрементирован PostToolUse-handler'ом при success предыдущего skill) |
| 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 варианта на выбор с цена каждого (плюсы / минусы).

### 4.5. AskUserQuestion answer parsing

Gate не unlock'ает turn просто по факту вызова AskUserQuestion. Gate **читает ответ заказчика** из transcript (AskUserQuestion tool result содержит структурированную информацию о выбранной опции) и интерпретирует:

| Ответ заказчика | Действие gate'а |
|---|---|
| Выбранная опция содержит `стоп` / `отмена` / `не делать` / `ничего` / `остановись` / `cancel` / `stop` (case-insensitive) | Gate **остаётся заблокированным**. Никакого unlock |
| Выбранная опция матчит конкретный skill/tool/action в формуле «делать X» / «выполнить X» / «использовать X» | Gate unlock'ает **только** этот конкретный skill/tool. Остальные mutating tools всё ещё блокируются (требуется новый AskUserQuestion для них) |
| Выбранная опция явно говорит «direct, без скила» / «продолжить без skill'а» / «делать напрямую» | Gate unlock'ает Edit/Write/MultiEdit (но не Bash mutating и не Skill/Task non-baseline). Bash требует whitelist (§5.1) |
| Свободный ответ (заказчик выбрал «Other» и написал текст) | Gate анализирует free-form text по тем же правилам. Если интерпретация неоднозначна — gate требует ещё одно AskUserQuestion для уточнения |
| **Любой ответ не матчащий ни один из 4 классов выше** — **default-CLOSE** | Gate **остаётся заблокированным**. Записывается `gate_interpretation: "no_match_remain_blocked"`. Контроллер обязан вызвать второй AskUserQuestion с переформулированными опциями (включая явные keyword-маркеры stop/approve). Если и второй ответ unmatched → 2/2 limit достигнут → fail-CLOSE: «AskUserQuestion ambiguity loop — заказчик пусть переформулирует prompt с явным указанием действия» |
| **Опция содержит «выполнить `<git command>`» / «approve `<git command>`» / «делать `<git command>`» / «Да — выполнить `<git command>`»** где `<git command>` matches pattern `git (rebase\|reset\|clean\|checkout --\|branch -[DfF]\|push --force\|stash drop\|cherry-pick\|revert)` | Gate пишет в `askuser-decisions-<sess>.jsonl`: `gate_interpretation: "approve_git_operation"`, `approved_tool: "Bash"`, `approved_action_pattern: "<exact git command>"`, `consumed: false`. `enforce-branch-switch.mjs` хук читает эту запись при next Bash tool-attempt — если `tool_input.command` matches `approved_action_pattern` strict exact (case-sensitive), И запись `consumed: false`, И `ts` within 5 minutes — gate allow, helper пишет `consumed: true` обратно (one-shot). Closes dangerous git operations через explicit per-operation consent |
| AskUserQuestion вызван, но заказчик ещё не ответил (in-flight) | Gate ждёт ответ — pre-answer state, никакого unlock |

Все решения логируются в `~/.claude/runtime/askuser-decisions-<sess>.jsonl`:

```json
{"ts": "2026-05-28T20:30:01.123Z", "question": "...", "options": [...], "chosen": "стоп — не делать", "gate_interpretation": "stop_remain_locked", "subsequent_tools": []}
```

**AskUserQuestion counter.** Gate ведёт счётчик `askuser_count_this_turn` в state-файле. Лимит **2 AskUserQuestion за один turn**. 3-я попытка → gate **fail-CLOSE**:

```
AskUserQuestion ambiguity loop detected (2/2 limit exhausted в этом turn'е).
Чтобы продолжить — отправь новый prompt с одной из явных фраз:
  • "стоп" / "отмена" — gate останется заблокированным
  • "делай <skill-name>" — direct invocation конкретного скила
  • "продолжить без скила" — direct без skill'а (Edit/Write разрешены, не Bash)
  • slash-команда типа "/brain-retro" — direct skill через slash
Счётчик AskUserQuestion сбросится на новом prompt'е.
```

### 4.6. Post-skill partial unlock

После вызова matching Skill (или Task subagent) gate **частично** unlock'ает turn:

| Tool | Разрешено после Skill match |
|---|---|
| Read / Grep / Glob / LS / TodoWrite / AskUserQuestion | ✅ Always |
| Next chain step (если chain активна) | ✅ Если матчит chain[chain_step] |
| Task subagent matching active recommendation | ✅ |
| Edit / Write / MultiEdit / NotebookEdit | ⚠️ Только после **дополнительного** AskUserQuestion с явным «выполнить такое-то изменение» |
| Bash mutating | ❌ Требует AskUserQuestion с конкретной командой (§4.5) + §5.1 whitelist |
| Skill / Task non-matching | ❌ |

После вызова рекомендованного skill'а я не могу автоматически делать **любые** правки — нужен дополнительный явный approval от заказчика на конкретное действие.

Soft-mitigation: rationalization-audit детектит pattern «Skill X called → 3+ tools потом без TodoWrite update с цитированием skill output» → flag в next turn (warn-only).

### 4.7. AskUserQuestion question quality detector

`enforce-rationalization-audit` расширяется новыми детекторами для AskUserQuestion content:

**Block (hard):**

- Опция «остановиться» / «отмена» / «cancel» **обязательна** в списке. Если её нет — gate **блокирует** вызов AskUserQuestion и требует переформулировать вопрос с opt-out.

**Soft warnings (surface в next turn):**

- Лейбл одной из опций содержит «(рекомендую)» / «(recommended)» / «(safe)» / «(quick)» / «(подходит лучше всего)» / «(оптимально)» — потенциальный leading question. Flag.
- Сильная разница в длине опций: **ratio > 4× между самой длинной и самой короткой**. Flag.
- **First-option position bias:** если **первая** опция в списке содержит suggestive lexicon (рекомендую/safe/quick/подходит/оптимально/best) И опции не отсортированы alphabetically/by-action-type — soft flag «возможно first-position bias». Detection: keyword-list scan на позиции 1 vs позиции 2+.
- Вопрос не относится к active recommendation / current task — потенциальный bypass attempt. Flag.

**Off-topic detection при silence:** когда recommendation отсутствует (Поведение 4), off-topic check сравнивает вопрос AskUser не с recommendation (её нет), а с **`task_classification`** field из router-state. Если classification = `bugfix`, а вопрос про дизайн / погоду / неотносящееся к bugfix — soft flag. Использует keyword-overlap heuristic (simple — без LLM call).

Flags попадают в `rationalization-flags-<sess>.jsonl` и сюрфейсятся в next prompt через `enforce-prompt-injection`. 90% реальных случаев ловятся — creative leading questions неискоренимы.

---

## 5. Безопасная база (всегда разрешено)

Эти tools never blocked, любое поведение:

- `Read` — просмотр файлов (кроме protected paths §3.1)
- `Grep` — поиск по содержимому
- `Glob` — поиск по имени
- `LS` — список файлов
- `TodoWrite` — мой список задач (внутренний)
- `AskUserQuestion` — обращение к заказчику (но gate проверяет content per §4.7 и parse-ит ответ per §4.5)
- `ListMcpResourcesTool` — список MCP-ресурсов
- `ReadMcpResourceTool` — чтение MCP-ресурса (только для tools явно помеченных read-only в реестре; default — block)
- Текстовый ответ (нет tool_use)

`Bash` **НЕ** в безопасной базе — для него отдельные правила в §5.1.

### 5.1. Bash content rules

Gate инспектирует **content** Bash-команды (string в `tool_input.command`). Парсит через простой shell-aware tokenizer (split по `;`, `&&`, `||`, `|`, `&` (background) — далее извлекает first token каждой ветви как command). **Tokenizer rule:** `&` split применяется только если **не** часть `&&` (lookahead `&(?!&)`). Закрывает `git status & mv evil good` атаку — токенизируется на 2 команды, `mv` ловится hard-blacklist'ом.

**Tokenizer limitation (acknowledged):** простой split не handle'ит quoted args / escaped chars (`git log --grep='foo;bar'` ломается на quoted `;`). Принимается как acceptable friction — false-positive блок проще обойти через AskUser, чем риск bypass через quote-escaping.

**Default-deny** — если command не в whitelist и не в conditional-whitelist, gate блокирует.

**Whitelist (всегда разрешено, любое поведение):**

| Команда | Допустимые args |
|---|---|
| `git status` | без mutating флагов |
| `git log` / `git show` / `git diff` / `git blame` / `git format-patch` | без `--exec`, **без `--output=*`, без `-o <path>`, без `--file=*`** (write to file через whitelisted command) |
| `git rev-parse` / `git merge-base` | любые |
| `git branch --show-current` | (только этот sub-command для `git branch`) |
| `git remote -v` / `git remote show` | без add/set-url |
| `ls` / `pwd` / `wc` / `head` / `tail` / `file` / `stat` | без `>` redirect, **path-deny overlay на ВСЕ path-args** |
| `grep` / `egrep` / `fgrep` | без `--exec`, без `-l` после `>`, **path-deny overlay на ВСЕ path-args** |
| `cat` / `less` / `more` | без `>` redirect, **path-deny overlay на ВСЕ path-args** |
| `node` | **mandatory positional `<script-path>` arg** (НЕ REPL mode); без `-e` / `--eval` / `-p` / `--print`, **без `-r` / `--require` / `--import` / `--experimental-loader`** (preload arbitrary module before main script), **без `-i` / `--interactive` / `--inspect*`** (REPL/debugger ports); pipe receiver `node` без positional path arg = block (closes audit CRITICAL-8: `echo 'code' \| node` arbitrary execution) |
| `npx vitest run ...` / `npx vitest --version` | любые run-args, не `--reporter` с suspicious patterns |
| `npm test` / `npm run test` / `npm run lint:*` (read-only scripts) | любые |
| `php artisan` (read-only commands только: `list`, `route:list`, `migrate:status`) | по whitelist |
| `composer show` / `composer outdated` | без `--update` |

**Hard-blacklist (никогда не разрешено, даже после unlock):**

- `rm` / `mv` / `cp` (любые формы — file mutation)
- `chmod` / `chown` / `chgrp`
- `>` / `>>` — redirect к файлу (создаёт/перезаписывает файл)
- `&&` / `||` chaining двух mutating commands
- `node -e` / `node --eval` / `python -c` / `bash -c` / `sh -c` / `eval` — arbitrary code execution
- `git push` / `git commit` / `git merge` / `git rebase` / `git reset` / `git checkout` / `git switch` / `git pull` / `git stash` / `git cherry-pick` / `git revert` / `git branch -f` / `git branch -d`
- `composer install` / `composer update` / `composer require` / `composer remove`
- `npm install` / `npm update` / `npm remove` / `yarn add` / `pnpm add`
- `npx claude-*` — запуск Claude изнутри Claude
- `curl -X POST/PUT/DELETE/PATCH` / `wget --post-data` — outbound mutations
- Любые pipe-команды (`|`) где receiver — mutating command OR `node` без positional script path (REPL via stdin)
- `<` (input redirect) к file — закрывает audit PARTIAL-15: `wc -l < ~/.claude/runtime/router-state.json` обход path-deny overlay через input redirect (whitelist reading command + `<` bypass)

**Sub-shell / heredoc broad sweep — команды содержащие любой из следующих токенов hard-blocked всегда (даже после unlock):**

- `` ` `` (backtick — command substitution)
- `$(...)` — POSIX command substitution
- `<(...)` / `>(...)` — process substitution
- `<<` / `<<-` — heredoc
- `<<<` — here-string (closes audit CRITICAL-9: `node <<< "fs.writeFileSync(...)"` arbitrary execution bypass)

Некоторые legitimate uses (`git log $(echo HEAD)`) ломаются. Принимаем как acceptable friction для закрытия sub-shell bypass attack surface.

**Conditional whitelist (разрешено только после явного AskUserQuestion approval per §4.5 ИЛИ Skill match per `SKILL_BASH_ALLOW`):**

- Specific commands из ответа заказчика (например если он выбрал «выполнить `npx vitest run`» — тогда разрешена эта exact command).
- После Skill match для конкретной семантики — разрешённые Bash-команды определяются **hardcoded mapping `SKILL_BASH_ALLOW`** в gate-code (не Skill-controlled, иначе Skill мог бы запросить произвольный Bash):

```js
// tools/router-gate-decide.mjs — SKILL_BASH_ALLOW
const SKILL_BASH_ALLOW = {
  'superpowers:test-driven-development': [
    /^npx vitest run( |$)/,
    /^npm test( |$)/,
    /^php artisan test( |$)/,
    /^composer test( |$)/,
  ],
  'superpowers:verification-before-completion': [
    /^npx vitest run( |$)/,
    /^composer test( |$)/,
    /^npm run lint:[a-z-]+( |$)/,
  ],
  'regression': [
    /^npx vitest run( |$)/,
    /^composer test( |$)/,
    /^php artisan test( |$)/,
    /^npm run lint:[a-z-]+( |$)/,
    /^npm run build( |$)/,
  ],
  // ... другие skills — расширяется по требованию из реальной работы.
};
```

Маппинг — **в коде gate'а, не в Skill определении**. Skill не может расширить свой allow-list (это снова controller-writable signal). Расширение mapping — только через `tools/router-gate-decide.mjs` Edit + tests + PR review (а файл — в §3.1 protected paths).

**Path-deny overlay (§3.1) — applied to ALL path-args (per-arg validation):** для **каждого** path-args whitelisted reading commands (`cat`/`tail`/`head`/`grep`/`less`/`more`/`file`/`stat`/`wc`/`ls`) gate нормализует path (§3.1 path normalization: UNC strip + 8.3 expand + resolve + realpath + case-fold + env expansion) и проверяет против protected list. Пример: `tail -n 100 ~/.claude/runtime/router-state-abc.json` — `tail` whitelisted, но `~/.claude/runtime/*` в protected → block.

**Tokenizer per-arg requirement (closes audit CRITICAL-6):** при tokenize Bash-команды извлекать НЕ только first token (command), но ВСЕ subsequent args. Каждый arg который parse'ится как path (relative or absolute, не option flag начинающийся с `-`) проходит через path-deny check independently. Команда `cat file1.txt ~/.claude/runtime/router-state.json` — `cat` whitelisted, **два** path-args, каждый normalize + проверяется → второй args block'ируется → команда block с reason про второй args. Все reading commands проверяются единообразно (не только `cat`).

**File-watcher для script execution — session-scoped, NOT turn-scoped:** gate tracks `edited_files_this_session` в `~/.claude/runtime/edited-files-<sess>.json` (session-wide persistence). Если в session был Edit/Write на `*.js` / `*.test.js` / `*.mjs` / `*.py` / `*.test.mjs` файл, последующие `node X`, `npx vitest run X`, `python X` блокируются если target path в session-list. Reset происходит **только** на:

- (a) **`git commit` exit code 0 AND lefthook GREEN.** Реализация: gate регистрирует PostToolUse handler на Bash `git commit*` и проверяет `tool_result.exit_code === 0` И `tool_result.stdout` не содержит lefthook FAIL markers (`✗`, `exit status N>0`, `summary: ... FAIL`). НЕ на attempt — если commit fail'ит из-за pre-commit хука, watcher state НЕ очищается.
- (b) явный markup `<!-- edited-files-clear -->` в user-prompt (НЕ в controller response — same pattern как chain-state restriction).
- (c) новый session-id.

Reason: «recently-edited script cannot be executed without explicit approval — request via AskUserQuestion».

Turn-scoped tracking позволял bypass — Edit в turn N, run в turn N+1, watcher state очищен. Session-scoped закрывает.

Default = block. Reason: «Bash command «{command}» не в whitelist; либо запроси конкретное разрешение через AskUserQuestion, либо используй read-only альтернативу».

### 5.2. Static content scan for executable scripts

Перед разрешением `node <script>` / `npx vitest run <pattern>` / `python <script>` gate **читает content** target-скрипта и сканирует regex'ом suspicious patterns:

**Blocked patterns в script content:**

- `fs.writeFileSync` / `fs.writeFile` / `fs.appendFileSync` / `fs.unlinkSync` / `fs.unlink` / `fs.rmSync` / `fs.rmdirSync` — если target path matches protected list (§3.1).
- `child_process.spawn` / `child_process.exec` / `execSync` / `spawnSync` — если args содержат commands из hard-blacklist (`rm`, `git push`, etc).
- Python equivalents: `os.remove`, `os.unlink`, `shutil.rmtree`, `subprocess.run` с suspicious args.

Если matched — block с reason: «Script contains forbidden operations targeting protected paths».

**Limitations:** статический скан ловит direct calls, но не creative indirection (например `fs[String.fromCharCode(...)]`). 95% real cases caught. Полное решение требовало бы sandbox runtime — выходит за scope этой итерации.

**Glob-aware scan:** если target argument в `npx vitest run <pattern>` / `node <pattern>` содержит glob-метасимволы (`*`, `?`, `[`, `]`, `{`, `}`) — gate **не** может статически выбрать файл для сканирования (vitest auto-discovers config + setup-hooks + transitively-imported tests). В этом случае gate требует **AskUserQuestion approval** с явным указанием pattern и ожидаемого scope. Conditional whitelist (§5.1) применяется к exact-pattern approval — заказчик пишет «approve `npx vitest run tools/*.test.mjs`» → gate unlocks exactly эту команду.

**Direct imports follow (1-level deep):** для single-target скриптов (`node tools/foo.mjs`) gate **дополнительно** сканит файлы, импортируемые из target напрямую (parse `import ... from './X'` / `require('./X')` regex, resolve to absolute, 1 level deep, без следования транзитивно). Если imported file тоже matches forbidden patterns — block с reason: «target script imports forbidden module». Закрывает базовый case «target innocent.mjs но импортирует ./evil.mjs». Транзитивная глубина оставлена как acknowledged limitation (deep imports → require sandboxing). Сканирование не следует за dynamic imports (`import()` / `require(variable)`) — известная edge.

---

## 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-<sess>.json` заменить `recommended_node` и `recommended_chain` на null. Следующий tool-call gate увидит «молчание роутера» — пойдёт сценарий AskUserQuestion.

---

## 7. Логирование и наблюдаемость

### Новый источник данных

`~/.claude/runtime/router-gate-decisions.jsonl` (формат — §3 + §10.2).

### Что теряется/искажается в brain-retro

- `override-usage.jsonl` пустеет (hook удалён, vocab нет). Таблица «Override events trend by day» исчезает.
- `hook-outcomes.jsonl` Cut 11 buckets (`passed-inline-override`, `passed-global-override`) не пишутся.
- Brain-retro Table 5 bucket `direct_ignored_rec` становится 0 (gate блокирует). Интерпретация меняется: теперь либо approved direct либо stop.
- Factor matrix `decision_provenance` — `autonomous` почти исчезает для mutating tools. Доминирует `user_chose_from_options`.
- `path_type` — всё mutating становится `regulated`, `improvised` только для read-only.

Архивы (`override-usage.jsonl`, `hook-outcomes.jsonl`) остаются на диске. Новый код их игнорирует.

### Что добавляется в 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` / `user_directed`. `autonomous` остаётся только для read-only и conversation.

**`path_type`** — добавить третье значение `gate_approved`.

### 7.1. Coverage-hint coordination

Gate (PreToolUse) при первом mutating tool call в turn'е пишет «expected coverage hint» в `~/.claude/runtime/coverage-hint-<sess>.json`:

```json
{
  "ts": "<iso>",
  "session_id": "<sess>",
  "turn_id": "<turn>",
  "expected_coverage": "skill:superpowers:writing-plans",
  "reason": "router recommended #19, AskUserQuestion approved",
  "first_mutating_tool": "Skill"
}
```

`enforce-coverage-verify` (Stop hook) при срабатывании читает hint и сравнивает с actual coverage в response. Если actual matches expected → ✅ pass. Если mismatch → flag в `rationalization-flags-<sess>.jsonl` для surface в next prompt. Post-fact дисциплина — не блокирует turn (уже закончен), но создаёт visible feedback loop.

Hint удаляется при Stop event (cleanup).

---

## 8. Этапы реализации

| Этап | Содержание | Часы |
|---|---|---|
| 1 — Подготовка инфраструктуры | Pure decision-модуль (4 поведения + §4.5/4.6/4.7) + I/O обёртка + chain-state persistence + TTL logic + decision logger + askuser-decisions logger + askuser-counter + ~60 unit-тестов | 2.5-3.5 |
| 1.1 — Bash content parser | Pure Bash-command tokenizer + whitelist/blacklist matcher + sub-shell broad sweep + path-deny overlay + edited-files watcher + ~25 unit-тестов | 1.5-2 |
| 1.2 — AskUserQuestion answer parser | Pure answer-text classifier (stop / specific-skill / direct-no-skill / freeform / git-pattern) + transcript reader + ~15 unit-тестов | 0.5-1 |
| 1.3 — Question quality detector | Расширение `enforce-rationalization-audit` с детекторами missing-stop / leading-options / off-topic + silence task_classification matching + first-option position bias + ~12 тестов | 0.5 |
| 1.4 — Path normalization (§3.1) | path.resolve + realpath + case-fold + env-var expansion + ~10 тестов | 0.5 |
| 1.5 — Atomic writes / file locking (§3.5) | write-tmp+rename + proper-lockfile integration + ~15 тестов | 1 |
| 1.6 — Gate budget / state cache (§3.6) | 2s timeout + in-memory cache TTL 5s + lazy transcript parsing + gate-config.json + ~10 тестов | 0.5-1 |
| 1.7 — Static content scan (§5.2) | Script content reader + regex scanner for fs.write/exec/spawn + protected-path matching + glob-aware AskUser + direct imports 1-level + ~15 тестов | 0.5 |
| 1.8 — Coverage-hint coordination (§7.1) | Hint write on first mutating tool + coverage-verify reads hint + mismatch flag + ~8 тестов | 0.5 |
| 2 — Удаление 5 хуков + vocab | Удалить 5 .mjs + 5 .test.mjs + vocab.json (11 файлов). 3 helper-функции в stubs. Регрессия GREEN | 1-2 |
| 2.1.0 — Pre-flight smoke-tests (§3.2.0) | **БЛОКЕР для 2.1/2.2.** Три user-run smoke-tests: (1) env propagation probe, (2) PostToolUse semantics probe, (3) subagent block-file write probe. Pass всех → продолжить §3.2 env-based + §3 chain PostToolUse + §3.4 S5 closure. Fail любого → stop эпик, degraded fallback | 1.5 |
| 2.1 — Subagent env-based inheritance (§3.2) | Расширение subagent-prompt-prefix.mjs — env vars + inheritance file write + gate-on-subagent reads env first + ~10 тестов. **Блокируется 2.1.0** | 1.5-2 |
| 2.2 — Subagent constraints + block-file (§3.4) | Remove AskUserQuestion from subagent baseline + block recursive Task + parallel limit 3 + subagent gate writes block-events to `subagent-block-<tool-use-id>.json` + parent gate reads block-file at PostTask + proper-lockfile integration + ~16 тестов | 1.5-2 |
| 2.3 — Branch-switch AskUserQuestion-gate | Rewrite `enforce-branch-switch.mjs` — больше не reads response markers, reads askuser-decisions.jsonl approval; one-shot consume; 5-min time window; extended dangerous git operations list (rebase / reset --hard / clean -f / push --force / stash drop / cherry-pick / revert / checkout -- / branch -D) + ~8 тестов | 0.5-1 |
| 3 — Регистрация в settings.json | Добавить router-gate (PreToolUse all + PostToolUse on Skill/Task/Bash для chain_step / edited-files / git commit), снять регистрации удалённых хуков. Smoke-test полной сессии | 0.5 |
| 4 — Документация Recovery | Памятка для заказчика по 3 уровням recovery | 1 |
| 5 — Прогон в реальной работе | Несколько дней наблюдения, brain-retro #11 | (не таск) |
| 6 — Brain-retro adaptation | Обновить `brain-retro-analyzer.mjs` + SKILL.md mandatory tables 11→13 | 1.5-2 |
| **Итого** | | **22.5-32** |

### Implementation order matrix

Protected paths должны быть live **до** того как соответствующие защиты ссылаются на них. Sequence per Этап:

| Этап | Protected-up-to | Защиты live |
|---|---|---|
| 1 (pure decision module) | settings.json только | Gate в shadow-mode (logs, не блокирует) |
| 1.4 (path normalization) | + `~/.claude/runtime/*` (включая `gate-config.json` — early protection, closes audit DOS D-1) + `tools/enforce-*.mjs` | Hard-deny на gate state-файлы; gate-config.json защищён ДО Этапа 2 чтобы tiny-budget patch attack невозможна |
| 2 (удаление 5 хуков + vocab) | + `docs/registry/nodes.yaml` + `tools/registry-load.mjs` + `tools/enforce-hook-helpers.mjs` + `tools/subagent-prompt-prefix.mjs` + `package.json` / `composer.json` | Полный §3.1 list active **до** включения router-gate (subagent-prompt-prefix должен быть в protected ДО первой записи new-schema inheritance файла) |
| 2.1 (subagent inheritance) | (без новых) | §3.2.0 smoke-tests run by user; если pass — env-based реализация активируется |
| 2.2 (subagent constraints + block-file) | (без новых) | §3.4 constraints + side-channel block-file активны |
| **2.3 (branch-switch AskUser-gate)** | (без новых) | **MUST complete BEFORE Этап 3** (closes audit CRITICAL-10): `enforce-branch-switch.mjs` rewrite under §4.5 git-pattern + askuser-decisions consume — иначе во время Этапа 3 enforce-mode OLD hook ещё принимает `BRANCH-SWITCH-CONFIRMED` controller-writable marker → S8 closure НЕ активна → dangerous git ops unprotected. Альтернатива: Этап 3 settings.json регистрация включает обновлённый branch-switch hook одной транзакцией |
| 3 (settings.json reg) | (без новых) | Router-gate switch from shadow to enforce-mode. **Pre-condition:** Этап 2.3 deployed (S8 closure live). Если 2.3 не готов → НЕ переходить в enforce-mode |

**Принцип:** каждый Этап **сначала** расширяет protected list, **потом** активирует защиту что на него ссылается. Между Этапами — gap-окно когда controller теоретически может Edit'нуть будущий protected file. Mitigation: Этапы 1.4 → 2 → 2.1 → 2.2 → 2.3 → 3 идут одной сессией (несколько часов), не дни — gap-окно short.

### Риски миграции

- **Этап 2 порядок-чувствительный** — сначала превращаем helper-функции в stubs (возвращают null/empty), потом удаляем vocab.json и тесты vocab. Если удалить vocab.json первым, тесты helpers упадут до stub-конверсии.
- **Этап 3 без отката** — если gate ломается, controller остаётся без всех PreToolUse-хуков (поставили новый, старые удалили). Mitigation — feature-branch + push до Этапа 3 для snapshot.

---

## 9. Открытые вопросы (для плана implementation)

- **Точный формат AskUserQuestion message** для каждого Поведения 2/3/4 — нужна шаблонная функция или жёсткие строки? Скорее всего шаблонная (template literal с placeholder'ами).
- **Throttling AskUserQuestion** — если за один turn я вызываю AskUserQuestion 3+ раза, gate должен warn'ать или block'ать? (Текущий design — fail-CLOSE на 3-й, см. §4.5.)
- **Free-form answer интерпретация** — когда заказчик ответил «Other» с текстом, как gate решает что разрешено? §4.5 говорит «если интерпретация неоднозначна — ещё одно AskUserQuestion». Но «неоднозначна» — нечёткая граница. Может потребоваться LLM-call для парсинга. Стоит обсудить отдельно.
- **Logging granularity** — все decision events в одном файле или раздельно (router-gate / askuser-decisions / gate-errors)? Сейчас разделено на 3 файла.
- **Bash whitelist completeness** — список в §5.1 покрывает основные read-only operations, но возможно есть пропуски (например `tree`, `du`, `find` с right args). Расширяется итеративно из реальной работы.

### Audit findings deferred to writing-plans (5 SHOULD fixes)

29.05.2026 adversarial audit ([docs/superpowers/audits/2026-05-29-router-gate-condensed-adversarial-audit.md](../audits/2026-05-29-router-gate-condensed-adversarial-audit.md)) выявил 5 findings которые требуют **design decisions per fix** и решаются в writing-plans фазе (не trivial inline patch):

1. **SHOULD-FIX-1 — vitest globalSetup cross-session blind spot** (audit CRITICAL-2): Static scan §5.2 расширить — при любом `npx vitest run` сканировать `vitest.config.*` + `vite.config.*` + все referenced `setupFiles` / `globalSetup` независимо от file-watcher state. Open question для plan: scan recursion limit (1-level transitive imports follow vs full transitive).
2. **SHOULD-FIX-2 — `git format-patch -o` handling verification** (audit CRITICAL-7): row label «git log / git show / git diff / git blame / git format-patch» с exclusion `-o <path>` может покрывать обе команды (row-wide application) или только первую (per-command implementation). Verify parser implementation в Этапе 1.1 (Bash content parser); если per-command — добавить explicit `git format-patch` row с `-o` exclusion И блокировкой write в protected paths.
3. **SHOULD-FIX-3 — `approved_action_pattern` mandatory file-path binding** (audit PARTIAL-1): §4.5 answer parser сейчас разрешает class-level unlock (`approved_tool: "Edit"` без path). Plan: каждое `approved_specific_tool` interpretation обязано извлечь `target_path` из user choice и сохранить как `approved_action_pattern`. Gate при mutating tool сравнивает `tool_input.file_path` против pattern (exact или prefix match). Open question: glob-style patterns (`docs/**`) разрешены или только exact?
4. **SHOULD-FIX-4 — Chain reset фразы только в organic root prompt** (audit PARTIAL-2): §3 chain-state TTL detection. AskUser chosen_label содержащий `новая задача` сейчас тригернёт reset. Plan: интегрировать с CRITICAL-1 fix (transcript metadata `user_message_type`) — chain reset check только на `user_message_type: "prompt"`. Open question: что делать если controller пишет AskUserQuestion option с явным `"начать новую задачу"` — block через §4.7 question quality detector или allow с warning?
5. **SHOULD-FIX-5 — chain-state malformed → fail-CLOSE** (audit PARTIAL-7): §3.3 failure mode table сейчас даёт «chain-state malformed → reset+warn» (fail-OPEN). Symmetric fix с router-state semantics. Open question: какой recovery path если chain-state легитимно corrupted (например crash mid-write) — manual reset через recovery §6 уровень 3 или auto-reset с явной user confirmation через AskUser?

Audit также identified 9 DOS findings + 15 INFO/acknowledged residuals — детали в audit report. Plan ОБЯЗАН адресовать минимум 5 DOS findings: D-2 (subagent success-marker file), D-3 (lefthook exit code parsing), D-7 (subagent gate reader-writer lock), D-8 (gate-config defaults fallback), D-9 (enforce-prompt-injection empty handling).

### Acceptable residuals (acknowledged design choices, не security holes)

- **Max 2 AskUserQuestion per turn limit** (§4.5): chain'ы с many mutating tools (>2 шагов с Edit/Write/Bash per ход) НЕ помещаются в один turn — заказчик вынужден re-prompt'ить или делить задачу. Acceptable UX-tradeoff — это design choice §4.5 (anti-fatigue), не дыра. Multi-step chains реально работают через переspawning context per next prompt. Документировать в README/onboarding.
- **Off-topic detection при silence** (§4.7): keyword-overlap heuristic с `task_classification` ловит грубые off-topic'и; closure требовал бы LLM-call что добавляет cost+latency. 90% real cases caught.
- **Tokenizer-quoting limitation** (§5.1): простой split не handle'ит quoted args / escaped chars. Acceptable friction — false-positive проще обойти через AskUser чем риск bypass через quote-escaping.
- **First-option position bias** (§4.7): soft detector добавлен, 100% closure невозможно для creative leading questions.

### Класс controller-writable signals закрыт архитектурно

**Hard wall полный.** Все известные controller-writable signals закрыты через side-channel writes в protected paths:

- **S5 (subagent BLOCKED-protocol)** — side-channel file `subagent-block-<tool-use-id>.json`. Пишет subagent gate-process, path derived из harness-assigned tool_use_id, file protected. См. §3.4.
- **S8 (branch-switch confirmation)** — side-channel record в `askuser-decisions-<sess>.jsonl`. Пишет §4.5 answer parser при реальном AskUser-ответе пользователя, file protected, exact-match + one-shot consume + 5-min window. См. §4.5 git-pattern row + §3 preserved hooks branch-switch row.

**Generalisable формула** для будущих новых механизмов: write-канал в protected file через отдельный gate-процесс (или §4.5 parser внутри gate'а) при harness-driven событии (Task spawn = unique tool_use_id; AskUser answer = structured user input).

---

## 10. Cross-refs

### 10.1. Function and registry references

- **`nodeMatches(recommendation, toolUse)`** — pure function для сравнения router-рекомендации (`#19`, `superpowers:writing-plans`) с tool invocation (Skill/Task). Существует в текущем `tools/enforce-classifier-match.mjs:42-66`. В реализации Уровня 4 переносится в `tools/router-gate-decide.mjs` как exported function. Обрабатывает aliases через registry: `#NN` ↔ `node.id`, `superpowers:X` / `skill:X` через canonical lookup в registry.
- **Registry источник** — `docs/registry/nodes.yaml` — единственный source of truth для skill/node names, aliases, slugs. Gate использует `tools/registry-load.mjs` для чтения. Direct-invocation strict whitelist (§4 Поведение 1) сравнивает с `node.name` и `node.slug` из этого реестра.
- **Subagent_type matching** — для Task tool gate сравнивает `tool_input.subagent_type` (например `coder-agent`) с recommendation в формате `slug` или `name` через тот же `nodeMatches()` с alias resolution.
- **Coverage-hint cross-ref recovery (§7.1 ↔ §6):** при coverage mismatch flag попадает в rationalization-flags и сюрфейсится в next prompt. Заказчик видит «прошлый turn нарушил coverage hint» — это уже surface, не блок. Если coverage-hint критичен (например я аннотировал `direct` но gate ожидал `skill:X`), заказчик использует §6 recovery уровень 3 (правка router-state) чтобы выровнять.
- **SDD-skill impact** — `superpowers:subagent-driven-development` skill активно использует Task tool. С §3.4 constraints (no AskUser в субагенте, no recursive Task, max 3 parallel) — sub-skill будет работать но с ограничениями: контроллер не сможет делегировать больше 3 субагентов параллельно (раньше можно было N), и subagent при lockout эскалирует через parent's AskUser. Skill сам **не правится** в этом эпике; impact документируется как known-behavior-change в release notes.

### 10.2. State file schemas

Все state-файлы — JSON или JSONL.

**`~/.claude/runtime/router-state-<sess>.json`** (существует, не меняется):

```jsonc
{
  "schema_version": 1,
  "ts": "<iso-8601>",
  "session_id": "<sess>",
  "classification": {
    "task_type": "feature|bugfix|analysis|planning|conversation|memory-sync|ambiguous|unknown",
    "confidence": 0.0,
    "recommended_node": "#19|null",
    "recommended_chain": ["#55", "#19", "#56"],
    "triggers_matched": ["string"],
    "primary_rationale": "string"
  },
  "source": "prefilter|regex|llm|cache"
}
```

**`~/.claude/runtime/chain-state-<sess>.json`** (новый):

```jsonc
{
  "schema_version": 1,
  "chain_active": ["#55", "#19", "#56"],
  "chain_step": 1,
  "initialized_at": "<iso-8601>",
  "last_step_at": "<iso-8601>"
}
```

**`~/.claude/runtime/askuser-decisions-<sess>.jsonl`** (новый) — append-only:

```jsonc
{
  "ts": "<iso-8601>",
  "session_id": "<sess>",
  "turn_id": "<id>",
  "question": "string",
  "options": ["string"],
  "chosen_label": "string",
  "chosen_text": "string",
  "gate_interpretation": "stop_remain_locked|approve_specific_tool|approve_direct_no_skill|ambiguous_requires_followup|no_match_remain_blocked|approve_git_operation",
  "approved_tool": "Skill|Edit|Bash|null",
  "approved_action_pattern": "string|null",
  "consumed": false,
  "consumed_at": null,
  "consumed_by_tool_use_id": null
}
```

Поля:

- `gate_interpretation` enum включает `approve_git_operation` (новое значение для S8 closure), генерируется §4.5 answer parser когда user choice matches git-pattern (`git rebase|reset|clean|checkout --|branch -[DfF]|push --force|stash drop|cherry-pick|revert`).
- `consumed` boolean (default false). `enforce-branch-switch.mjs` ставит `true` после use approval один раз (one-shot). Subsequent attempts с тем же approval отклоняются как «already used».
- `consumed_at` / `consumed_by_tool_use_id` — audit-trail когда и каким tool_use_id consumed; для brain-retro анализа frequency repeat-approvals.

Backward-compat: старые записи без `consumed`/`consumed_at`/`consumed_by_tool_use_id` интерпретируются как `consumed: false` по default — никакой миграции данных.

**`~/.claude/runtime/router-gate-decisions.jsonl`** (новый) — append-only:

```jsonc
{
  "ts": "<iso-8601>",
  "session_id": "<sess>",
  "turn_id": "<id>",
  "tool_name": "Edit|Write|Skill|Task|Bash|...",
  "tool_input_summary": "string (truncated for log)",
  "decision": "allow|block|unlock",
  "reason": "string",
  "behavior_branch": "1_direct_invocation|2_single_rec|3_chain|4_silence",
  "state_snapshot": {
    "rec_node": "string|null",
    "rec_chain": ["string"],
    "chain_step": 0,
    "askuser_called_this_turn": false,
    "askuser_count_this_turn": 0,
    "skill_invoked_matching": false,
    "is_direct_invocation": false,
    "edited_files_this_turn": ["path"]
  },
  "gate_latency_ms": 0
}
```

**`~/.claude/runtime/subagent-inheritance-<task-id>.json`** (новый) — short-lived:

```jsonc
{
  "schema_version": 1,
  "parent_session_id": "<parent-id>",
  "allowed_actions": ["pattern"],
  "subagent_constraints": {
    "can_use_askuser": false,
    "can_spawn_task": false,
    "max_parallel": 1
  },
  "created_at": "<iso-8601>"
}
```

NB: `parent_router_state_path` / `parent_chain_state_path` поля **удалены** из schema. Субагент gate derives оба пути hardcoded из `parent_session_id` (path-injection защита — см. §3.2).

**`~/.claude/runtime/subagent-block-<tool-use-id>.json`** (новый — S5 TRUE closure) — short-lived per Task tool call, append-on-block:

```jsonc
{
  "schema_version": 1,
  "tool_use_id": "<id from Task tool, harness-assigned>",
  "subagent_session_id": "<sess>",
  "created_at": "<iso-8601>",
  "blocks": [
    {
      "ts": "<iso-8601>",
      "tool_name": "Edit|Write|MultiEdit|NotebookEdit|Bash|Skill|Task|...",
      "tool_input_summary": "string (truncated for log)",
      "reason": "string (gate decide reason)",
      "decision_kind": "1_direct_invocation|2_single_rec|3_chain|4_silence|hard_deny_path"
    }
  ]
}
```

Пишется subagent gate-процессом atomic-append через proper-lockfile (§3.5) при каждом decide=block. Читается parent gate at PreToolUse of next tool после Task return. Удаляется parent gate после чтения. Background cleanup script: orphans > 24h → delete (защита от parent crash после read но до delete). **Protected status:** glob `~/.claude/runtime/*` в §3.1 + явное упоминание для устранения двусмысленности. Controller не может Edit/Write/Delete.

**`~/.claude/runtime/edited-files-<sess>.json`** (новый) — session-scoped, append-on-edit:

```jsonc
{
  "schema_version": 1,
  "session_id": "<sess>",
  "edited_files": [
    {
      "path": "tools/foo.mjs",
      "edited_at": "<iso-8601>",
      "tool": "Edit|Write|MultiEdit|NotebookEdit",
      "tool_use_id": "<id>"
    }
  ],
  "reset_history": [
    {
      "reset_at": "<iso-8601>",
      "reason": "git_commit_success|user_explicit_clear|new_session"
    }
  ]
}
```

`edited_files` array содержит все Edit/Write на script-файлы (`*.js` / `*.test.js` / `*.mjs` / `*.py` / `*.test.mjs` per §5.1 file-watcher). Append-on-edit, full-rewrite on reset (per §5.1 — только на `git commit exit 0` + lefthook GREEN, явный markup в user-prompt, новый session-id). `reset_history` ведёт audit-trail почему очищали. Protected per §3.1.

**`~/.claude/runtime/coverage-hint-<sess>.json`** (новый) — short-lived per turn:

```jsonc
{
  "ts": "<iso-8601>",
  "session_id": "<sess>",
  "turn_id": "<id>",
  "expected_coverage": "skill:X|direct:Y|chain:[...]",
  "reason": "string",
  "first_mutating_tool": "Skill|Edit|...",
  "first_mutating_tool_ts": "<iso-8601>"
}
```

**`~/.claude/runtime/gate-errors.jsonl`** (новый) — append-only:

```jsonc
{
  "ts": "<iso-8601>",
  "session_id": "<sess>",
  "error_type": "state_missing|state_malformed|gate_internal|timeout|lock_contention",
  "error_message": "string",
  "stack_trace": "string (optional)",
  "tool_attempted": "string"
}
```

**`~/.claude/runtime/gate-config.json`** (новый) — tuning параметры:

```jsonc
{
  "max_decision_time_ms": 2000,
  "state_cache_ttl_ms": 5000,
  "transcript_lookback_turns": 5,
  "max_askuser_per_turn": 2,
  "max_parallel_subagents": 3,
  "chain_state_ttl_hours": 24,
  "lock_timeout_ms": 1000
}
```

### 10.3. Test strategy

**Pure decision-функции (unit, vitest):**

- `decide()` — 4 поведения × ~10 вариаций = ~40 тестов (§4).
- `parseAskUserAnswer()` — stop/skill/direct/freeform/git-pattern classification = ~15 тестов (§4.5).
- `detectDirectInvocation()` — strict whitelist patterns + negative cases = ~15 тестов (§4 Поведение 1).
- `bashCommandClassify()` — whitelist/blacklist/sub-shell sweep + tokenizer edge cases = ~25 тестов (§5.1).
- `staticContentScan()` — fs.write / exec patterns + protected paths + glob-aware + direct imports 1-level = ~15 тестов (§5.2).
- `pathNormalize()` — resolve/realpath/case/env = ~10 тестов (§3.1).
- `questionQualityCheck()` — missing-stop / leading / off-topic / first-option bias = ~12 тестов (§4.7).
- `chainStateUpdate()` — progression / TTL / replace-on-new-chain = ~10 тестов (§3 chain-state).

**Integration тесты (vitest + temp fs):**

- Atomic state writes — concurrent simulated writers, no corruption = ~10 тестов (§3.5).
- File locking — lock acquire/release + timeout fail-CLOSE = ~5 тестов (§3.5).
- Gate budget enforcement — slow decision → fail-CLOSE = ~5 тестов (§3.6).
- State cache invalidation на mtime change = ~5 тестов (§3.6).

**Subagent inheritance (вместе с расширением subagent-prompt-prefix):**

- Env-vars set + inheritance file write/read = ~8 тестов (§3.2).
- Subagent constraints — AskUser blocked / Task nested blocked / parallel > 3 blocked = ~8 тестов (§3.4).
- Subagent block-file write/read by parent = ~8 тестов (§3.4).

**Branch-switch AskUserQuestion-gate:**

- `approve_git_operation` write/consume/expire = ~8 тестов (§4.5 git-pattern + §3 branch-switch row).

**Golden snapshot tests (decision traces):**

- 10-15 end-to-end сценариев записанных как input → expected decision trace JSON. Покрывают типичные real workflows (writing-plans → Edit → Bash вышеуказанной vitest, chain progression across turns, silence + AskUser + Edit).

**Smoke тесты (полная Claude сессия):**

- Запустить Claude с тестовым prompt'ом, проверить что новый gate активен и старые не fire'ятся = 5-7 manual scenarios.
- **3 user-run smoke-tests из §3.2.0** — env propagation / PostToolUse semantics / subagent block-file write.

**Регрессия после каждого этапа:** `npx vitest run --exclude=".claude/**" --exclude="app/**" --exclude="tools/ruflo-*"` — должна оставаться GREEN.

**Целевое покрытие тестами:** ~160 новых unit-тестов + 10-15 integration + 10-15 golden snapshots + 5-7 smoke + 3 user-run smoke-tests.

### 10.4. Success metrics

После реализации и прогона ~1 неделю под Уровнем 4:

**Метрики которые должны drop'нуться к нулю (quantitative):**

- `~/.claude/runtime/override-usage.jsonl` — пустеет полностью (хука нет, vocab нет).
- `hook-outcomes.jsonl` Cut 11 buckets (`passed-inline-override`, `passed-global-override`) — не пишутся.
- Brain-retro Table 5 bucket `direct_ignored_rec` (Skill should have been used but wasn't) — 0 (gate физически блокирует).
- Rationalization flags `rationalization-phrase: временно` и подобные — должны drop'нуться значительно (но не до нуля — soft detector).

**Метрики которые должны вырасти (quantitative):**

- `router-gate-decisions.jsonl` entries per day — fresh source данных.
- `askuser-decisions-<sess>.jsonl` entries — visible user approval pattern.
- Brain-retro Table 12-new `approved-alternative-skill` — показывает где я ловлю плохие рекомендации router'а.

**Качественные метрики (brain-retro #11 после прогона):**

- Brain-retro Table 13-new `Lockout incidents` показывает <5 incidents per 100 episodes — gate помогает, не раздражает.
- Self-retrospect #3 (через ~50 episodes под Уровнем 4) подтверждает: attitude commitments больше не нужны — гибридная архитектура enforce'ит.
- Reviewer Table «node_quality» percentages: correct % > 60% (vs 41% на 28.05), wrong_node < 5% (vs 11%).
- Time-to-first-action в типичной задаче не превышает baseline +30% (acceptable friction от AskUser).

**Acceptance criteria для перехода в production:**

- 150+ unit tests GREEN.
- 10+ integration tests GREEN.
- 5 smoke scenarios passing.
- 3 user-run smoke-tests из §3.2.0 — все pass.
- Manual prod test: 1 час real workflow без false-positive блокировок.
- Brain-retro #11 (через 1 неделю) показывает 0 missed activations, 0 bypass attempts via gate.

### 10.5. Rollback plan

**Уровень 1 — выключить новый gate, сохранить остальное:** В `.claude/settings.json` секции `PreToolUse` удалить запись про `tools/enforce-router-gate.mjs`. Все 7 preserved хуков продолжают работать. Эффект: возврат к dynamics без router-routing enforcement (vocab.json уже удалён, нет escape-фраз).

**Уровень 2 — revert all v4 commits:** `git revert <gate-implementation-commit-range>`. Восстанавливает 5 удалённых хуков + vocab.json. Полный возврат к pre-эпику состоянию.

**Уровень 3 — checkout spec baseline:** `git checkout b510a758 -- docs/superpowers/specs/2026-05-28-router-gate-hard-wall-design.md` для rebuild с нуля.

### 10.6. Этапы и параллелизм

Большинство этапов и подэтапов в §8 могут идти параллельно через `superpowers:subagent-driven-development`:

**Sequential обязательно:**

- Этап 1 (pure decision module) → ВСЕ другие подэтапы (1.1-1.8 расширяют этот модуль).
- Этап 2 (удаление хуков) → Этап 3 (регистрация в settings.json — нужно знать какие удалить).
- Этап 6 (brain-retro adaptation) → после Этапа 3 (нужны real data из нового gate).
- Этап 2.1.0 (smoke-tests) → Этап 2.1 (subagent inheritance) → Этап 2.2 (subagent constraints + block-file) → Этап 2.3 (branch-switch AskUser-gate).

**Параллелизуемое:**

- Этапы 1.1 / 1.2 / 1.3 / 1.4 / 1.5 / 1.6 / 1.7 / 1.8 — независимые подмодули, можно делегировать одновременно (max 3 параллельных Task tool вызовов per §3.4).
- Этап 2 (удаление хуков) — параллелен с этапами 1.X (если 1.X не зависит от удаляемых хуков).

**Оценочное wall-clock с параллелизмом:** 6-9 часов вместо 22.5-32 sequential. Требует чёткой координации между субагентами и хорошего decomposition в writing-plans.

---

## 11. Источники

- **Предшественник:** [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 28.05.2026).
- **Audit-история v3.5:** [docs/superpowers/specs/2026-05-28-router-gate-hard-wall-design.md](2026-05-28-router-gate-hard-wall-design.md) — оригинал v1 → v3.5 с полной §11 «История версий» (180 строк) + блоки «Changes vN→vN+1» в TL;DR. Этот condensed-спек удалил их для plan writing; full audit-trail в git log на оригинале и в [docs/observer/notes/2026-05-28-brain-retro-10.md](../../observer/notes/2026-05-28-brain-retro-10.md) / [docs/observer/notes/2026-05-28-self-retrospect-2.md](../../observer/notes/2026-05-28-self-retrospect-2.md).
- **Текущая enforcement-архитектура:** 5 PreToolUse хуков + vocab.json в `tools/` — будут удалены в Этапе 2.
- **Pravila §16 brain governance, §17 universal skill-coverage** — будут обновляться отдельной задачей через `claude-md-management` после этого эпика.
