# Активационная обёртка роутера-наставника Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans task-by-task (субагенты в этом контуре запрещены правилами владельца). Steps — чек-боксы (`- [ ]`).

## Цель

Дать собранному наставнику (Фаза B, runbook Этап 1) исполняемую «входную дверь»: PostToolUse-обёртка
на запись плана производит вердикт наставника + журнал переговоров + task-id и персистит их;
freeze-gate аддитивно встаёт в путь печати судьи — печать плана без содержательного вердикта,
binding plan_hash и чистого проверенного контекста отклоняется (ТОЛЬКО при включённом рубильнике
наставника; выключен — печать как сегодня). Решения владельца 2026-06-12: флаг
`ROUTER_MENTOR_SEAM_ENABLED`; «зубы сразу»; `export PLAN_PATH_RE`; **контекст едет в плане**
(блок `verified-context-json`).

**Architecture:** все детали — из готовых примитивов, новых концепций ноль: рубильник — зеркало
[judge-gate-config.mjs:10-32]; событие «запись плана» — `PLAN_PATH_RE` + `Write`
[enforce-judge-gate.mjs:107,114]; шаги из md — `sealablePlan` [seal-orchestration.mjs:19];
транспорт — `callAnthropicAPI` [router-classifier.mjs:502]; журнал — примитивы
[action-journal.mjs:83-119] + ключ `resolveReceiptKey`; гейт печати — `freezeGate`
[freeze-gate.mjs:28] с инъекциями из таблицы W7 плана C2. Обёртка-производитель НИКОГДА
не блокирует (PostToolUse, пост-фактум); блокирует печать только freeze-gate в пути судьи.

**Tech Stack:** Node ESM `.mjs`, vitest tools-config (рабочая формула с абсолютными путями).

**Контракты-якоря:** W7 (план C2) · freeze-gate FR-1 · F-C2-4 (журнал = загруженная цепь) ·
C-3 (`readFileImpl` через `path.resolve(REPO_ROOT)`).

---

### Task 1: `mentor-gate-config.mjs` — рубильник SEAM

**Files:** Create: `tools/mentor-gate-config.mjs`. Test: `tools/mentor-gate-config.test.mjs`.

Зеркало judge-gate-config: активен = флаг `ROUTER_MENTOR_SEAM_ENABLED === '1'` И есть
`ROUTER_LLM_KEY` (транспорт). Без них — inert, $0, гейт печати не подключается.

- [ ] **Step 1: Write the failing test**

```js
// tools/mentor-gate-config.test.mjs
import { describe, it, expect } from 'vitest';
import { mentorSeamActive, _internals } from './mentor-gate-config.mjs';

describe('mentor-gate-config (рубильник SEAM)', () => {
  it('флаг=1 + ключ → активен', () => {
    expect(mentorSeamActive({ env: { ROUTER_MENTOR_SEAM_ENABLED: '1', ROUTER_LLM_KEY: 'k' } })).toBe(true);
  });
  it('нет флага / флаг не "1" / нет ключа → inert (по одному условию)', () => {
    expect(mentorSeamActive({ env: { ROUTER_LLM_KEY: 'k' } })).toBe(false);
    expect(mentorSeamActive({ env: { ROUTER_MENTOR_SEAM_ENABLED: 'true', ROUTER_LLM_KEY: 'k' } })).toBe(false);
    expect(mentorSeamActive({ env: { ROUTER_MENTOR_SEAM_ENABLED: '1' } })).toBe(false);
    expect(mentorSeamActive({ env: {} })).toBe(false);
  });
  it('имя флага не пересекается с судейским ROUTER_MENTOR_JUDGE_ENABLED', () => {
    expect(_internals.ENABLED_ENV).toBe('ROUTER_MENTOR_SEAM_ENABLED');
    expect(mentorSeamActive({ env: { ROUTER_MENTOR_JUDGE_ENABLED: '1', ROUTER_LLM_KEY: 'k' } })).toBe(false);
  });
});
```

- [ ] **Step 2: Run — FAIL** (`Cannot find module './mentor-gate-config.mjs'`).
- [ ] **Step 3: Write minimal implementation**

```js
// tools/mentor-gate-config.mjs
#!/usr/bin/env node
/**
 * mentor-gate-config — рубильник наставника (зеркало judge-gate-config.mjs:10-32).
 * Активен ТОЛЬКО при ROUTER_MENTOR_SEAM_ENABLED=1 И наличии ROUTER_LLM_KEY (транспорт).
 * Без них — inert: обёртка no-op ($0), freeze-gate в печать НЕ подключается.
 * Имя SEAM — чтобы владелец не путал с судейским ROUTER_MENTOR_JUDGE_ENABLED.
 */
const ENABLED_ENV = 'ROUTER_MENTOR_SEAM_ENABLED';

export function mentorSeamActive({ env = process.env } = {}) {
  return env[ENABLED_ENV] === '1' && !!env.ROUTER_LLM_KEY;
}

export const _internals = { ENABLED_ENV };
```

