diff --git a/docs/superpowers/specs/2026-06-09-router-mentor-stage3-shadow-replay-design.md b/docs/superpowers/specs/2026-06-09-router-mentor-stage3-shadow-replay-design.md new file mode 100644 index 00000000..0592665a --- /dev/null +++ b/docs/superpowers/specs/2026-06-09-router-mentor-stage3-shadow-replay-design.md @@ -0,0 +1,156 @@ +# 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 (журнал)** — субстрат, наполняется при работе остальных; целостность через `verifyChain`. + +## 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({ journal, episodes, riskyFixture })` — собирает события: безобидные из журнала М1 + и эпизодов наблюдателя + рискованные из курированной фикстуры. Возвращает массив + `{ 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, в одну сторону):** +`журнал М1 + эпизоды + фикстура рискованных` → `buildCorpus` → `runMachine ×6` → `classifyOutcome` +→ `renderReport` → `отчёт.md`. Обратно в систему — ничего; машины не вызываются как хуки. + +**Сигнатуры машин (считаны 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)` — для обкатки НЕ зовём живьём, а читаем уже залогированные рекомендации эпизодов. +- М1 `action-journal` — источник корпуса + `verifyChain` (целостность). + +## 4. Корпус событий и честная разбивка «реальное vs синтетика» + +- **Безобидные события** — из журнала М1 (`action-journal`) и эпизодов + (`docs/observer/episodes-*.jsonl`). Кросс-сессионные транскрипты НЕ читаем (под read-deny + exfil). +- **Рискованные события** — курированная фикстура (написана руками): push в `.env`, Write вне + плана, разрушительный `Remove-Item`, мутация мимо двери владельца. Реальные опасные действия НЕ + выполняются — только описываются как вход. + +| Машина | Безобидные | Рискованные | Реальность данных | +|---|---|---|---| +| М5 пол | реальные из журнала/эпизодов | синтетика-фикстура | в основном реальные | +| М6 снимок/escape | реальные из журнала/эпизодов | синтетика-фикстура | в основном реальные | +| М2 стена | синтетика (план+шаги) | синтетика | полностью синтетика — нет живой истории | +| М4 судья | синтетика (mode/verdict) | синтетика | синтетика + судейство не подключено (A1) | +| М3 роутер | реальные логи эпизодов (советовал ↔ взяли) | — (не блокирует) | реальные, без новых LLM-вызовов | +| М1 журнал | — | — | целостность через `verifyChain` | + +**Честно:** для М5/М6/М3 обкатка на реальных данных; для М2/М4 — на синтетике, потому что эти +стены ещё не работали живьём (реальной истории нет). Синтетика проверяет логику решений +(in-plan→allow, out-of-plan→block, observe-only→allow); реальное «ловит ли на живом» для М2/М4 +подтвердится на Этапе 4 (М2 — когда пойдут реальные планы; М4 — после A1). + +## 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 — рискованное пропущено). Внизу — сводка по +6 машинам и общий вывод «готово / не готово к Этапу 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` — парсит журнал/эпизоды + грузит фикстуру; +- `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. Допущения и открытые мелочи + +- Точные форматы записей журнала М1 и эпизодов под `buildCorpus` уточняются в шаге + `audit-context-building` (шаг 2 цепочки). +- Размер курированной фикстуры рискованных — минимально-достаточный (по 2–4 рискованных на каждую + блокирующую машину), уточняется в `writing-plans`. +- Отчёт коммитим как docs-only артефакт-доказательство первого прогона (без push).