# Router-mentor — Этап 3: shadow-обкатка через холостой прогон (design)

**Дата:** 2026-06-09 · **Эпик:** «роутер-наставник» · **Ветка:** `worktree-brainrepo` · commit-not-push.
**Статус:** дизайн на утверждение владельца.
**Источник:** реестр хвостов `2026-06-08-router-mentor-loose-ends-registry.md` (Этап 3) + handoff `2026-06-09-router-mentor-blockB-r08-handoff.md`.

---

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

Машины М1–М6 собраны и инертны до боевого включения (Фаза 8). Прежде чем включать их живьём,
владелец хочет **обкатать** их в режиме наблюдения — увидеть, как они РЕШАЛИ БЫ, не ломая рабочий
цикл. Прямой триггер — урок Layer 4 LLM-judge: живой судья сразу клинил `git status`/`log`
(over-block), потому что включился в hard-block без предварительного наблюдения.

**Критерий успеха (выбор владельца — «полная картина»):** по каждой машине видеть ОБА:
1. **over-block** — где машина ложно заблокировала бы безобидное действие;
2. **real-catch** — что на заведомо рискованных действиях она срабатывает как надо.
Право на Этап 4 (боевое включение) даёт отчёт, где over-block = 0 (или объяснены и приняты) и
рискованные действия ловятся (real-catch), без miss (рискованное → пропущено).

**Находка разведки (факт из кода, 2026-06-09):**
- Режим наблюдения «из коробки» есть только у **М4** (`judge-gate-config.mjs`: `inert` / `shadow`
  по умолчанию при активации / `live-block`).
- **М2 (стена)** и **М5 (пол)** — режима наблюдения нет, `decide()` сразу `allow`/`block`.
- **М6 (снимок/escape)** — режима нет.
- **М3 (роутер)** — ЖИВОЙ: `router-classifier.classify()` по умолчанию зовёт реальный
  `callAnthropicAPI` (ProxyAPI); мок только в тестах. Это recommender, не блокатор.
- **М1 (журнал)** — субстрат; живёт в read-protected `~/.claude/runtime` → ВНЕ scope harness;
  целостность уже покрыта своими unit-тестами (пин `5782ede3`).

## 2. Подход (выбор владельца — Подход 1)

**Холостой прогон (read-only).** Один новый инструмент `tools/shadow-replay.mjs` прогоняет корпус
событий через чистые `decide()` каждой машины и пишет отчёт. **Ничего не регистрирует как хук, не
трогает `settings.json`, не правит стены** — только импортирует их `decide()` и читает журналы.

Отвергнуто: Подход 2 (живой shadow судьи + холостой остальные — лишние шаги владельца, судейство
всё равно не подключено, хвост A1); Подход 3 (добавить флаг наблюдения в стены — правка несущего
кода стен, дороже всего, риск внести реальный обход).

## 3. Архитектура

Один read-only CLI-инструмент `tools/shadow-replay.mjs` (+ тест `shadow-replay.test.mjs`).
Компоненты — каждый одна понятная чистая функция:

- `buildCorpus({ episodes, benignFixture, riskyFixture })` — собирает события: безобидные из
  курированной фикстуры (вкл. over-block-склонные: readonly git/Read/Grep) + приближённые
  file-события из эпизодов; рискованные из курированной фикстуры. Runtime/журнал НЕ читаем
  (read-deny + sparse). Возвращает массив `{ id, kind: 'benign'|'risky', toolUse, context }`.
- `runMachine(machineId, corpus, deps)` — гоняет корпус через `decide()` одной машины; возвращает
  массив `{ event, decision }`. Чистая обёртка над уже существующими `decide()` (импорт, не правка).
- `classifyOutcome(event, decision)` — раскладывает результат в ведро:
  `over-block` (benign → block) / `real-catch` (risky → block) / `allow` (benign → allow) /
  `miss` (risky → allow — ТРЕВОГА).
- `renderReport(resultsByMachine)` — markdown-отчёт с вердиктом по машине и сводкой.
- `main()` — склейка: читает входы, пишет отчёт в файл, печатает сводку. fail-quiet, состояние
  НЕ пишет.