- [ ] **Step 4: Run — PASS** (3 passed).

---

### Task 2: `mentor-journal-store.mjs` — персист журнала и вердикта

**Files:** Create: `tools/mentor-journal-store.mjs`. Test: `tools/mentor-journal-store.test.mjs`.

F-C2-4: `journalEntries` в `onPlanWrite` обязан быть ЗАГРУЖЕННОЙ цепью задачи. Файлы:
`mentor-journal-<sess>.jsonl` + `.head` (формат action-journal [:83-119]) и
`mentor-verdict-<sess>.json` (atomic temp→rename, зеркало saveTaskId F-C1). Persist журнала —
полная атомарная перезапись цепи (выход `onPlanWrite.journal = {entries, headSig}`).

- [ ] **Step 1: Write the failing test**

```js
// tools/mentor-journal-store.test.mjs
import { describe, it, expect } from 'vitest';
import { loadMentorJournal, persistMentorJournal, loadMentorVerdict, persistMentorVerdict } from './mentor-journal-store.mjs';

function memFs(files = {}) {
  return {
    files,
    readFileSync(p) { if (!(p in files)) { const e = new Error('ENOENT'); e.code = 'ENOENT'; throw e; } return files[p]; },
    writeFileSync(p, d) { files[p] = String(d); },
    renameSync(a, b) { files[b] = files[a]; delete files[a]; },
  };
}

describe('mentor-journal-store (F-C2-4 персист цепи + вердикта)', () => {
  it('нет файлов → {entries:[], headSig:null} / null-вердикт', () => {
    const fs = memFs();
    expect(loadMentorJournal({ sessionId: 's1', runtimeDir: '/r', fsImpl: fs })).toEqual({ entries: [], headSig: null });
    expect(loadMentorVerdict({ sessionId: 's1', runtimeDir: '/r', fsImpl: fs })).toBe(null);
  });
  it('persist → load круговая (журнал: jsonl+head; вердикт: json)', () => {
    const fs = memFs();
    const journal = { entries: [{ seq: 1, ts: 5, payload: { task_id: 't', round: 1, side: 'mentor', utterance: 'u', justification: 'j' }, prev_hash: '0'.repeat(64), chain_hash: 'h1' }], headSig: 'SIG' };
    persistMentorJournal({ journal, sessionId: 's1', runtimeDir: '/r', fsImpl: fs });
    expect(loadMentorJournal({ sessionId: 's1', runtimeDir: '/r', fsImpl: fs })).toEqual({ entries: journal.entries, headSig: 'SIG' });
    const v = { ok: true, wired: true, planHash: 'PH', verdict: { recommendation: 'r' } };
    persistMentorVerdict({ record: v, sessionId: 's1', runtimeDir: '/r', fsImpl: fs });
    expect(loadMentorVerdict({ sessionId: 's1', runtimeDir: '/r', fsImpl: fs })).toEqual(v);
  });
  it('atomic: после persist временных файлов не остаётся', () => {
    const fs = memFs();
    persistMentorVerdict({ record: { ok: false }, sessionId: 's1', runtimeDir: '/r', fsImpl: fs });
    expect(Object.keys(fs.files).some((k) => k.includes('.tmp'))).toBe(false);
  });
  it('битый sessionId → throw (path-injection guard N3)', () => {
    const fs = memFs();
    expect(() => loadMentorJournal({ sessionId: '../x', runtimeDir: '/r', fsImpl: fs })).toThrow();
  });
});
```

- [ ] **Step 2: Run — FAIL** (`Cannot find module './mentor-journal-store.mjs'`).
- [ ] **Step 3: Write minimal implementation**

