# Машина 4 — Судья — план реализации (СОБРАНА 2026-06-06)

> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development / executing-plans. Этот файл — историческая запись: Машина 4 **уже собрана** контроллером (Opus) сам, по TDD, в worktree `worktree-brainrepo`, коммит `079adfd1` (28 файлов, +1503). Здесь — карта семи под-планов 4-A…4-G, их модули, контракты и хвосты.

**Goal:** построить судью роутер-наставника — критика (не исполнителя), который выносит мотивированные возражения и пропускает/блокирует на пяти функциях (Гейт-1 спека / Гейт-2 план / A2 надзор / приёмка части / Гейт-3 результат↔цель), не переопределяя детерминированный пол (J9).

**Architecture:** «механика отдельно от думающего» (как Машина 3). Чистые pure-модули + персист с инъекцией I/O (fs/keychain), без LLM в проде-проверках; думающая часть — построитель промпта (чистый) + мокнутый вызов модели + механические проверки вокруг. Стоит на М1 (журнал/подпись/keychain), М2 (печать `freezeArtifact`/`freezePlan`, состояние «заморожен»), М3 (граф узлов / контракты / машина охвата — чтение). Файлы судьи `tools/judge-*.mjs` + `tools/enforce-judge-gate.mjs` — непересечение с М1/М2/М3 по построению.

**Tech Stack:** Node.js ESM, `node:crypto`, `keytar` (опц., lazy), vitest tools-only. Импорт М1: `action-journal` (`appendEntry`/`verifyChain`), `receipt-sign` (`canonicalJson`).

**Источник истины:** `docs/superpowers/specs/2026-06-05-router-mentor-machine-4-judge-design.md` (полный дизайн, фикс-сет F1–F10, проверка против 26 хуков) + мастер-карта §«МАШИНА 4» (K1–K7, A0–A3, J1–J10) + K5-спека (`2026-06-04-router-mentor-m3-shape-and-k4-design.md`) + гейт-1/K7 (`2026-06-05-router-conversational-input-and-emergent-catch-design.md`).

**Порядок сборки:** 4-A → (4-B ∥ 4-C) → 4-D → 4-E → 4-F → 4-G.

**Верификация (2026-06-06):** полная регрессия tools-only **2485 GREEN / 2 skipped**; прогон всех тестов судьи **228 GREEN**; 117 новых тестов в файлах судьи. Команда: `npx vitest run --root "<worktree>/app" --config vitest.config.tools.mjs`.

---

## 4-A — Пол судьи (дисциплина) — 6 модулей + сводный инвариант, 41 тест

Тупо-механический пол честности самого судьи (§9, §11, F1/F2). Чистый TDD.

- [x] **`judge-key-config.mjs`** — `resolveJudgeKey({env, keychainGet})`. Отдельный ключевой домен судьи (keychain `router-mentor-judge` → env `ROUTER_MENTOR_JUDGE_KEY` → null; R-31). Контроллеру недоступен → не подделает прилежность. (6 тестов)
- [x] **`judge-subrun-journal.mjs`** (F1, §9.1) — `appendJudgeSubRun` / `loadJudgeSubRuns` / `verifyJudgeJournal` / `requiredSubRunsPresent`. Judge-only файл `judge-subrun-<sess>.jsonl`, цепочка+голова подписаны ключом судьи (переиспользует `appendEntry`/`verifyChain` М1). Контроллер чужим ключом валидную запись не сделает; нет под-прогона требуемой линзы → вердикт фейк. (8)
- [x] **`judge-verdict-slots.mjs`** (§9.2) — `validateVerdictSlots(verdict, requiredLenses, {minLen})`. Отдельный валидатор формы: пустой/тривиальный слот → невалидно → ГО не выдаётся. (5)
- [x] **`judge-anchor.mjs`** (§9.3/§11.1) — `classifyObjection` / `partitionObjections` / `ANCHOR_KINDS`. Якорное возражение может блокировать; безъякорное → авто-в-совет (обе стороны: голос-призрак не клинит ворота). (7)
- [x] **`judge-watermark.mjs`** (K6, §11/§2, F2) — `readWatermark` / `bumpWatermark` / `checkNoRollback`. Монотонная зарубка в OS-keychain (`router-mentor-judge-watermark`), per-session; current < виденного → откат → блок; свежая сессия = генезис 0. (6)
- [x] **`judge-slop-counter.mjs`** (§9.5) — `computeSlopStats(verdicts, {threshold})`. Частота голых ГО / тонких вердиктов / пропусков линзы → флаг систематической халтуры. (4)
- [x] **`m4a-discipline-invariants.test.mjs`** — сквозные инварианты пола. (5)