**Поток данных (read-only, в одну сторону):**
`эпизоды наблюдателя + фикстура (benign+risky)` → `buildCorpus` → `runMachine ×5` → `classifyOutcome`
→ `renderReport` → `отчёт.md`. Обратно в систему — ничего; машины не вызываются как хуки. Runtime
не читаем.

**Сигнатуры машин (считаны 2026-06-09, для вшивки):**
- М2 `supreme-gate.decide({ toolUse, frozenPlan, frozenArtifact, stepPtr, key, verifyImpl, verifyArtifactImpl, normalize })` → `{ decision, reason, advanceTo? }`.
- М5 `floor-decide.floorDecide({ toolUse, escapeGrants, escapeConsumed, now, normalizeImpl })`.
- М6 `snapshot-decide.snapshotNeeded(toolName, toolInput)` + escape-гранты (`escape-grant` / `floor-escape-consume`).
- М4 `enforce-judge-gate.decide({ mode, verdict, floorBlocked })` (gate-логика; `runJudgeGate` пока `GO/wired:false` — A1).
- М3 `router-classifier.classify(prompt, registry, options)` — для обкатки НЕ зовём живьём, а читаем уже залогированные рекомендации эпизодов (`classifier_output.recommended_chain` + `primary_rationale.node_chosen` + `task_classification`).
- М1 `action-journal` — ВНЕ scope harness (runtime read-protected); целостность под своим unit-тестом.

## 4. Корпус событий и честная разбивка «реальное vs синтетика»

- **Безобидные события** — курированная фикстура в репо (вкл. over-block-склонные: readonly
  `git status`/`git log`, Read, Grep, TodoWrite, EnterPlanMode, правка обычного файла) +
  приближённые file-события из эпизодов (`docs/observer/episodes-*.jsonl`: `task_size.files`,
  `events[].counts`). Журнал М1 и `~/.claude/runtime` НЕ читаем (read-deny + sparse: журнал хранит
  только `{op,object}` Skill-уровня, не per-событийные Bash-команды). Кросс-сессионные транскрипты
  НЕ читаем (read-deny + exfil).
- **Рискованные события** — курированная фикстура (написана руками): push в `.env`, Write вне
  плана, разрушительный `Remove-Item`, мутация мимо двери владельца. Реальные опасные действия НЕ
  выполняются — только описываются как вход.

| Машина | Безобидные | Рискованные | Реальность данных |
|---|---|---|---|
| М3 роутер | реальные логи эпизодов (советовал ↔ взяли) | — (не блокирует) | **реальные**, без новых LLM-вызовов |
| М5 пол | курированная фикстура + приближённые file-события эпизодов | синтетика-фикстура | курированные/приближённые |
| М6 снимок/escape | то же | синтетика-фикстура | курированные/приближённые |
| М2 стена | синтетика (план+шаги) | синтетика | полностью синтетика — нет живой истории |
| М4 судья | синтетика (mode/verdict) | синтетика | синтетика + судейство не подключено (A1) |
| М1 журнал | — (runtime read-protected) | — | ВНЕ scope harness; целостность под unit-тестом |

**Честно:** реально-данными подкреплён только **М3**. М5/М6 — курированная фикстура (намеренно
включает «обожжённые» over-block случаи: readonly git/Read/Grep — их не нужно ждать в реальном
логе) + приближённые file-события эпизодов. М2/М4 — синтетика, потому что эти стены ещё не
работали живьём (реальной истории нет). Это **не ослабляет цель**: over-block ловится курированной
фикстурой надёжнее, чем надеждой на лог; real-catch и так синтетика. Реальное «ловит ли на живом»
для М2/М4 подтвердится на Этапе 4 (М2 — когда пойдут реальные планы; М4 — после A1). М1 целостность —
свой unit-тест (пин `5782ede3`), в холостом прогоне не участвует.

## 5. Формат отчёта и расположение

Markdown-таблица + вердикт по каждой машине:

```
## М5 пол  —  GREEN
events: 142 | over-block: 0 | real-catch: 7/7 | allow: 135 | miss: 0

## М2 стена  —  YELLOW
events: 60 (синтетика) | over-block: 2 | real-catch: 12/12 | allow: 46 | miss: 0
  over-block #1: Edit app/Foo.php — reason "действие не в плане (ожидался шаг 3)"
  over-block #2: Bash "git log" — reason "разговорный режим"
```

Каждая ложная блокировка показывает событие + причину машины (`reason`), чтобы владелец глазами
оценил, правда ли это ложное срабатывание. Вердикты: **GREEN** (0 over-block, 0 miss) / **YELLOW**
(есть over-block — перечислены) / **RED** (есть miss — рискованное пропущено). Внизу — сводка по
5 машинам (М2/М3/М4/М5/М6) + строка-напоминание «М1: вне холостого прогона, целостность — unit-тест»
и общий вывод «готово / не готово к Этапу 4».

**Расположение:** `docs/observer/notes/<дата>-shadow-replay.md` (рядом с brain-retro заметками,
обычный `.md`). Перезапуск перезаписывает свежим прогоном.

**Запуск:** `node tools/shadow-replay.mjs` (все 6) или `--machine M2` (одна). Печатает сводку +
пишет отчёт. Ничего не регистрирует, состояние не пишет, fail-quiet.

## 6. Тестирование (TDD)

Новый код → по TDD, наблюдаемый RED до зелёного (честный RED, не характеризация — код новый):
- `buildCorpus` — парсит эпизоды + грузит фикстуру (benign+risky); runtime/журнал не читает;
- `classifyOutcome` — каждое ведро (over-block / real-catch / allow / miss) раскладывается верно;
- `runMachine` — по каждой машине известное событие → решение в нужном ведре (М2 in-plan→allow,
  out-of-plan→block; М5 разрушительное→block; М6 `snapshotNeeded`→true; М4 gate-логика по
  `{mode,verdict}`; М3 — расхождение «советовал↔взяли»);
- `renderReport` — стабильный markdown.
Фикстура рискованных = заодно вход для тестов. Регрессия tools-only остаётся GREEN + новые тесты.

## 7. Жёсткие ограничения

- Стены М2/М5/М4/М6 НЕ трогаем (только импорт `decide()`), не ослабляем.
- `settings.json` НЕ трогаем; инструмент не регистрируется как хук.
- Нормативку (Pravila/Tooling/PSR/MEMORY/CLAUDE.md) НЕ двигаем.
- commit-not-push (push только по слову «пуш»); чужую грязь дерева не коммитим — стейдж своих
  файлов поимённо.
- fail-quiet `main()`, состояние не пишет; отчёт — единственный выход.

## 8. Согласованная цепочка скилов (утверждена владельцем 2026-06-09)

1. `brainstorming` — этот дизайн (выполнено).
2. `audit-context-building` — построчно подтвердить точные форматы входов `decide()` каждой машины
   перед вшивкой.
3. `writing-plans` — план реализации `shadow-replay.mjs` (по функциям + фикстура).
4. `executing-plans` — исполнение инлайн по TDD (наблюдаемый RED → зелёный).
5. `verification-before-completion` — доказательства: новые тесты GREEN + регрессия tools-only GREEN.
6. `regression` (quick) — финальный sweep.

Сознательно НЕ включены: `sharp-edges` / `variant-analysis` (нет правки несущего кода стен —
read-only читатель); `requesting-code-review` (по правилу владельца).

## 9. Допущения и открытые мелочи

- Форматы входов подтверждены `audit-context-building` (2026-06-09): `buildCorpus` читает только
  эпизоды (`docs/observer/episodes-*.jsonl`) + фикстуру в репо; журнал М1/`~/.claude/runtime` НЕ
  читаем (read-deny + журнал хранит только `{op,object}` Skill-уровня). М1 — вне harness.
- Размер курированной фикстуры рискованных — минимально-достаточный (по 2–4 рискованных на каждую
  блокирующую машину), уточняется в `writing-plans`.
- Отчёт коммитим как docs-only артефакт-доказательство первого прогона (без push).