```js
// tools/mentor-journal-store.mjs
#!/usr/bin/env node
/**
 * mentor-journal-store — персист журнала переговоров наставника (F-C2-4) + вердикта.
 * Формат журнала = action-journal (jsonl-цепь + head-подпись [action-journal.mjs:83-119]);
 * файлы mentor-journal-<sess>.jsonl/.head + mentor-verdict-<sess>.json. Atomic temp→rename
 * (зеркало saveTaskId F-C1 / writeAtomic plan-lock). Node fs — НЕ Write-tool.
 */
import fsDefault from 'node:fs';
import { assertSafeSessionId } from './action-journal.mjs';

function paths(runtimeDir, sessionId) {
  assertSafeSessionId(sessionId);
  const sep = runtimeDir.endsWith('/') ? '' : '/';
  return {
    jsonl: `${runtimeDir}${sep}mentor-journal-${sessionId}.jsonl`,
    head: `${runtimeDir}${sep}mentor-journal-${sessionId}.head`,
    verdict: `${runtimeDir}${sep}mentor-verdict-${sessionId}.json`,
  };
}

function writeAtomic(path, data, fsImpl) {
  const tmp = `${path}.tmp`;
  fsImpl.writeFileSync(tmp, data);
  fsImpl.renameSync(tmp, path);
}

/** Загрузить цепь переговоров (F-C2-4: ИМЕННО это идёт в onPlanWrite.journalEntries). */
export function loadMentorJournal({ sessionId, runtimeDir, fsImpl = fsDefault }) {
  const { jsonl, head } = paths(runtimeDir, sessionId);
  let entries = [];
  try {
    entries = String(fsImpl.readFileSync(jsonl, 'utf8')).split('\n').filter(Boolean).map((l) => JSON.parse(l));
  } catch (e) {
    if (e && e.code === 'ENOENT') return { entries: [], headSig: null };
    throw e;
  }
  let headSig = null;
  try { headSig = String(fsImpl.readFileSync(head, 'utf8')).trim() || null; } catch { headSig = null; }
  return { entries, headSig };
}

/** Персист полной цепи (выход onPlanWrite.journal = {entries, headSig}) — atomic. */
export function persistMentorJournal({ journal, sessionId, runtimeDir, fsImpl = fsDefault }) {
  const { jsonl, head } = paths(runtimeDir, sessionId);
  const lines = (journal && Array.isArray(journal.entries) ? journal.entries : []).map((e) => JSON.stringify(e)).join('\n');
  writeAtomic(jsonl, lines ? lines + '\n' : '', fsImpl);
  writeAtomic(head, String((journal && journal.headSig) ?? ''), fsImpl);
}

/** Персист/чтение последнего вердикта наставника (потребитель — freeze-gate в пути печати). */
export function persistMentorVerdict({ record, sessionId, runtimeDir, fsImpl = fsDefault }) {
  writeAtomic(paths(runtimeDir, sessionId).verdict, JSON.stringify(record ?? null), fsImpl);
}
export function loadMentorVerdict({ sessionId, runtimeDir, fsImpl = fsDefault }) {
  try { return JSON.parse(fsImpl.readFileSync(paths(runtimeDir, sessionId).verdict, 'utf8')); }
  catch (e) { if (e && e.code === 'ENOENT') return null; throw e; }
}
```

- [ ] **Step 4: Run — PASS** (4 passed).

---

### Task 3: `export PLAN_PATH_RE` (аддитивно, М4-обёртка)

**Files:** Modify: `tools/enforce-judge-gate.mjs:107` (одна строка). Test: дописать в `tools/mentor-gate-config.test.mjs` (характеризация).

Решение владельца: единый источник «что есть план» для судьи и наставника.

- [ ] **Step 1: Дописать характеризационный тест (БЕЗ RED — SE9-прецедент)**

```js
// в mentor-gate-config.test.mjs, импорт внизу перед describe (ESM hoisting)
import { PLAN_PATH_RE } from './enforce-judge-gate.mjs';

describe('PLAN_PATH_RE — единый источник «что есть план» (решение владельца 2026-06-12)', () => {
  it('матчит docs/superpowers/plans/*.md и НЕ матчит specs', () => {
    expect(PLAN_PATH_RE.test('docs/superpowers/plans/2026-06-12-x.md')).toBe(true);
    expect(PLAN_PATH_RE.test('docs\\superpowers\\plans\\x.md')).toBe(true);
    expect(PLAN_PATH_RE.test('docs/superpowers/specs/x.md')).toBe(false);
  });
});
```

- [ ] **Step 2: Run — FAIL** (`PLAN_PATH_RE` не экспортирован → SyntaxError/undefined).
- [ ] **Step 3: Edit `enforce-judge-gate.mjs:107`**: `const PLAN_PATH_RE = ...` → `export const PLAN_PATH_RE = ...` (содержимое regex НЕ меняется).
- [ ] **Step 4: Run — PASS** + регрессия `enforce-judge-gate.test.mjs` (без падений).

---

### Task 4: `plan-verified-context.mjs` — парсер контекст-блока плана

**Files:** Create: `tools/plan-verified-context.mjs`. Test: `tools/plan-verified-context.test.mjs`.

