docs(router-mentor): A1 judge gate2 wiring design spec
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
# A1 — Проводка судьи М4 на Гейт-2 (план), за флагом, $0 — дизайн
|
||||
|
||||
**Дата:** 2026-06-09 · **Эпик:** «роутер-наставник» · **Ветка:** `worktree-brainrepo`
|
||||
**Кодовая фраза:** «роутер-наставник» · **Режим:** commit-not-push.
|
||||
**Хвост реестра:** A1 / R-33 (часть). **Зависимость порядка:** A1 → A6 → A7 (судья раньше наставника).
|
||||
|
||||
---
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Оживить судью Машины 4 на **одном** продукте — **плане реализации (Гейт-2)** — за рубильником
|
||||
владельца, с нулевым расходом ($0) до включения. Это первая живая проводка судьи; объём намеренно
|
||||
минимальный (один гейт), чтобы безопасно обкатать путь «детект → извлечение → модель → вердикт →
|
||||
журнал» в режиме shadow до перевода в block.
|
||||
|
||||
**Вне scope A1** (делается позже): Гейт-1 (спека), Гейт-3 (результат), A2 (divergence/destructive),
|
||||
правка пола/стен, регистрация в `settings.json` (рубильник владельца), наставник М3 (A7).
|
||||
|
||||
## 2. Контекст (что уже есть)
|
||||
|
||||
- `judge-engine.runJudge({ functionName, requiredLenses, llmCall, promptArgs })` — думающая часть
|
||||
собрана: `llmCall(prompt) → {decision, slots, objections}` инъектируется; механика вокруг (валидация
|
||||
слотов, под-прогоны, якоря, обратимость) fail-closed. `buildJudgePrompt({functionName, requiredLenses,
|
||||
product, goal, cards})` — чистый.
|
||||
- `judge-gate-config.judgeGateMode()` — режим `inert` ($0, нет флага/ключа) / `shadow` (active,
|
||||
логирует, НЕ блокирует — D28) / `live-block` (active + MODE=block, блокирует на NO-GO).
|
||||
`judgeActive` = флаг `ROUTER_MENTOR_JUDGE_ENABLED=1` И ключ keychain `router-mentor-judge`.
|
||||
- `judge-orchestrator.finalGate({judgeDecision, floorBlocked})` — пол перевешивает «да» судьи;
|
||||
`logVerdict({verdict, nowMs, journal})` — append-only журнал вердиктов (J8).
|
||||
- `enforce-judge-gate` — тонкая обёртка-рубильник. Шов `runJudgeGate(_event)` сейчас заглушка
|
||||
`{decision:'GO', wired:false}`. Регистрировать в `settings.json` нужно **обёртку**, не движок.
|
||||
- Транспорт-эталон — `callAnthropicAPI` (как у классификатора), модель `CLASSIFIER_MODEL` (Sonnet).
|
||||
|
||||
## 3. Находка, которую дизайн обязан закрыть
|
||||
|
||||
Текущий `enforce-judge-gate.main()` в режиме `shadow` **не вызывает** `runJudgeGate` — сразу `allow`
|
||||
(строка 50). Это противоречит контракту shadow из runbook («судья пишет вердикты в журнал, но НЕ
|
||||
блокирует»). **A1 чинит main():** shadow обязан прогонять судью и логировать вердикт, но всегда
|
||||
пропускать.
|
||||
|
||||
## 4. Компоненты
|
||||
|
||||
### 4.1 `extractGate2Product(event)` — детектор + извлечение (чистая)
|
||||
|
||||
Смотрит harness-событие. Возвращает либо `{ shouldJudge:false }`, либо
|
||||
`{ shouldJudge:true, functionName:'gate2', product, goal, cards }`.
|
||||
|
||||
- **Срабатывает только** если событие — запись плана: `tool ∈ {Write, Edit, MultiEdit}` И путь под
|
||||
`docs/superpowers/plans/*.md` (нормализованный, прямые/обратные слэши). Иначе `shouldJudge:false`.
|
||||
- `product` = текст плана из `tool_input` (Write → `content`; Edit/MultiEdit → `new_string`/итог).
|
||||
План виден ещё до записи (PreToolUse) — это и есть «продукт на суд».
|
||||
- `goal` = best-effort цель плана: его секция цели (`## Цель`/`## Goal`/первый абзац) или пустая
|
||||
строка, если не нашли (движок держит пустой якорь).
|
||||
- `cards` = `[]` (для A1; карточки навыков для оценки плана не критичны — их подаёт наставник на A7).
|
||||
|
||||
Защита от over-block встроена сюда: на обычные тулы `shouldJudge:false` → модель не зовётся, $0.
|
||||
|
||||
### 4.2 `buildJudgeLlmCall({ apiKey, model, transport })` — фабрика живого `llmCall`
|
||||
|
||||
Возвращает `llmCall(prompt) → {decision, slots, objections}`:
|
||||
- зовёт `transport` (по умолчанию `callAnthropicAPI`) с `{system, user}` из `buildJudgePrompt`,
|
||||
ключ `apiKey` (keychain `router-mentor-judge`), модель `model` (Sonnet);
|
||||
- парсит ответ модели (ожидается JSON со `slots`/`objections`/`decision`) в строгую форму;
|
||||
- **битый/непарсящийся ответ → форма, которую `runJudge` отвергает** (невалидные слоты → NO-GO).
|
||||
Fail-closed: модель не штампует GO мимо механики.
|
||||
|
||||
`transport` инъектируется (для тестов мок; в проде — `callAnthropicAPI`).
|
||||
|
||||
### 4.3 `runJudgeGate(event, deps)` — оркестрация шва
|
||||
|
||||
`deps = { mode?, key?, model?, transport?, journal?, nowMs? }` (инъекция; по умолчанию — реальные
|
||||
резолверы конфига/ключа).
|
||||
|
||||
Порядок:
|
||||
1. `judgeActive` (флаг + ключ). **Нет → нейтральный GO `{decision:'GO', wired:false}`, $0** (ни одного
|
||||
вызова модели).
|
||||
2. Активен → `extractGate2Product(event)`. `shouldJudge:false` → нейтральный GO, $0.
|
||||
3. `shouldJudge:true` → `judge-engine.runJudge({ functionName:'gate2',
|
||||
requiredLenses: requiredLensesFor('gate2'), llmCall: buildJudgeLlmCall(...), promptArgs:{product, goal, cards} })`.
|
||||
Вернуть `{ decision, wired:true, verdict }`.
|
||||
|
||||
Любой бросок внутри → пробрасывается в дисциплинарный обработчик main() (fail-closed в live-block).
|
||||
|
||||
### 4.4 `main()` — три режима (правка находки §3)
|
||||
|
||||
- `inert` → `allow`, без прогона ($0).
|
||||
- `shadow` → `runJudgeGate(event)` (живой llmCall, если есть что судить) → `logVerdict(...)` в журнал J8
|
||||
→ **ВСЕГДА `allow`** (D28).
|
||||
- `live-block` → `runJudgeGate(event)` → `logVerdict(...)` → `decide({mode, verdict, floorBlocked})`
|
||||
(`finalGate`: NO-GO → блок; пол перевешивает).
|
||||
|
||||
Спенд во всех ветках за `judgeActive` → по умолчанию (нет флага/ключа) `inert`, $0.
|
||||
|
||||
## 5. Поток данных
|
||||
|
||||
```
|
||||
PreToolUse event
|
||||
│ main(): judgeGateMode()
|
||||
├─ inert ──────────────────────────► allow ($0)
|
||||
├─ shadow ─► runJudgeGate ─► extract gate2? ──нет──► neutral GO ($0) ─► log? нет ─► allow
|
||||
│ └─да─► runJudge(live llmCall) ─► verdict ─► logVerdict ─► allow (D28)
|
||||
└─ live-block ─► runJudgeGate ─► (то же) ─► verdict ─► logVerdict ─► finalGate ─► allow|block
|
||||
```
|
||||
|
||||
## 6. Обработка ошибок
|
||||
|
||||
- Нет ключа/флага → `inert`/$0 (по конструкции).
|
||||
- Парс ответа модели упал → невалидные слоты → `runJudge` отдаёт NO-GO (fail-closed). В shadow это
|
||||
лишь логируется; в live-block — блок (но пол всё равно перевешивает через `finalGate`).
|
||||
- Бросок транспорта/извлечения → в `live-block` дисциплинарный обработчик main() даёт блок
|
||||
(fail-closed, как сейчас); в `shadow` — `allow` (shadow не блокирует никогда), вердикт в журнал не
|
||||
пишем при упавшем прогоне (логируем только состоявшийся вердикт).
|
||||
- `judgeActive` истинно, но `shouldJudge:false` → нейтральный GO, журнал не трогаем (нечего судить).
|
||||
|
||||
## 7. Тестирование (TDD, наблюдаемый RED)
|
||||
|
||||
Чистые функции — детерминированно:
|
||||
- `extractGate2Product`: план-путь срабатывает; не-план / не-Write / иной путь → `shouldJudge:false`;
|
||||
извлечение product/goal из Write и Edit; нормализация слэшей.
|
||||
- `buildJudgeLlmCall`: валидный JSON-ответ → `{decision,slots,objections}`; битый ответ → форма,
|
||||
которую `runJudge` отвергает (NO-GO).
|
||||
- `runJudgeGate`: не активен → `{wired:false}`, транспорт не дёрнут (мок-счётчик 0); активен +
|
||||
не-план → `{wired:false}`, транспорт не дёрнут; активен + план → `runJudge` с живым llmCall, вердикт.
|
||||
- `main()` (инъекция конфига/транспорта/журнала): inert → allow без прогона; shadow → прогон + лог +
|
||||
allow; live-block GO → allow; live-block NO-GO → block; пол blocked → block независимо от судьи.
|
||||
|
||||
Спенд-инвариант (ключевой): **без флага/ключа транспорт не вызывается ни в одной ветке** (мок-счётчик
|
||||
вызовов = 0). Регрессия tools-only vitest в конце (через Bash: `npx vitest run --root "<wt>/app"
|
||||
--config "<wt>/app/vitest.config.tools.mjs" <фильтр> --reporter dot`).
|
||||
|
||||
## 8. Границы и якоря
|
||||
|
||||
- A1 — только Гейт-2; не трогает Гейт-1/3, A2, пол, стены, нормативку, settings.json.
|
||||
- Регистрация обёртки + флаг + ключ + перевод shadow→block — **рубильник владельца** (runbook A1, шаги владельца).
|
||||
- commit-not-push. CLAUDE.md в worktree не трогаем (чужая зона).
|
||||
- Цепочка кода после спеки/плана: `audit-context-building → sharp-edges → variant-analysis →
|
||||
writing-plans → test-driven-development → verification-before-completion → regression`. НЕ
|
||||
`requesting-code-review`.
|
||||
Reference in New Issue
Block a user