---

## 4-B — Печать-канал гейта — 1 модуль, 10 тестов

`judge-seal-channel.mjs` (K1/F4/F5/F6, §6). Единственная разрешённая запись судьи — печать на ГО.

- [x] `contentHash(obj)` — хеш ровно содержания (для F4-сверки).
- [x] `sealOnApproval({artifact, verdict, key, nowMs, freezeImpl})` — печать только на `decision==='GO'`; **F4**: `verdict.judged_hash !== contentHash(artifact)` → подмена после суда → печати нет; carve-out: деньги/тупик → `ownerRequired`; печать делегируется injected `freezeImpl` (в проде — `freezeArtifact` М2, ESM-импортом, не require).
- [x] `detectMoney` / `requiresOwnerSeal` — осторожный детерминированный детектор (без \b у кириллицы).
- [x] `applyWaivers(objections, waivers)` (F6) — снятое владельцем не пере-блокируется ниже.

---

## 4-C — Механический пол гейтов ($0-проверки) — 1 модуль, 15 тестов

`judge-gate-floor.mjs` (K2/K5/F7). Бесплатные «было/не было», переиспользуют М1/М2/М3.

- [x] `existenceCheck` (Гейт-1 шаг 1) — вопросы отвечены + обещанные навыки реально вызваны по журналу.
- [x] `specToPlanCoverage` (Гейт-2) — каждый § спеки покрыт ≥1 шагом плана.
- [x] `k5CriterionCheck` (Гейт-2, K5) — значимый шаг → конкретный критерий; голое «проверено/готово» за факт не берём; значимость осторожна (сомнение → значим).
- [x] `criteriaGreenMatched` (Гейт-3) — критерий ↔ настоящий ЗЕЛЁНЫЙ прогон (красный/отсутствие → не доказан).
- [x] `skillTakenByJournal` (A2, K2) — навык взят по журналу вызовов, не по тексту.

---

## 4-D — Думающая часть судьи — 1 модуль, 16 тестов

`judge-engine.mjs` (§7/§8/§10, K7, G1). Построитель промпта чистый, вызов модели мокается, проверки вокруг механические.

- [x] `VOTE_LAYOUTS` + `requiredLensesFor(fn,{risk})` — раскладка голосов по 5 функциям (§7); риск добавляет Атакующего и Деньги.
- [x] `PREMORTEM_CLASSES` (§10) — фиксированные классы провала (общая карта, не растущий список); K7 = премортем-слот на Гейте-1.
- [x] `buildJudgePrompt({...})` — чистый детерминированный `{system, user}`; слеп к переписке по конструкции (на входе только продукт+цель+карточки); K5-напоминание «план — будущее».
- [x] `classifyByReversibility` (G1) — фатальное = тяжёлое И необратимое → блок; иначе совет.
- [x] `runJudge({...llmCall})` — один вердикт + механический пост-чек: слоты валидны (4-A) / под-прогоны на месте (4-A) / якорь+необратимость решают, что реально блокирует. Модель не проштампует ГО мимо механики.
- [x] `consensusDecision(verdicts)` (J3) — высокий риск: один НЕТ → блок.

---

## 4-E — Оркестрация 5 функций + машинерия — 1 модуль, 16 тестов

`judge-orchestrator.mjs` (§3/§6/§12, A0, J1/J2/J5/J8/J9, F8/F10). Чистая логика сборки.