Решение владельца («контекст в плане»): план-md несёт fenced-блок
```` ```verified-context-json ```` с массивом записей §5.5 (`{id, kind, claim, ref, anchor, derivation_ref}`).
Fail-safe направление: нет блока / битый JSON / не-массив → `[]` → VA-9 freeze-gate заблокирует
печать (пустой контекст НЕ проходит) — деградация к строгости, не к дыре.

- [ ] **Step 1: Write the failing test**

```js
// tools/plan-verified-context.test.mjs
import { describe, it, expect } from 'vitest';
import { parseVerifiedContext } from './plan-verified-context.mjs';

describe('parseVerifiedContext (контекст-в-плане, решение владельца 2026-06-12)', () => {
  it('блок парсится в массив записей', () => {
    const md = '# План\n```verified-context-json\n[{"id":"1","kind":"EXTRACTED","claim":"c","ref":"tools/x.mjs:1","anchor":"export function x"}]\n```\nдалее';
    const r = parseVerifiedContext(md);
    expect(r).toHaveLength(1);
    expect(r[0].kind).toBe('EXTRACTED');
  });
  it('нет блока → [] (VA-9 заблокирует печать — деградация к строгости)', () => {
    expect(parseVerifiedContext('# План без контекста')).toEqual([]);
  });
  it('битый JSON / не-массив → []', () => {
    expect(parseVerifiedContext('```verified-context-json\n{оборвано\n```')).toEqual([]);
    expect(parseVerifiedContext('```verified-context-json\n{"a":1}\n```')).toEqual([]);
  });
  it('не-строка → []', () => {
    expect(parseVerifiedContext(null)).toEqual([]);
  });
});
```

- [ ] **Step 2: Run — FAIL** (`Cannot find module './plan-verified-context.mjs'`).
- [ ] **Step 3: Write minimal implementation**

```js
// tools/plan-verified-context.mjs
#!/usr/bin/env node
/**
 * plan-verified-context — извлечение verified-context артефакта (§5.5) из план-md
 * (решение владельца 2026-06-12 «контекст в плане»: блок ```verified-context-json```).
 * Fail-safe: нет блока / битый JSON / не-массив → [] → VA-9 freeze-gate блокирует печать
 * (пустой контекст не проходит) — деградация к СТРОГОСТИ. Записи дальше проверяет
 * context-verity (anchor-резолв) + freeze-gate FR-1 (VF-1/SE-A1 inline).
 */
export function parseVerifiedContext(md) {
  if (typeof md !== 'string') return [];
  const m = md.match(/```verified-context-json\s*\n([\s\S]*?)```/);
  if (!m) return [];
  try {
    const o = JSON.parse(m[1]);
    return Array.isArray(o) ? o : [];
  } catch { return []; }
}
```

- [ ] **Step 4: Run — PASS** (4 passed).

---

### Task 5: `enforce-mentor-on-plan-write.mjs` — обёртка-производитель

**Files:** Create: `tools/enforce-mentor-on-plan-write.mjs`. Test: `tools/enforce-mentor-on-plan-write.test.mjs`.

Чистая `runMentorOnPlanWrite(event, deps)`: inert → no-op $0; не план-Write → no-op;
иначе полный производитель W7. `main()`: PostToolUse, ВСЕГДА allow (производитель не блокирует;
блокирует печать freeze-gate Task 6). llmCall-адаптер: паттерн судьи [enforce-judge-gate.mjs:167-177] —
throw транспорта НЕ глотать (его ловит `runMentorVerdict` → `wired:false` [mentor-verdict.mjs:72]).

- [ ] **Step 1: Write the failing test**

```js
// tools/enforce-mentor-on-plan-write.test.mjs
import { describe, it, expect } from 'vitest';
import { runMentorOnPlanWrite, buildLlmCall } from './enforce-mentor-on-plan-write.mjs';

const PLAN_MD = [
  '# План', '',
  '```steps-json', '[{"n":1,"op":"Edit","object":"tools/x.mjs"}]', '```', '',
  '```verified-context-json', '[{"id":"1","kind":"EXTRACTED","claim":"c","ref":"tools/x.mjs:1","anchor":"якорь-подстрока"}]', '```',
].join('\n');
const GOOD_VERDICT = { plan_points_addressed: ['шаг 1 ок'], reasoning: 'r', recommendation: 'rec', confidence: 0.9 };
const planEvent = { tool_name: 'Write', tool_input: { file_path: 'docs/superpowers/plans/2026-06-12-t.md', content: PLAN_MD }, session_id: 'S1' };

