# Двухуровневые переговоры + арбитраж владельца — план реализации

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Достроить двухуровневые переговоры (контроллер↔наставник L1, контроллер↔судья L2) с двумя независимыми счётчиками (потолок 3) и арбитражем владельца на тупике — переиспользуя живые кирпичи.

**Architecture:** Подход 1 — максимум переиспользования. Замечание едет на блок-сообщении хука (П1); позиция контроллера — в разделе «## Переговоры» план-файла (П2); решение владельца — через существующий escape-grant / waivers (П3). Новый код мал и чист: счётчик наставника (зеркало `bumpJudgeNoGo`), форматтеры замечаний, парсер «Переговоры», модуль карточки арбитража. L2 живёт с судьёй в Фазе 8 — все задачи проверяются юнит-тестами при судье inert.

**Tech Stack:** Node ESM (`tools/*.mjs`), vitest (tools-only конфиг). Спека: `docs/superpowers/specs/2026-06-12-router-mentor-two-level-negotiation-design.md`.

**Пусковой рецепт регрессии (канон роадмапа):** `npx vitest run --root app --config vitest.config.tools.mjs` (npx-кэш, vitest 4.1.8). Запуск через `node app/node_modules/vitest/...` ПАДАЕТ на out-of-root тестах — не использовать.

**Живое состояние / границы:** наставник АКТИВЕН (Этап 4) — трогать бережно, регрессия на каждом шаге. Судья inert в main → L2 тестируется без живого блока, активация — Фаза 8 (шаг владельца). Read-deny и страж секретов НЕ ослабляем.

---

## Задачи

### Task 1: Дописать §12 «брешь seed-write» в спеку + закоммитить спеку (обещание владельцу, «не забудь»)

Это первый шаг намеренно: текст §12 был заблокирован `supreme-gate` в разговорном режиме; после печати плана реализация открыта.

**Files:**