- [x] `gateFires({ready, alreadySealed})` (A0) — ворота срабатывают структурно от состояния, не «по желанию».
- [x] `runGateLadder(steps)` — лесенка (существование $0 → верность → судья); провал любого → стоп раньше.
- [x] `finalGate({judgeDecision, floorBlocked})` (J9) — «да» судьи не отпирает пол; пол перевешивает.
- [x] `appealStep({round, maxRounds})` (F8) — 3 круга на одно несогласие → к владельцу.
- [x] `judgeHealth({available, degraded})` (J5) — упал/деградировал → пост пустой + крик + режим пола.
- [x] `gate3Terminal(verdict)` (F10) — Гейт-3 терминален, печати-вниз нет.
- [x] `logVerdict({verdict, nowMs, journal})` (J8) — каждый вердикт append-only (инъекция).

---

## 4-F — Оценщик постфактум — 1 модуль, 5 тестов

`judge-evaluator.mjs` (D31, §9.4). `evaluatePostfactum({verdicts, surfaced, threshold})` — сверяет прошлые GO судьи И роутера с тем, что вылезло ниже; промах = проблема, пропущенная GO-вердиктом; флаг систематической халтуры по следу; разбивка по источнику.

---

## 4-G — Хук-обёртка (инертна, $0) + сквозные проверки — 14 тестов

- [x] **`judge-gate-config.mjs`** — `resolveJudgeEnabled` / `judgeActive`. Судья активен только при флаге `ROUTER_MENTOR_JUDGE_ENABLED=1` И ключе; иначе $0/инертен (D28). (5)
- [x] **`enforce-judge-gate.mjs`** — тонкая хук-обёртка. `decide({active})` никогда не блокирует на этом этапе (D28 «сперва тихо»); `main()` fail-open пока инертна. Регистрировать в settings.json нужно ОБЁРТКУ (не движок). (2)
- [x] **`m4-judge-invariants.test.mjs`** — сквозной инвариант: Гейт-1 happy-path (существование→судья GO→печать→пол пропускает); фатально-необратимое возражение → NO-GO → печати нет; J9 (GO + floorBlocked → block); F1 (чужой ключ → журнал невалиден); лесенка стоп-раньше; деградация. (7)

---

## Хвосты — шаг ВЛАДЕЛЬЦА (live-wiring, НЕ код судьи; D28)

1. **Ключ судьи** — положить в keychain (`router-mentor-judge`) ИЛИ env `ROUTER_MENTOR_JUDGE_KEY`. Без него подпись=null, судья инертен (fail-closed).
2. **Рубильник** — `ROUTER_MENTOR_JUDGE_ENABLED=1`.
3. **Регистрация** `enforce-judge-gate.mjs` в `.claude/settings.json` (Claude'у settings.json закрыт).
4. **Живой режим** — заменить мокнутый `llmCall` на реальный `callAnthropicAPI` + подключить orchestrator к реальному `freezeArtifact`/`freezePlan` М2 (`freezeImpl`) + загрузку frozen-плана/артефакта; сперва тихий режим (логирует, не блокирует), потом включение.
5. **K4-исключение** в стене М2 (`decideMode`) активировать ПОСЛЕ реального K1/печати.

## Решения / границы
- Ключ судьи отдельный (`router-mentor-judge`), не общий с расписками — доменное разделение R-31.
- Печать делегируется injected `freezeImpl` — 4-B не лезет во внутренний формат заморозки М2 (развязка + анти-коллизия с параллельной М3).
- §15 дизайна закрыл Хайку/консенсус: одиночные ворота Sonnet, J3-консенсус (с Хайку) только на высоком риске.
- Дисциплина #4 (оценщик D31) — это 4-F (постфактум); J6 (защищённый файл правил судьи) — конфиг/owner, не runtime-модуль.

**Регрессия после всей Машины 4: 2485 GREEN / 2 skipped. Коммит `079adfd1`.**