function deps(over = {}) {
  const persisted = { journal: null, verdict: null, taskId: null };
  return {
    persisted,
    mentorActiveImpl: () => true,
    llmCall: async () => GOOD_VERDICT,
    loadJournalImpl: () => ({ entries: [], headSig: null }),
    persistJournalImpl: (j) => { persisted.journal = j; },
    persistVerdictImpl: (r) => { persisted.verdict = r; },
    loadTaskIdImpl: () => null,
    persistTaskIdImpl: (id) => { persisted.taskId = id; },
    journalKey: 'k',
    graphSectionImpl: () => null,
    ...over,
  };
}

describe('runMentorOnPlanWrite (обёртка-производитель W7)', () => {
  it('inert → no-op $0 (llmCall не зовётся)', async () => {
    let called = false;
    const d = deps({ mentorActiveImpl: () => false, llmCall: async () => { called = true; return GOOD_VERDICT; } });
    const r = await runMentorOnPlanWrite(planEvent, d);
    expect(r.ran).toBe(false);
    expect(called).toBe(false);
  });
  it('не план-Write (Edit / чужой путь) → no-op', async () => {
    const d = deps();
    expect((await runMentorOnPlanWrite({ ...planEvent, tool_name: 'Edit' }, d)).ran).toBe(false);
    expect((await runMentorOnPlanWrite({ tool_name: 'Write', tool_input: { file_path: 'docs/x.md', content: 'y' }, session_id: 'S1' }, d)).ran).toBe(false);
  });
  it('план-Write → вердикт произведён, binding planHash, журнал и task-id персистнуты', async () => {
    const d = deps();
    const r = await runMentorOnPlanWrite(planEvent, d);
    expect(r.ran).toBe(true);
    expect(r.ok).toBe(true);
    expect(d.persisted.verdict.verdict.plan_hash).toBe(d.persisted.verdict.planHash); // нах.F4
    expect(d.persisted.journal.entries).toHaveLength(1);                               // ДР-6 запись круга
    expect(d.persisted.taskId).toMatch(/^task:/);                                      // ✅O17
  });
  it('план без steps-json → ran:false с reason (вердикт не фабрикуется)', async () => {
    const noSteps = { ...planEvent, tool_input: { ...planEvent.tool_input, content: '# без шагов' } };
    const r = await runMentorOnPlanWrite(noSteps, deps());
    expect(r.ran).toBe(false);
    expect(r.reason).toMatch(/steps/i);
  });
  it('сбой транспорта → ran:true, wired:false (SE-R6-6), вердикт-отказ персистнут', async () => {
    const d = deps({ llmCall: async () => { throw new Error('сеть'); } });
    const r = await runMentorOnPlanWrite(planEvent, d);
    expect(r.ran).toBe(true);
    expect(r.wired).toBe(false);
    expect(d.persisted.verdict.wired).toBe(false);
  });
});

describe('buildLlmCall (адаптер транспорта, паттерн судьи :167-177)', () => {
  it('зовёт transport с {system,user} от buildPrompt и {apiKey, model}', async () => {
    let got = null;
    const transport = async (prompt, opts) => { got = { prompt, opts }; return '{"x":1}'; };
    const call = buildLlmCall({ apiKey: 'K', model: 'M', transport });
    const out = await call({ buildPrompt: () => ({ system: 'S', user: 'U' }) });
    expect(out).toBe('{"x":1}');
    expect(got.prompt).toEqual({ system: 'S', user: 'U' });
    expect(got.opts).toEqual({ apiKey: 'K', model: 'M' });
  });
});
```

- [ ] **Step 2: Run — FAIL** (`Cannot find module './enforce-mentor-on-plan-write.mjs'`).
- [ ] **Step 3: Write minimal implementation**

```js
// tools/enforce-mentor-on-plan-write.mjs
#!/usr/bin/env node
/**
 * enforce-mentor-on-plan-write — активационная обёртка наставника (runbook Этап 1, W3/W7).
 * PostToolUse на запись плана (PLAN_PATH_RE + Write — канон судьи): производит вердикт
 * наставника (onPlanWrite: task-id ✅O17 + binding нах.F4 + журнал ДР-6) и персистит
 * вердикт/журнал/task-id. ПРОИЗВОДИТЕЛЬ, НЕ ГЕЙТ: exit всегда allow — печать блокирует
 * freeze-gate в пути судьи (Task 6). Рубильник mentorSeamActive: без флага+ключа — $0 no-op.
 */