- Modify: `docs/superpowers/specs/2026-06-12-router-mentor-two-level-negotiation-design.md` (вставить «## 12» перед «## 12. Ссылки», переименовать старую в «## 13. Ссылки»)

- [ ] **Step 1: Вставить секцию «брешь seed-write»**

Заменить строку `## 12. Ссылки` на блок (текст согласован с владельцем 12.06):

```markdown
## 12. Известная брешь процесса: seed-write без ИИ-сторожа (честно)

Эта спека сама написана в условиях бреши, которую закрывает эпик.

**Что произошло.** Запись файла спеки (Write) прошла без проверки наставником и судьёй. Это не случайность, а **семя**: `supreme-gate` пропускает «печать артефакта» в разговорном режиме (иначе bootstrap-круг — нельзя написать план/спеку, не имея права что-то записать; ср. OQ4/D12). Но содержание семени в такой сессии не прошло ни одного ИИ-сторожа: судья inert в main, а хук наставника сторожит запись `plans/` и исполнение, не запись `specs/`. Брешь не в том, что «можно записать файл», а в том, что **содержимое семени-артефакта не проверяется ИИ-стражем при записи**.

**Как закрыть (по убыванию силы):**
1. **Фаза 8 — включить судью в main.** Судья на Stop / на Гейте-1 (`extractGate1Product` уже судит `specs/`) проверяет содержание. Без Фазы 8 в main брешь принципиально не закрывается. Цена: живой LLM + активация владельцем.
2. **Расширить триггер наставника на `specs/`, не только `plans/`** — запись спеки тоже будит наставника, вердикт едет на блок-сообщении (П1).
3. **Зажать семя (H4 — enforcement, не обещание):** печать артефакта может РОВНО создать файл, без чтений/команд/иных правок.

**Честный предел (не сводится к нулю машинами):** запись первого артефакта невозможной сделать нельзя (bootstrap); проверяющий — сам ИИ. Последний рубеж — владелец на видимом артефакте (D7/D17), предел recursive-trust (L2).

**Связь с дизайном:** двухуровневая схема — часть закрытия. После сборки + Фазы 8 записи плана идут через наставника (L1) и судью (L2). Сегодня не действовала: схемы ещё нет и судья выключен.

---

## 13. Ссылки
```

- [ ] **Step 2: Закоммитить спеку + план**

Run:

```bash
git add docs/superpowers/specs/2026-06-12-router-mentor-two-level-negotiation-design.md docs/superpowers/plans/2026-06-12-router-mentor-two-level-negotiation.md
git commit -m "docs(mentor): спека+план двухуровневых переговоров + арбитраж владельца (+§12 брешь seed-write)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```

Expected: docs-only коммит проходит (`enforce-verify-before-push` пропускает .md per CLAUDE.md §5 п.13). Если `supreme-gate` ещё держит — это значит план не считается «напечатанным» до завершения; тогда коммит выполнить в конце Task 2 вместе с первым кодовым изменением.

---

### Task 2: Счётчик NO-GO наставника (`bumpMentorNoGo`) — зеркало `bumpJudgeNoGo`

**Files:**

- Create: `tools/mentor-nogo-counter.mjs`
- Test: `tools/mentor-nogo-counter.test.mjs`

- [ ] **Step 1: Написать падающий тест**

```js
import { describe, it, expect } from 'vitest';
import { bumpMentorNoGo, MENTOR_ESCALATE_AFTER } from './mentor-nogo-counter.mjs';

describe('bumpMentorNoGo', () => {
  const mem = () => {
    const store = {};
    return {
      readFileSync: (p) => { if (!(p in store)) { const e = new Error('no'); e.code = 'ENOENT'; throw e; } return store[p]; },
      writeFileSync: (p, d) => { store[p] = d; },
      mkdirSync: () => {},
    };
  };
  it('инкремент на blocked, сброс на allow', () => {
    const fsImpl = mem(); const dir = '/r'; const sessionId = 's1';
    expect(bumpMentorNoGo({ sessionId, blocked: true, fsImpl, dir })).toBe(1);
    expect(bumpMentorNoGo({ sessionId, blocked: true, fsImpl, dir })).toBe(2);
    expect(bumpMentorNoGo({ sessionId, blocked: false, fsImpl, dir })).toBe(0);
    expect(bumpMentorNoGo({ sessionId, blocked: true, fsImpl, dir })).toBe(1);
  });
  it('порог эскалации = 3', () => { expect(MENTOR_ESCALATE_AFTER).toBe(3); });
  it('небезопасный sessionId санитизируется (не падает)', () => {
    const fsImpl = mem();
    expect(bumpMentorNoGo({ sessionId: '../../x', blocked: true, fsImpl, dir: '/r' })).toBe(1);
  });
});
```

- [ ] **Step 2: Запустить — убедиться, что падает**

Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/mentor-nogo-counter.test.mjs`
Expected: FAIL — модуль не существует.

- [ ] **Step 3: Реализовать (точное зеркало `bumpJudgeNoGo` из `enforce-judge-gate.mjs:319-327`)**

```js
#!/usr/bin/env node
/**
 * mentor-nogo-counter — счётчик подряд идущих NO-GO наставника (L1), зеркало
 * bumpJudgeNoGo. blocked=true → +1; allow → сброс 0. Порог 3 → арбитраж (round-control C-12).
 */
import fsDefault from 'node:fs';
import { join } from 'node:path';
import { runtimeDir } from './enforce-hook-helpers.mjs';

export const MENTOR_ESCALATE_AFTER = 3;

export function bumpMentorNoGo({ sessionId, blocked, fsImpl = fsDefault, dir = runtimeDir() } = {}) {
  const safe = String(sessionId || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '_');
  const path = join(dir, `mentor-nogo-${safe}.json`);
  let count = 0;
  try { count = (JSON.parse(fsImpl.readFileSync(path, 'utf8')).count) || 0; } catch { count = 0; }
  const next = blocked ? count + 1 : 0;
  try { fsImpl.mkdirSync(dir, { recursive: true }); fsImpl.writeFileSync(path, JSON.stringify({ count: next })); } catch { /* best-effort */ }
  return next;
}
```

- [ ] **Step 4: Запустить — убедиться, что проходит**

Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/mentor-nogo-counter.test.mjs`
Expected: PASS (3 теста).

- [ ] **Step 5: Коммит**

```bash
git add tools/mentor-nogo-counter.mjs tools/mentor-nogo-counter.test.mjs
git commit -m "feat(mentor): счётчик NO-GO наставника (L1, зеркало bumpJudgeNoGo)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```

---

### Task 3: Модуль карточки арбитража (`arbitration-card.mjs`) — чистый, дословные цитаты

**Files:**

- Create: `tools/arbitration-card.mjs`
- Test: `tools/arbitration-card.test.mjs`

- [ ] **Step 1: Написать падающий тест**

```js
import { describe, it, expect } from 'vitest';
import { buildArbitrationCard, ARBITRATION_OPTIONS } from './arbitration-card.mjs';

describe('buildArbitrationCard', () => {
  const base = {
    side: 'mentor',            // 'mentor' | 'judge'
    level: 'L1',
    round: 3,
    objectionVerbatim: 'Шаг 2 склеен: чтение и правка в одном пункте — разбей.',
    controllerPositionVerbatim: 'Не согласен: шаг 2 атомарен, чтение нужно ровно для этой правки.',
  };
  it('цитаты переданы ДОСЛОВНО (без перефраза)', () => {
    const c = buildArbitrationCard(base);
    expect(c.objection).toBe(base.objectionVerbatim);
    expect(c.position).toBe(base.controllerPositionVerbatim);
  });
  it('три варианта выбора в фиксированном порядке', () => {
    const c = buildArbitrationCard(base);
    expect(c.options.map((o) => o.key)).toEqual(['hold', 'accept', 'own']);
    expect(ARBITRATION_OPTIONS).toEqual(['hold', 'accept', 'own']);
  });
  it('каждый вариант несёт нейтральную строку «что меняет выбор»', () => {
    const c = buildArbitrationCard(base);
    for (const o of c.options) expect(typeof o.whatChanges === 'string' && o.whatChanges.length > 0).toBe(true);
  });
  it('есть аффорданс «объясни подробнее»', () => {
    expect(buildArbitrationCard(base).explainAffordance).toBe(true);
  });
  it('сторона судьи меняет заголовок, не ломает форму', () => {
    const c = buildArbitrationCard({ ...base, side: 'judge', level: 'L2' });
    expect(c.title).toMatch(/судь/i);
    expect(c.options.map((o) => o.key)).toEqual(['hold', 'accept', 'own']);
  });
  it('пустые/нестроковые цитаты не роняют (fail-safe → пустая строка)', () => {
    const c = buildArbitrationCard({ ...base, objectionVerbatim: null, controllerPositionVerbatim: undefined });
    expect(c.objection).toBe('');
    expect(c.position).toBe('');
  });
});
```

- [ ] **Step 2: Запустить — убедиться, что падает**

Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/arbitration-card.test.mjs`
Expected: FAIL — модуль не существует.

- [ ] **Step 3: Реализовать**

```js
#!/usr/bin/env node
/**
 * arbitration-card — чистый сборщик карточки арбитража (спека §6). Дословные цитаты обеих
 * сторон + нейтральная строка «что меняет выбор» на каждый вариант + аффорданс «объясни
 * подробнее». НЕ перефразирует: objection/position проходят как есть (только String-coerce).
 * Решение владельца пишется НЕ здесь, а через escape-grant/waivers (П3).
 */
export const ARBITRATION_OPTIONS = ['hold', 'accept', 'own'];

const verbatim = (s) => (typeof s === 'string' ? s : '');

export function buildArbitrationCard({ side, level, round, objectionVerbatim, controllerPositionVerbatim } = {}) {
  const who = side === 'judge' ? 'судьи' : 'наставника';
  return {
    side, level, round,
    title: `Арбитраж (${level}): тупик спора с ${side === 'judge' ? 'судьёй' : 'наставником'} после ${round} кругов`,
    objection: verbatim(objectionVerbatim),
    position: verbatim(controllerPositionVerbatim),
    options: [
      { key: 'hold', label: 'Держусь позиции контроллера', whatChanges: `Ворота пропустят версию контроллера — замечание ${who} отведено тобой (escape-грант).` },
      { key: 'accept', label: 'Согласиться с замечанием', whatChanges: `Контроллер переделает по замечанию ${who}; счётчик круга сбрасывается.` },
      { key: 'own', label: 'Своё решение', whatChanges: 'Ты вписываешь своё — оно становится обязательным для контроллера.' },
    ],
    explainAffordance: true,
  };
}
```

- [ ] **Step 4: Запустить — убедиться, что проходит**

Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/arbitration-card.test.mjs`
Expected: PASS (6 тестов).

- [ ] **Step 5: Коммит**

```bash
git add tools/arbitration-card.mjs tools/arbitration-card.test.mjs
git commit -m "feat(mentor): чистый сборщик карточки арбитража (дословные цитаты + что меняет выбор)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```

---

### Task 4: Парсер раздела «## Переговоры» план-файла (позиция контроллера, П2)

Позиция контроллера в споре живёт в плане; парсер достаёт её дословно — для подачи наставнику (negotiationLog) и для карточки арбитража.

**Files:**

- Create: `tools/negotiation-section.mjs`
- Test: `tools/negotiation-section.test.mjs`

- [ ] **Step 1: Написать падающий тест**

```js
import { describe, it, expect } from 'vitest';
import { parseNegotiationSection } from './negotiation-section.mjs';

const PLAN = `# План
## Цель
Сделать X.
## Переговоры
### Круг 1
Не согласен с замечанием про шаг 2: он атомарен.
### Круг 2
Принял про шаг 4, переписал.
## Шаги
- [ ] ...`;

describe('parseNegotiationSection', () => {
  it('достаёт круги с дословным текстом позиции', () => {
    const r = parseNegotiationSection(PLAN);
    expect(r).toEqual([
      { round: 1, position: 'Не согласен с замечанием про шаг 2: он атомарен.' },
      { round: 2, position: 'Принял про шаг 4, переписал.' },
    ]);
  });
  it('нет раздела → пустой массив', () => {
    expect(parseNegotiationSection('# План\n## Цель\nY')).toEqual([]);
  });
  it('последний круг (для арбитража) = max по round', () => {
    const r = parseNegotiationSection(PLAN);
    const last = r[r.length - 1];
    expect(last.round).toBe(2);
  });
  it('не падает на мусоре', () => {
    expect(parseNegotiationSection(null)).toEqual([]);
  });
});
```

- [ ] **Step 2: Запустить — убедиться, что падает**

Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/negotiation-section.test.mjs`
Expected: FAIL.

- [ ] **Step 3: Реализовать**

```js
#!/usr/bin/env node
/**
 * negotiation-section — парсер раздела «## Переговоры» план-файла. Возвращает круги
 * [{round, position}] с ДОСЛОВНЫМ текстом позиции контроллера (для наставника и арбитража).
 * Раздел = «## Переговоры» до следующего «## », внутри — «### Круг N» блоки.
 */
export function parseNegotiationSection(md) {
  const text = typeof md === 'string' ? md : '';
  const sec = text.match(/^##\s*Переговоры[^\n]*\n([\s\S]*?)(?:\n##\s|$)/im);
  if (!sec) return [];
  const body = sec[1];
  const out = [];
  const re = /^###\s*Круг\s*(\d+)[^\n]*\n([\s\S]*?)(?=\n###\s|$)/gim;
  let m;
  while ((m = re.exec(body)) !== null) {
    out.push({ round: Number(m[1]), position: m[2].trim() });
  }
  return out;
}
```

- [ ] **Step 4: Запустить — убедиться, что проходит**

Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/negotiation-section.test.mjs`
Expected: PASS (4 теста).

- [ ] **Step 5: Коммит**

```bash
git add tools/negotiation-section.mjs tools/negotiation-section.test.mjs
git commit -m "feat(mentor): парсер раздела «Переговоры» план-файла (позиция контроллера, П2)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```

---

### Task 5: Форматтер замечания судьи (`formatJudgeObjection`) + вставка в блок-сообщение

Сейчас блок-сообщение судьи (`decide()` в `enforce-judge-gate.mjs:50`) = «судья=NO-GO, пол=… → блок» — без текста возражения. Добавляем дословный текст возражений из `verdict.objections`/`verdict.slots`.

**Files:**

- Create: `tools/judge-objection-format.mjs`
- Test: `tools/judge-objection-format.test.mjs`
- Modify: `tools/enforce-judge-gate.mjs` (импорт + использование в `decide()` message)

- [ ] **Step 1: Написать падающий тест форматтера**

```js
import { describe, it, expect } from 'vitest';
import { formatJudgeObjection } from './judge-objection-format.mjs';

describe('formatJudgeObjection', () => {
  it('склеивает дословные возражения (anchor.ref + severity)', () => {
    const v = { objections: [
      { verdict: 'NO', anchor: { kind: 'failed_criterion', ref: 'шаг 3 не проверяемый исход' }, severity: 'heavy' },
    ], slots: {} };
    const s = formatJudgeObjection(v);
    expect(s).toContain('шаг 3 не проверяемый исход');
    expect(s).toContain('heavy');
  });
  it('нет возражений → пустая строка', () => {
    expect(formatJudgeObjection({ objections: [], slots: {} })).toBe('');
  });
  it('мусорный вердикт не роняет', () => {
    expect(formatJudgeObjection(null)).toBe('');
    expect(formatJudgeObjection({ objections: 'x' })).toBe('');
  });
});
```

- [ ] **Step 2: Запустить — убедиться, что падает**

Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/judge-objection-format.test.mjs`
Expected: FAIL.

- [ ] **Step 3: Реализовать форматтер**

```js
#!/usr/bin/env node
/**
 * judge-objection-format — дословная сводка возражений судьи для блок-сообщения (П1).
 * Берёт verdict.objections (форма parseJudgeResponse: {verdict,anchor:{kind,ref},severity}).
 * Fail-safe: не-массив/пусто → ''.
 */
export function formatJudgeObjection(verdict) {
  const objs = verdict && Array.isArray(verdict.objections) ? verdict.objections : [];
  const lines = objs
    .filter((o) => o && o.anchor && o.anchor.ref)
    .map((o) => `— [${o.severity || 'light'}] ${o.anchor.ref}`);
  return lines.join('\n');
}
```

- [ ] **Step 4: Запустить форматтер — PASS**

Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/judge-objection-format.test.mjs`
Expected: PASS (3 теста).

- [ ] **Step 5: Вставить замечание в блок-сообщение `decide()`**

В `tools/enforce-judge-gate.mjs`: добавить импорт `import { formatJudgeObjection } from './judge-objection-format.mjs';` и в `decide()` (строка ~50) расширить message:

```js
return gate === 'allow'
  ? { block: false, reason: 'live-block: судья GO + пол чист' }
  : { block: true, message: `[judge-gate] live-block: судья=${decision}, пол=${floorBlocked} → блок` + (formatJudgeObjection(verdict) ? `\nЗамечание судьи:\n${formatJudgeObjection(verdict)}` : '') };
```

- [ ] **Step 6: Тест на расширенный `decide()`-message (добавить в существующий `tools/enforce-judge-gate.test.mjs`)**

```js
it('decide() включает дословное замечание судьи в блок-сообщение', async () => {
  const { decide } = await import('./enforce-judge-gate.mjs');
  const verdict = { decision: 'NO-GO', objections: [{ anchor: { ref: 'шаг 3 не проверяемый' }, severity: 'heavy' }] };
  const r = decide({ mode: 'live-block', verdict, floorBlocked: false });
  expect(r.block).toBe(true);
  expect(r.message).toContain('шаг 3 не проверяемый');
});
```

- [ ] **Step 7: Запустить полный файл судьи + форматтер — PASS**

Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/judge-objection-format.test.mjs tools/enforce-judge-gate.test.mjs`
Expected: PASS (включая старые тесты судьи — не сломаны).

- [ ] **Step 8: Коммит**

```bash
git add tools/judge-objection-format.mjs tools/judge-objection-format.test.mjs tools/enforce-judge-gate.mjs tools/enforce-judge-gate.test.mjs
git commit -m "feat(judge): дословное замечание судьи в блок-сообщении (П1)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```

---

### Task 6: Замечание наставника доходит до контроллера (non-blocking surface, П1)

Наставник — производитель (`enforce-mentor-on-plan-write.mjs`, exit всегда allow). Его NO-GO должен дойти до контроллера сообщением, НЕ блокируя запись.

**Files:**

- Create: `tools/mentor-objection-format.mjs`
- Test: `tools/mentor-objection-format.test.mjs`
- Modify: `tools/enforce-mentor-on-plan-write.mjs` (в `main()` — surface сообщения при `ran && wired && !ok`)

- [ ] **Step 1: Сначала прочитать `tools/enforce-hook-helpers.mjs` — подтвердить, поддерживает ли `exitDecision` non-blocking message (PostToolUse additionalContext/systemMessage)**

Run: `grep -n "export function exitDecision" tools/enforce-hook-helpers.mjs` и прочитать тело.
Если `exitDecision` умеет `{ block:false, message }` (выводит сообщение, не блокируя) — использовать его. Если НЕТ — surface через `console.error(...)` (PostToolUse stderr на allow показывается контроллеру; так делает `logGuardBlock`-сосед) ИЛИ через возврат PostToolUse JSON `{ "decision": undefined, "reason": "..." }`. Выбрать поддержанный путь, зафиксировать в коде Step 4.

- [ ] **Step 2: Написать падающий тест форматтера**

```js
import { describe, it, expect } from 'vitest';
import { formatMentorObjection } from './mentor-objection-format.mjs';

describe('formatMentorObjection', () => {
  it('собирает дословный reason + замечания вердикта', () => {
    const s = formatMentorObjection({ ok: false, wired: true, reason: 'шаг 2 склеен', verdict: { objections: [{ anchor: { ref: 'разбей шаг 2' } }] } });
    expect(s).toContain('шаг 2 склеен');
    expect(s).toContain('разбей шаг 2');
  });
  it('ok=true → пустая строка (нет замечания)', () => {
    expect(formatMentorObjection({ ok: true, wired: true })).toBe('');
  });
  it('не wired → пустая строка ($0, наставник не судил)', () => {
    expect(formatMentorObjection({ ok: false, wired: false })).toBe('');
  });
  it('мусор не роняет', () => { expect(formatMentorObjection(null)).toBe(''); });
});
```

- [ ] **Step 3: Запустить — FAIL**

Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/mentor-objection-format.test.mjs`
Expected: FAIL.

- [ ] **Step 4: Реализовать форматтер**

```js
#!/usr/bin/env node
/**
 * mentor-objection-format — дословная сводка замечания наставника для surface контроллеру
 * (П1, non-blocking). Источник — результат onPlanWrite {ok,wired,reason,verdict}.
 * Fail-safe: не wired или ok → '' (нечего показывать).
 */
export function formatMentorObjection(r) {
  if (!r || r.wired !== true || r.ok === true) return '';
  const head = typeof r.reason === 'string' && r.reason ? `Замечание наставника: ${r.reason}` : 'Замечание наставника.';
  const objs = r.verdict && Array.isArray(r.verdict.objections) ? r.verdict.objections : [];
  const lines = objs.filter((o) => o && o.anchor && o.anchor.ref).map((o) => `— ${o.anchor.ref}`);
  return [head, ...lines].join('\n');
}
```

- [ ] **Step 5: Запустить форматтер — PASS**

Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/mentor-objection-format.test.mjs`
Expected: PASS (4 теста).

- [ ] **Step 6: Вставить surface + bump счётчика в `main()` наставника**

В `tools/enforce-mentor-on-plan-write.mjs` после вызова `runMentorOnPlanWrite(...)` сохранить результат в `const res = await runMentorOnPlanWrite(...)` и перед `exitDecision({ block: false })`:

```js
import { formatMentorObjection } from './mentor-objection-format.mjs';
import { bumpMentorNoGo } from './mentor-nogo-counter.mjs';
// ... в main(), после res:
try {
  if (res && res.ran) {
    const blocked = res.wired === true && res.ok !== true;
    bumpMentorNoGo({ sessionId: sess, blocked });
    const msg = formatMentorObjection(res);
    if (msg) console.error(msg); // surface контроллеру, НЕ блокирует (producer)
  }
} catch { /* surface best-effort */ }
```

(Если Step 1 показал поддержку `exitDecision({block:false, message})` — использовать его вместо `console.error`.)

- [ ] **Step 7: Тест non-blocking surface (добавить в `tools/enforce-mentor-on-plan-write.test.mjs`)**

```js
it('NO-GO наставника не блокирует, но surface даёт текст замечания', () => {
  const r = formatMentorObjection({ ok: false, wired: true, reason: 'переформулируй шаг 3' });
  expect(r).toContain('переформулируй шаг 3'); // exit остаётся allow — проверяется отдельным существующим тестом producer
});
```

- [ ] **Step 8: Регрессия наставника — PASS (producer по-прежнему allow)**

Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/enforce-mentor-on-plan-write.test.mjs tools/mentor-objection-format.test.mjs`
Expected: PASS (старые producer-тесты не сломаны).

- [ ] **Step 9: Коммит**

```bash
git add tools/mentor-objection-format.mjs tools/mentor-objection-format.test.mjs tools/enforce-mentor-on-plan-write.mjs tools/enforce-mentor-on-plan-write.test.mjs
git commit -m "feat(mentor): замечание наставника доходит до контроллера + счётчик NO-GO (П1, L1)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```

---

### Task 7: Эскалация судьи (3 NO-GO) → карточка арбитража

Сейчас при 3-м NO-GO `main()` судьи (`enforce-judge-gate.mjs:346`) даёт generic-сообщение «escape или подтверди отказ». Заменяем на карточку арбитража с дословной позицией контроллера (из плана).

**Files:**

- Modify: `tools/enforce-judge-gate.mjs` (ветка `n >= JUDGE_ESCALATE_AFTER`)
- Test: `tools/enforce-judge-gate.test.mjs`

- [ ] **Step 1: Написать падающий тест на формат эскалации**

```js
it('3-й NO-GO судьи даёт карточку арбитража с дословной позицией контроллера', async () => {
  const mod = await import('./enforce-judge-gate.mjs');
  // через buildJudgeArbitrationMessage(verdict, planContent, n)
  const msg = mod.buildJudgeArbitrationMessage(
    { objections: [{ anchor: { ref: 'шаг 3 не проверяем' }, severity: 'heavy' }] },
    '## Переговоры\n### Круг 3\nНе согласен: критерий проверяемый — pest зелёный.',
    3,
  );
  expect(msg).toContain('шаг 3 не проверяем');               // замечание судьи дословно
  expect(msg).toContain('критерий проверяемый — pest зелёный'); // позиция контроллера дословно
  expect(msg).toMatch(/держусь|согласиться|своё/i);          // три выбора
});
```

- [ ] **Step 2: Запустить — FAIL**

Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/enforce-judge-gate.test.mjs`
Expected: FAIL — `buildJudgeArbitrationMessage` не существует.

- [ ] **Step 3: Реализовать `buildJudgeArbitrationMessage` + вставить в `main()`**

В `tools/enforce-judge-gate.mjs`:

```js
import { buildArbitrationCard } from './arbitration-card.mjs';
import { formatJudgeObjection } from './judge-objection-format.mjs';
import { parseNegotiationSection } from './negotiation-section.mjs';

export function buildJudgeArbitrationMessage(verdict, planContent, n) {
  const neg = parseNegotiationSection(planContent);
  const position = neg.length ? neg[neg.length - 1].position : '(позиция не указана в разделе «Переговоры» плана)';
  const card = buildArbitrationCard({
    side: 'judge', level: 'L2', round: n,
    objectionVerbatim: formatJudgeObjection(verdict) || '(судья не дал текста возражения)',
    controllerPositionVerbatim: position,
  });
  const opts = card.options.map((o) => `• ${o.label}: ${o.whatChanges}`).join('\n');
  return [
    `[judge-gate] ${card.title}`,
    `Замечание судьи:\n${card.objection}`,
    `Позиция контроллера:\n${card.position}`,
    `Что меняет выбор:\n${opts}`,
    'Можно сказать «объясни подробнее» — дам нейтральное пояснение. Решение действует только через escape-грант/вейвер владельца (не со слов Claude).',
  ].join('\n\n');
}
```

В `main()` заменить тело ветки `if (result.block && n >= JUDGE_ESCALATE_AFTER)` на:

```js
const planContent = String((event && event.tool_input && event.tool_input.content) ?? '');
result = { ...result, message: buildJudgeArbitrationMessage(result.verdictForCard || (result.verdict), planContent, n) };
```

Примечание: вердикт для карточки нужно прокинуть из `runJudgeTurn`. Прокинуть в `result` поле `verdict` (расширить возврат `runJudgeTurn` полем `verdict` из `runJudgeGate`) ИЛИ повторно префетчить нельзя (дорого) — взять из лога. **Шаг реализации:** в `runJudgeTurn` вернуть `{ block, message, verdict }` (verdict уже в области видимости после `runJudgeGate`); в `main()` использовать `result.verdict`.

- [ ] **Step 4: Запустить — PASS (включая старые тесты судьи)**

Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/enforce-judge-gate.test.mjs`
Expected: PASS.

- [ ] **Step 5: Коммит**

```bash
git add tools/enforce-judge-gate.mjs tools/enforce-judge-gate.test.mjs
git commit -m "feat(judge): эскалация 3 NO-GO → карточка арбитража (дословные обе стороны, L2)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```

---

### Task 8: Эскалация наставника (3 NO-GO) → карточка арбитража + регрессия

**Files:**

- Modify: `tools/enforce-mentor-on-plan-write.mjs` (в `main()` — при `bumpMentorNoGo >= MENTOR_ESCALATE_AFTER` собрать карточку наставника и surface)
- Test: `tools/enforce-mentor-on-plan-write.test.mjs`

- [ ] **Step 1: Написать падающий тест на карточку наставника**

```js
it('3-й NO-GO наставника даёт карточку арбитража (сторона mentor)', async () => {
  const mod = await import('./enforce-mentor-on-plan-write.mjs');
  const msg = mod.buildMentorArbitrationMessage(
    { ok: false, wired: true, reason: 'шаг 2 склеен', verdict: { objections: [{ anchor: { ref: 'разбей шаг 2' } }] } },
    '## Переговоры\n### Круг 3\nШаг 2 атомарен — настаиваю.',
    3,
  );
  expect(msg).toContain('разбей шаг 2');                 // замечание наставника дословно
  expect(msg).toContain('Шаг 2 атомарен — настаиваю');   // позиция контроллера дословно
  expect(msg).toMatch(/L1/);
});
```

- [ ] **Step 2: Запустить — FAIL**

Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/enforce-mentor-on-plan-write.test.mjs`
Expected: FAIL.

- [ ] **Step 3: Реализовать `buildMentorArbitrationMessage` + вставить в `main()`**

В `tools/enforce-mentor-on-plan-write.mjs`:

```js
import { buildArbitrationCard } from './arbitration-card.mjs';
import { formatMentorObjection } from './mentor-objection-format.mjs';
import { parseNegotiationSection } from './negotiation-section.mjs';
import { MENTOR_ESCALATE_AFTER } from './mentor-nogo-counter.mjs';

export function buildMentorArbitrationMessage(res, planContent, n) {
  const neg = parseNegotiationSection(planContent);
  const position = neg.length ? neg[neg.length - 1].position : '(позиция не указана в разделе «Переговоры» плана)';
  const card = buildArbitrationCard({
    side: 'mentor', level: 'L1', round: n,
    objectionVerbatim: formatMentorObjection(res) || '(нет текста замечания)',
    controllerPositionVerbatim: position,
  });
  const opts = card.options.map((o) => `• ${o.label}: ${o.whatChanges}`).join('\n');
  return [`[mentor] ${card.title}`, `Замечание наставника:\n${card.objection}`, `Позиция контроллера:\n${card.position}`, `Что меняет выбор:\n${opts}`, 'Скажи «объясни подробнее» — поясню. Решение — через escape/вейвер владельца.'].join('\n\n');
}
```

В `main()` (Task 6 Step 6 блок) — расширить: если `bumpMentorNoGo(...) >= MENTOR_ESCALATE_AFTER` и blocked, то `console.error(buildMentorArbitrationMessage(res, content, n))` вместо обычного surface (`content` уже извлечён выше в `runMentorOnPlanWrite`; пробросить через возврат или повторно прочитать `event.tool_input.content`).

- [ ] **Step 4: Запустить — PASS (producer по-прежнему allow)**

Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/enforce-mentor-on-plan-write.test.mjs`
Expected: PASS.

- [ ] **Step 5: Коммит**

```bash
git add tools/enforce-mentor-on-plan-write.mjs tools/enforce-mentor-on-plan-write.test.mjs
git commit -m "feat(mentor): эскалация 3 NO-GO → карточка арбитража (сторона mentor, L1)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```

---

### Task 9: Полная регрессия tools-only + финальный коммит

**Files:** нет правок кода — только прогон и фиксация.

- [ ] **Step 1: Полная регрессия tools-only**

Run: `npx vitest run --root app --config vitest.config.tools.mjs`
Expected: все GREEN (baseline 3754 + новые тесты Tasks 2-8; 0 регрессий). Зафиксировать число в финальном коммите/заметке.

- [ ] **Step 2: Если есть незакоммиченное — финальный коммит**

```bash
git add -A tools/
git commit -m "test(mentor): двухуровневые переговоры — полная регрессия GREEN

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```

- [ ] **Step 3: Зафиксировать в роадмапе/памяти (отдельный ход с активным claude-md-management + coverage direct:memory-sync)**

Дописать в `docs/superpowers/specs/2026-06-12-router-mentor-roadmap.md` пункт «двухуровневые переговоры собраны (L1+L2 механика, L2 живёт с Фазой 8)» — через нормальный docs-канал.

---

## Self-Review (против спеки)

**Покрытие спеки:**

- §3/§4 L1 поток + счётчик L1 → Task 2 (`bumpMentorNoGo`) + Task 6 (surface + bump) + Task 8 (эскалация-карточка). ✓
- §5 L2 поток + счётчик L2 → счётчик `bumpJudgeNoGo` уже живой; Task 5 (замечание судьи в блоке) + Task 7 (эскалация-карточка). ✓
- §6 карточка арбитража + канал решения → Task 3 (карточка) + Task 7/8 (вставка) + переиспользование escape-grant/`applyWaivers` (П3, существуют — отдельная сборка не нужна). ✓
- §2 П1 (замечание на блоке) → Task 5 (судья) + Task 6 (наставник). П2 (позиция в плане) → Task 4 (парсер). П3 (канал решения) → escape-grant/waivers (живые). ✓
- §8 fail-closed → формат-функции fail-safe (Tasks 3/5/6 «мусор не роняет»); счётчики best-effort (Task 2). Битый счётчик/нет решения → консервативно (наследуется от round-control/escape-grant). ✓
- §12 (брешь seed-write) → Task 1 (дописать в спеку + коммит). ✓ (обещание владельцу «не забудь»).

**Гэп честно:** «канал решения владельца» (П3) в плане не строит нового модуля — опирается на живые escape-grant (`escapeGrantOpen`) + `applyWaivers`. Если при реализации окажется, что маппинг «вариант карточки → escape/вейвер» требует кода — добавить Task 8a (тонкая привязка). Это отмечено, не placeholder.

**Плейсхолдеры:** код-шаги несут реальный код; «read-then-confirm» шаги (Task 6 Step 1) — настоящие шаги проверки живого модуля перед правкой, не заглушки.

**Согласованность имён:** `bumpMentorNoGo`/`MENTOR_ESCALATE_AFTER` (Task 2) используются в Tasks 6/8; `buildArbitrationCard`/`ARBITRATION_OPTIONS` (Task 3) — в Tasks 7/8; `parseNegotiationSection` (Task 4) — в Tasks 7/8; `formatJudgeObjection` (Task 5) — в Task 7; `formatMentorObjection` (Task 6) — в Task 8. Совпадают.
