# План: оркестратор «наставник → судья» (строгая последовательность)

> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:test-driven-development (inline, RED→GREEN). НЕ субагентами — деликатное ядро стены.
> Спека: `docs/superpowers/specs/2026-06-13-seal-race-mentor-then-judge-design.md` (секции D1/D2/D3).

**Goal:** Один PostToolUse Write-хук гонит наставника ДО КОНЦА, потом судью — судья читает свежий mentor-GO/вердикт, гонка печати уходит.

**Architecture:** Новый `tools/enforce-mentor-then-judge.mjs` запускает машины (enforce-mentor-on-plan-write / enforce-judge-gate) как дочерние процессы последовательно, событие пробрасывает в stdin. Машины байт-в-байт не меняются. Регистрация в settings.json — шаг владельца.

**Tech Stack:** Node ESM (.mjs), child_process, vitest (tools-config). Регрессия: `npx vitest run --root app --config vitest.config.tools.mjs` (база 3901).

## Цель

Обеспечить «судья строго после наставника» в одном процессе → mentor-GO/вердикт всегда свежие → печать встаёт за одну церемонию.

```skills-json
["test-driven-development"]
```

## Шаги (wall-исполнимый TDD-скрипт)

Каждый шаг — отдельный шаг плана с `ref` на решение спеки. D1 — оркестратор+ядро; D2 — exit-семантика; D3 — машины целы + регрессия.

```steps-json
[{"n":1,"op":"Write","object":"tools/enforce-mentor-then-judge.test.mjs","ref":"D1"},
{"n":2,"op":"Bash","object":"npx vitest run --root app --config vitest.config.tools.mjs enforce-mentor-then-judge","ref":"D1"},
{"n":3,"op":"Write","object":"tools/enforce-mentor-then-judge.mjs","ref":"D1"},
{"n":4,"op":"Bash","object":"npx vitest run --root app --config vitest.config.tools.mjs enforce-mentor-then-judge","ref":"D2"},
{"n":5,"op":"Bash","object":"npx vitest run --root app --config vitest.config.tools.mjs","ref":"D3"}]
```

## Откат

`git restore --staged tools/enforce-mentor-then-judge.*` + удалить файлы (новые, не закоммичены). Машины не трогались.

---

## Task 1 (ref D1/D2): чистое ядро `runMentorThenJudge` + оркестратор

**Files:**
- Create: `tools/enforce-mentor-then-judge.mjs`
- Test: `tools/enforce-mentor-then-judge.test.mjs`

- [ ] **Шаг 1: Падающий тест.** Создать `tools/enforce-mentor-then-judge.test.mjs`:

```js
// tools/enforce-mentor-then-judge.test.mjs
import { describe, it, expect } from 'vitest';
import { runMentorThenJudge } from './enforce-mentor-then-judge.mjs';

describe('runMentorThenJudge — строгая последовательность наставник→судья', () => {
  it('наставник NO-GO (exit 2) → block с его stderr, СУДЬЯ НЕ запускается (круг L1)', async () => {
    const calls = [];
    const spawnHook = async (script) => {
      calls.push(script);
      return /mentor/.test(script) ? { code: 2, stderr: 'замечание наставника' } : { code: 0, stderr: '' };
    };
    const r = await runMentorThenJudge('{"x":1}', { spawnHook });
    expect(r.block).toBe(true);
    expect(r.message).toMatch(/замечание наставника/);
    expect(calls).toHaveLength(1);
    expect(calls[0]).toMatch(/mentor/);
  });
  it('наставник GO → судья запускается ПОСЛЕ, его GO → allow', async () => {
    const calls = [];
    const spawnHook = async (script) => { calls.push(script); return { code: 0, stderr: '' }; };
    const r = await runMentorThenJudge('{"x":1}', { spawnHook });
    expect(r.block).toBe(false);
    expect(calls).toHaveLength(2);
    expect(calls[0]).toMatch(/mentor/);
    expect(calls[1]).toMatch(/judge/);
  });
  it('наставник GO → судья NO-GO (exit 2) → block с замечанием судьи', async () => {
    const spawnHook = async (script) => (/judge/.test(script) ? { code: 2, stderr: 'замечание судьи' } : { code: 0, stderr: '' });
    const r = await runMentorThenJudge('{"x":1}', { spawnHook });
    expect(r.block).toBe(true);
    expect(r.message).toMatch(/замечание судьи/);
  });
  it('порядок строгий: судья вызван ТОЛЬКО после наставника', async () => {
    const order = [];
    const spawnHook = async (script) => { order.push(/mentor/.test(script) ? 'M' : 'J'); return { code: 0, stderr: '' }; };
    await runMentorThenJudge('{"x":1}', { spawnHook });
    expect(order).toEqual(['M', 'J']);
  });
  it('наставник «упал» (code 1, не 2) → не блок, судья всё равно запускается (producer fail-safe)', async () => {
    const calls = [];
    const spawnHook = async (script) => { calls.push(script); return /mentor/.test(script) ? { code: 1, stderr: 'crash' } : { code: 0, stderr: '' }; };
    const r = await runMentorThenJudge('{"x":1}', { spawnHook });
    expect(r.block).toBe(false);
    expect(calls).toHaveLength(2);
  });
});
```