import { readStdin, parseEventJson, exitDecision, runtimeDir } from './enforce-hook-helpers.mjs';
import { mentorSeamActive } from './mentor-gate-config.mjs';
import { PLAN_PATH_RE } from './enforce-judge-gate.mjs';
import { sealablePlan } from './seal-orchestration.mjs';
import { planId } from './plan-lock.mjs';
import { onPlanWrite } from './on-plan-write.mjs';
import { parseVerifiedContext } from './plan-verified-context.mjs';
import { loadMentorJournal, persistMentorJournal, persistMentorVerdict } from './mentor-journal-store.mjs';
import { loadTaskId, saveTaskId } from './router-task-id.mjs';
import { callAnthropicAPI } from './router-classifier.mjs';
import { CLASSIFIER_MODEL } from './router-config.mjs';
import { resolveReceiptKey } from './receipt-key-config.mjs';
import { resolveSessionId } from './enforce-supreme-gate.mjs';

/** Адаптер llmCall (паттерн судьи [enforce-judge-gate.mjs:167-177]): throw НЕ глотаем —
 *  его ловит runMentorVerdict → wired:false (SE-R6-6, не суд). */
export function buildLlmCall({ apiKey, model = CLASSIFIER_MODEL, transport = callAnthropicAPI }) {
  return async ({ buildPrompt }) => transport(buildPrompt(), { apiKey, model });
}

/**
 * Чистый производитель: inert → {ran:false}; не план-Write → {ran:false}; план без
 * steps-json → {ran:false, reason} (вердикт НЕ фабрикуется — печать всё равно fail-CLOSE
 * у судьи [enforce-judge-gate.mjs:79]); иначе onPlanWrite + персист. ВСЕ deps инъектируются.
 */
export async function runMentorOnPlanWrite(event, {
  mentorActiveImpl, llmCall, loadJournalImpl, persistJournalImpl, persistVerdictImpl,
  loadTaskIdImpl, persistTaskIdImpl, journalKey, graphSectionImpl, nowMs = null,
} = {}) {
  if (!mentorActiveImpl()) return { ran: false, reason: 'mentor inert ($0)' };
  const tool = event && event.tool_name;
  const filePath = String((event && event.tool_input && event.tool_input.file_path) || '');
  if (tool !== 'Write' || !PLAN_PATH_RE.test(filePath)) return { ran: false, reason: 'не запись плана' };
  const content = String((event && event.tool_input && event.tool_input.content) ?? '');
  let steps;
  try { steps = sealablePlan(content).steps; } catch { steps = null; }
  if (!Array.isArray(steps) || steps.length === 0) {
    return { ran: false, reason: 'план без steps-json блока — вердикт не фабрикуется (печать fail-CLOSE у судьи)' };
  }
  const journal0 = loadJournalImpl(); // F-C2-4: загруженная цепь, не []
  const negotiationLog = (journal0.entries || []).map((e) => e && e.payload).filter(Boolean);
  let graphSection = null;
  try { graphSection = graphSectionImpl(); } catch { graphSection = null; } // F-C6: null → маркер ОТСУТСТВИЯ
  const verifiedContext = parseVerifiedContext(content);
  const r = await onPlanWrite({
    planSteps: steps,
    existingTaskId: loadTaskIdImpl(),
    persistTaskIdImpl,
    llmCall,
    journalEntries: journal0.entries,
    journalKey,
    nowMs,
    verifiedContext,
    negotiationLog,
    graphSection,
  });
  const planHash = planId(steps);
  try { persistVerdictImpl({ ok: r.ok, wired: r.wired, reason: r.reason ?? null, planHash, verdict: r.verdict }); } catch { /* best-effort */ }
  if (r.journalOk && r.journal) { try { persistJournalImpl(r.journal); } catch { /* best-effort (SE10) */ } }
  return { ran: true, ok: r.ok, wired: r.wired, reason: r.reason, taskId: r.taskId };
}

async function main() {
  try {
    const event = parseEventJson(await readStdin());
    const dir = runtimeDir();
    const sess = resolveSessionId(event);
    await runMentorOnPlanWrite(event, {
      mentorActiveImpl: () => mentorSeamActive(),
      llmCall: buildLlmCall({ apiKey: process.env.ROUTER_LLM_KEY }),
      loadJournalImpl: () => loadMentorJournal({ sessionId: sess, runtimeDir: dir }),
      persistJournalImpl: (j) => persistMentorJournal({ journal: j, sessionId: sess, runtimeDir: dir }),
      persistVerdictImpl: (rec) => persistMentorVerdict({ record: rec, sessionId: sess, runtimeDir: dir }),
      loadTaskIdImpl: () => loadTaskId({ sessionId: sess, runtimeDir: dir, fsImpl: (await import('node:fs')).default }),
      persistTaskIdImpl: (id) => saveTaskId({ taskId: id, sessionId: sess, runtimeDir: dir, fsImpl: (await import('node:fs')).default }),
      journalKey: resolveReceiptKey(),
      graphSectionImpl: () => null, // боевой граф — Task 7 runbook-нота (best-effort, F-C6 маркер)
    });
  } catch { /* производитель никогда не блокирует */ }
  exitDecision({ block: false });
}

import { fileURLToPath } from 'node:url';
const isCli = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
if (isCli) main();
```

NB к Step 3: `await import('node:fs')` внутри стрелок невалиден — в реализации поднять
`const fs = (await import('node:fs')).default;` строкой выше в `main()` и передавать `fsImpl: fs`.

- [ ] **Step 4: Run — PASS** (6 passed).

---

### Task 6: freeze-gate в путь печати судьи (аддитивно, «зубы»)

**Files:** Modify: `tools/enforce-judge-gate.mjs` (`sealOnWiredGo` plan-ветка + `sealTurnProd`). Test: дописать в `tools/enforce-mentor-on-plan-write.test.mjs`.

Решение владельца «зубы сразу»: перед `persistPlan` — `deps.mentorGate({content})`;
`pass:false` → `{sealed:false, reason}`. Прод-сборка: гейт подключается ТОЛЬКО при
`mentorSeamActive()` (выключен флаг → `mentorGate` undefined → печать как сегодня — не кирпич).

- [ ] **Step 1: Дописать тесты (RED)**

```js
// в enforce-mentor-on-plan-write.test.mjs, импорт внизу перед describe (ESM hoisting)
import { sealOnWiredGo } from './enforce-judge-gate.mjs';

describe('Task 6: mentorGate в sealOnWiredGo (зубы, решение владельца 2026-06-12)', () => {
  const planFp = 'docs/superpowers/plans/2026-06-12-t.md';
  const goVerdict = { wired: true, decision: 'GO' };
  const sealDeps = (persisted) => ({
    key: 'k',
    sealPlan: () => ({ sealed: true, seal: { plan_id: 'P' } }),
    loadCurrentArtifact: () => ({ artifact_id: 'A' }),
    persistPlan: (s) => { persisted.plan = s; },
  });
  it('mentorGate pass:false → sealed:false, persistPlan НЕ зовётся', () => {
    const persisted = {};
    const r = sealOnWiredGo({ event: { tool_input: { file_path: planFp, content: 'x' } }, verdict: goVerdict, judgeMode: 'shadow',
      deps: { ...sealDeps(persisted), mentorGate: () => ({ pass: false, reason: 'нет вердикта наставника' }) } });
    expect(r.sealed).toBe(false);
    expect(r.reason).toMatch(/наставник|mentor/i);
    expect(persisted.plan).toBe(undefined);
  });
  it('mentorGate pass:true → печать как раньше', () => {
    const persisted = {};
    const r = sealOnWiredGo({ event: { tool_input: { file_path: planFp, content: 'x' } }, verdict: goVerdict, judgeMode: 'shadow',
      deps: { ...sealDeps(persisted), mentorGate: () => ({ pass: true, reason: 'ok' }) } });
    expect(r.sealed).toBe(true);
    expect(persisted.plan).toEqual({ plan_id: 'P' });
  });
  it('mentorGate undefined (наставник выключен) → старое поведение (характеризация)', () => {
    const persisted = {};
    const r = sealOnWiredGo({ event: { tool_input: { file_path: planFp, content: 'x' } }, verdict: goVerdict, judgeMode: 'shadow',
      deps: sealDeps(persisted) });
    expect(r.sealed).toBe(true);
  });
  it('mentorGate бросил → fail-CLOSE (sealed:false), печать не проходит', () => {
    const persisted = {};
    const r = sealOnWiredGo({ event: { tool_input: { file_path: planFp, content: 'x' } }, verdict: goVerdict, judgeMode: 'shadow',
      deps: { ...sealDeps(persisted), mentorGate: () => { throw new Error('boom'); } } });
    expect(r.sealed).toBe(false);
    expect(persisted.plan).toBe(undefined);
  });
});
```

- [ ] **Step 2: Run — FAIL** (mentorGate игнорируется → sealed:true в 1-м и 4-м кейсах).
- [ ] **Step 3: Edit `sealOnWiredGo` plan-ветки** (аддитивно, ДО `deps.sealPlan`):

```js
  if (PLAN_PATH_RE.test(fp)) {
    // Зубы наставника (решение владельца 2026-06-12): freeze-gate ПЕРЕД печатью плана.
    // mentorGate инъектируется прод-сборкой ТОЛЬКО при mentorSeamActive() — выключен
    // рубильник → undefined → печать как раньше. Бросок гейта → fail-CLOSE (не печатаем).
    if (typeof deps.mentorGate === 'function') {
      let g;
      try { g = deps.mentorGate({ content }); } catch { g = { pass: false, reason: 'mentor freeze-gate бросил (fail-CLOSE)' }; }
      if (!g || g.pass !== true) return { sealed: false, kind: 'plan', reason: `mentor freeze-gate: ${(g && g.reason) || 'нет pass'}` };
    }
    const cur = deps.loadCurrentArtifact ? deps.loadCurrentArtifact() : null;
    ...
```

и в `sealTurnProd` deps добавить (прод-сборка инъекций по W7 + C-3):

```js
      mentorGate: !mentorSeamActive() ? undefined : ({ content }) => {
        const rec = loadMentorVerdict({ sessionId, runtimeDir: dir });
        const steps = sealablePlan(content).steps;
        return freezeGate({
          mentorVerdict: rec && rec.verdict,
          mentorWired: !!(rec && rec.wired),
          planHash: planId(steps),
          verifiedContextArtifact: parseVerifiedContext(content),
          hasUnresolvedExtractedImpl: (art) =>
            artifactHasUnresolvedExtracted(art, (f) => fsDefault.readFileSync(pathResolve(process.cwd(), f), 'utf8')), // C-3
        });
      },
```

(+ импорты в enforce-judge-gate: `mentorSeamActive`, `loadMentorVerdict`, `parseVerifiedContext`,
`freezeGate`, `artifactHasUnresolvedExtracted`, `planId`, `resolve as pathResolve` из node:path.)

- [ ] **Step 4: Run — PASS** (4 новых) + полный `enforce-judge-gate.test.mjs` без падений.

---

### Task 7: Полная регрессия + runbook-апдейт + коммит-блок

**Files:** Modify: `docs/superpowers/runbooks/2026-06-11-router-mentor-activation-runbook.md` (этапы 2-3: точные строки).

- [ ] **Step 1: Полная регрессия tools** (рабочая формула, timeout 600000). Expected: 0 падений, +новые тесты.
- [ ] **Step 2: Runbook-апдейт** — вписать вместо «даст Этап 1»: флаг `ROUTER_MENTOR_SEAM_ENABLED=1`;
  JSON-блок PostToolUse-хука `node tools/enforce-mentor-on-plan-write.mjs` (matcher Write);
  формат блока `verified-context-json` в плане (+ что без него печать отклоняется);
  нота: `graphSectionImpl` в `main()` пока `()=>null` (карта районов наставнику — маркером
  ОТСУТСТВИЯ F-C6; боевой граф — отдельный шаг после обкатки).
- [ ] **Step 3: git status + paste-ready коммит-блок владельцу.**

---

## Self-Review (writing-plans)

**1. Spec coverage:** runbook Этап 1 (обёртка+персист+рубильник) → Tasks 1/2/5. W7-инъекции
(llmCall/journalEntries-цепь/planHash-binding/readFileImpl-C-3/F-C6) → Tasks 5/6. Решения
владельца (SEAM/зубы/export/контекст-в-плане) → Tasks 1/6/3/4. F-C2-4 → Task 2. ✅

**2. Placeholder scan:** полный код в каждом шаге; единственная отложка — `graphSectionImpl:
()=>null` (явно помечена в Task 7 Step 2 как нота runbook, F-C6-маркер делает её видимой
наставнику и владельцу — не тихая). ✅

**3. Type consistency:** `runMentorOnPlanWrite`→`{ran, ok?, wired?, reason?, taskId?}`;
`persistMentorVerdict.record`→`{ok, wired, reason, planHash, verdict}` = то, что читает
`mentorGate` (`rec.verdict`/`rec.wired`); `freezeGate` args = W7. `buildLlmCall`→`({buildPrompt})=>text`. ✅

**Известное ограничение (честно):** наставник в этой сборке судит план БЕЗ карты районов
(`graphSection: null` → промпт несёт явный маркер «КАРТА РАЙОНОВ ОТСУТСТВУЕТ», F-C6);
подключение живого графа B (parseGraph→buildDistrictMap→staleness) — следующий шаг после
обкатки, отдельным согласованием. Переговорная петля (mentorLoop, круги) в обёртку НЕ
включена — один вердикт на запись плана; круги = пере-запись плана после recommendation
(каждая пере-запись = новый круг в журнале через roundCount).