- [ ] **Шаг 2: Прогон — FAIL.** `npx vitest run --root app --config vitest.config.tools.mjs enforce-mentor-then-judge` → модуль не найден / runMentorThenJudge не определён.

- [ ] **Шаг 3: Реализация.** Создать `tools/enforce-mentor-then-judge.mjs`:

```js
#!/usr/bin/env node
/**
 * enforce-mentor-then-judge — оркестратор строгой последовательности «наставник → судья».
 * Заменяет ДВА параллельных PostToolUse Write-хука одним: наставник до конца, ПОТОМ судья
 * (читает свежий mentor-GO/вердикт). Машины enforce-mentor-on-plan-write / enforce-judge-gate
 * НЕ меняются — запускаются как дочерние процессы последовательно, событие в stdin.
 * Регистрация (один блок вместо двух) — шаг владельца.
 */
import { readStdin, exitDecision } from './enforce-hook-helpers.mjs';
import { spawn } from 'node:child_process';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
export const MENTOR_HOOK = join(__dirname, 'enforce-mentor-on-plan-write.mjs');
export const JUDGE_HOOK = join(__dirname, 'enforce-judge-gate.mjs');

/**
 * Запустить хук-процесс, передать событие в stdin, вернуть {code, stderr}.
 * Протокол (exitDecision): block → stderr + exit 2; allow → stdout '{}' + exit 0.
 * stdout дочки гасим (не льём в свой stdout). Не смог запуститься → code 0 (producer fail-safe).
 */
export function spawnHookProc(scriptPath, eventJson) {
  return new Promise((resolve) => {
    let ch;
    try { ch = spawn(process.execPath, [scriptPath], { stdio: ['pipe', 'pipe', 'pipe'] }); }
    catch { resolve({ code: 0, stderr: '' }); return; }
    let stderr = '';
    ch.stderr.on('data', (d) => { stderr += String(d); });
    ch.on('close', (code) => resolve({ code: code == null ? 0 : code, stderr: stderr.trim() }));
    ch.on('error', () => resolve({ code: 0, stderr: '' }));
    try { ch.stdin.write(eventJson); ch.stdin.end(); } catch { /* дочка закрыла stdin */ }
  });
}

/**
 * Чистое ядро: наставник ДО КОНЦА, потом судья (строго после, свежий mentor-GO/вердикт).
 * Наставник exit-2 → block с его stderr, судья НЕ запускается (круг L1 первым). Наставник
 * allow (любой не-2 код — producer fail-safe) → судья запускается, его решение наружу.
 */
export async function runMentorThenJudge(eventJson, { spawnHook } = {}) {
  const m = await spawnHook(MENTOR_HOOK, eventJson);
  if (m && m.code === 2) return { block: true, message: m.stderr };
  const j = await spawnHook(JUDGE_HOOK, eventJson);
  return { block: !!(j && j.code === 2), message: (j && j.code === 2) ? j.stderr : undefined };
}

async function main() {
  try {
    const raw = await readStdin();
    const r = await runMentorThenJudge(raw, { spawnHook: spawnHookProc });
    exitDecision({ block: r.block, message: r.block ? r.message : undefined });
  } catch {
    exitDecision({ block: false }); // обёртка-оркестратор никогда не кирпичит на своей ошибке
  }
}

const isCli = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
if (isCli) main();
```

- [ ] **Шаг 4: Прогон — PASS.** `npx vitest run --root app --config vitest.config.tools.mjs enforce-mentor-then-judge` → 5 тестов зелёные.

---

## Task 2 (ref D3): регрессия + проверка целостности машин

- [ ] **Шаг 5: Полная регрессия.** `npx vitest run --root app --config vitest.config.tools.mjs`
  Expected: весь набор зелёный (база 3901 + 5 новых тестов оркестратора), 0 регрессий. Машины не трогались (git diff enforce-mentor-on-plan-write.mjs / enforce-judge-gate.mjs пуст). RED → systematic-debugging. Затем superpowers:verification-before-completion. Регистрация оркестратора в settings.json — шаг владельца (вне этого плана).

```verified-context-json
[{"id":"1","kind":"EXTRACTED","claim":"логика наставника экспортируется как runMentorOnPlanWrite","ref":"tools/enforce-mentor-on-plan-write.mjs:101","anchor":"export async function runMentorOnPlanWrite"},
{"id":"2","kind":"EXTRACTED","claim":"логика судьи экспортируется как runJudgeTurn","ref":"tools/enforce-judge-gate.mjs:278","anchor":"export async function runJudgeTurn"},
{"id":"3","kind":"EXTRACTED","claim":"exitDecision: block→stderr+exit2, allow→stdout+exit0 (протокол дочки)","ref":"tools/enforce-hook-helpers.mjs:360","anchor":"export function exitDecision"}]
```
