# Sub-plan C — живой шов наставника (ЯДРО) Implementation Plan

> **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:** Собрать ЖИВОЙ шов наставника (§6 спеки): task-id (✅O17), tamper-evident журнал переговоров (§6.3), mentor-вердикт со СВОИМИ слотами (§6.1), обогащённый вход наставника (проверенный контекст из A + журнал + граф-секция из B, §6.2), живой `runMentorRound` поверх существующего `runRouter`, и предусловный `freeze-gate` (mentor-вердикт VA-8 + verity ✅O2 ПЕРЕД печатью — plan-lock НЕ трогаем, CD-3).

**Architecture:** 4 новых чистых модуля + предусловный freeze-gate. **Развязка зависимости (ключевое):** модули C НЕ импортируют A/B на верхнем уровне — внешние деки́деры (`artifactHasUnresolvedExtracted` из A, выход `buildGraphSection` из B) ПЕРЕДАЮТСЯ как данные/инъекции. Поэтому модули+тесты C зелены АВТОНОМНО (стабы в тестах); A+B нужны только на БОЕВОЙ проводке (передать реальные функции/секцию) + живой `llmCall`-транспорт (активация владельца). plan-lock.mjs / seal / М6 — НЕ трогаются (CD-3).

**Tech Stack:** Node ESM (`.mjs`), vitest (tools-config). Источник истины спеки: `docs/superpowers/specs/2026-06-10-router-mentor-live-rebuild-design.md` §6/6.1/6.2/6.3/6.4 + §4 (петля) + §5.6 (✅O2) + ✅O17.

**Сверенные факты кода (построчно):**
- Freeze-choke `plan-lock.mjs`: `freezePlan:62` (единственная печать плана, подпись), `freezeArtifact:194` (печать артефакта, есть `sections`/`judge_mode`), `refResolves:226`, `assertValidJudgeMode:53-57`. **CD-3 §6.1: «Чек — ОТДЕЛЬНОЕ предусловие к freezePlan; seal/plan-lock/М6 НЕ трогаются».**
- Хеш-цепь `action-journal.mjs`: `appendEntry:25-41` → `{entries, headSig}`, `verifyChain:53-68` → `{ok, brokenAt}`, `computeEntryHash:17`, `GENESIS_HASH:14`, `assertSafeSessionId:77-82`. Переиспуётся для журнала переговоров (§6.3 SE-R6-4 «хеш-цепь зеркало М1»).
- Роутер `router-engine.mjs`: `buildRouterPrompt:98` ({prompt,plan,graph,catalog,contracts}→{system,user}), `runRouter:140` (live `llmCall` инъектируется, `confidenceThreshold` 0.5), `validateTrace` (confidence finite∈[0,1] :65), `groundTrace:73-79`, `chosenIsCandidate:86-90`, `parseRouterResponse:127`.

**⚠️ Зависимость (риск принят владельцем):** боевая проводка (Task 4 интеграция секции B, Task 6 verity A) требует РЕАЛИЗОВАННЫХ A+B. Модули/тесты C — автономны (инъекция). Живой `llmCall` в `runMentorRound` — активация владельца, не в scope C.

**Scope (граница):**
- **В C:** `tools/router-task-id.mjs`, `tools/mentor-journal.mjs`, `tools/mentor-verdict.mjs`, `tools/mentor-seam.mjs`, `tools/freeze-gate.mjs` (+ их `*.test.mjs`).
- **НЕ в C:** активация (settings.json, ключ, флаг, живой `llmCall`-транспорт) — владелец; правка `plan-lock.mjs`/`enforce-supreme-gate.mjs`/seal — НЕТ (CD-3); A0-хук как РЕГИСТРАЦИЯ в settings.json — владелец (C даёт чистую функцию `onPlanWrite`).

**Квирк среды (ОБЯЗАТЕЛЬНО для шагов «Run»):** vitest ТОЛЬКО из `app/`:
```
cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs <фильтр> --reporter dot
```
cwd между Bash-вызовами ненадёжен — префиксить `cd app && ...` в одной команде.

**NB git:** git зажат стеной у Claude — `git commit`-шаги выполняет ВЛАДЕЛЕЦ/исполнитель вне стены.

**NB keytar:** рабочий ТОЛЬКО в КОРНЕВОМ node_modules; receipt-sign/подпись головы журнала в проде берут ключ оттуда (активация владельца). Тесты передают ключ инъекцией.

---

### Task 1: `router-task-id.mjs` — `deriveTaskId` (persisted first-plan-anchor, ✅O17)

**Files:**
- Create: `tools/router-task-id.mjs`
- Test: `tools/router-task-id.test.mjs`

**✅O17.** task-id присваивается A0-хуком при ПЕРВОМ плане задачи, ПЕРСИСТИТСЯ, НЕИЗМЕНЕН весь цикл (re-issue плана с новым хешем НЕ сбрасывает; стабилен через re-issue, различен между задачами — НЕ plan-hash, НЕ prompt).

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

```js
// tools/router-task-id.test.mjs
import { describe, it, expect } from 'vitest';
import { deriveTaskId } from './router-task-id.mjs';

describe('deriveTaskId (✅O17)', () => {
  it('первый план → присвоить из firstPlanHash', () => {
    expect(deriveTaskId({ existingTaskId: null, firstPlanHash: 'abc123' })).toBe('task:abc123');
  });
  it('существующий task-id СТАБИЛЕН через re-issue (новый хеш НЕ сбрасывает)', () => {
    const id = deriveTaskId({ existingTaskId: 'task:abc123', firstPlanHash: 'NEWHASH999' });
    expect(id).toBe('task:abc123'); // персист, не сброс
  });
  it('разные задачи (разный firstPlanHash, нет existing) → разные id', () => {
    expect(deriveTaskId({ existingTaskId: null, firstPlanHash: 'p1' }))
      .not.toBe(deriveTaskId({ existingTaskId: null, firstPlanHash: 'p2' }));
  });
  it('пустой existing и пустой firstPlanHash → null (нечего якорить)', () => {
    expect(deriveTaskId({ existingTaskId: '', firstPlanHash: '' })).toBe(null);
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/router-task-id.test.mjs --reporter dot`
Expected: FAIL — `Cannot find module './router-task-id.mjs'`.

- [ ] **Step 3: Write minimal implementation**

```js
// tools/router-task-id.mjs
#!/usr/bin/env node
/**
 * router-task-id (✅O17) — persisted first-plan-anchor. task-id присваивается при ПЕРВОМ
 * плане задачи и НЕ меняется через re-issue (новый plan-hash не сбрасывает). Чистое ядро;
 * персист (loadTaskId/saveTaskId) — отдельно, инъектируемый fs, assertSafeSessionId guard.
 */
import { assertSafeSessionId } from './action-journal.mjs';

/** Стабильный task-id: существующий → возвращается как есть (персист); иначе якорь из
 *  firstPlanHash (первый план задачи). Нет ни того ни другого → null. */
export function deriveTaskId({ existingTaskId = null, firstPlanHash = '' } = {}) {
  if (typeof existingTaskId === 'string' && existingTaskId.trim()) return existingTaskId;
  const h = String(firstPlanHash || '').trim();
  return h ? `task:${h}` : null;
}

function taskIdPath(runtimeDir, sessionId) {
  assertSafeSessionId(sessionId);
  const sep = runtimeDir.endsWith('/') ? '' : '/';
  return `${runtimeDir}${sep}router-task-id-${sessionId}.txt`;
}

/** Загрузить персистнутый task-id (или null). fsImpl инъектируем. */
export function loadTaskId({ sessionId, runtimeDir, fsImpl }) {
  try { return String(fsImpl.readFileSync(taskIdPath(runtimeDir, sessionId), 'utf8')).trim() || null; }
  catch (e) { if (e && e.code === 'ENOENT') return null; throw e; }
}

/** Персистнуть task-id (идемпотентно — A0 пишет один раз при первом плане). */
export function saveTaskId({ taskId, sessionId, runtimeDir, fsImpl }) {
  fsImpl.writeFileSync(taskIdPath(runtimeDir, sessionId), String(taskId || ''));
}
```

- [ ] **Step 4: Run test to verify it passes**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/router-task-id.test.mjs --reporter dot`
Expected: PASS — 4 passed.

- [ ] **Step 5: Commit**

```bash
git add tools/router-task-id.mjs tools/router-task-id.test.mjs
git commit -m "feat(router-task-id): deriveTaskId persisted anchor (O17, sub-plan C Task 1)"
```

---

### Task 2: `mentor-journal.mjs` — журнал переговоров (tamper-evident, §6.3)

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

**§6.3 SE-R6-4.** Журнал переговоров ≠ М1 action-journal, но tamper-evident на той же хеш-цепи (зеркало М1) — переиспуём примитивы `appendEntry`/`verifyChain` `[action-journal.mjs:25-41,53-68]`. Payload несёт task-id (✅O17 binding) + круг + сторону + реплику + обоснование (ДР-6 симметрия: каждая сторона обязана обоснование). Счётчик кругов per-ЗАДАЧЕ.

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

```js
// tools/mentor-journal.test.mjs
import { describe, it, expect } from 'vitest';
import { appendNegotiation, verifyNegotiation, roundCount } from './mentor-journal.mjs';

const KEY = 'test-key';

describe('mentor-journal (§6.3 tamper-evident)', () => {
  it('append + verify цепь цела', () => {
    let s = { entries: [], headSig: null };
    s = appendNegotiation(s.entries, { taskId: 'task:a', round: 1, side: 'mentor', utterance: 'расширь план', justification: 'риск X' }, { key: KEY, nowMs: 1 });
    s = appendNegotiation(s.entries, { taskId: 'task:a', round: 1, side: 'claude', utterance: 'не согласен', justification: 'Y покрывает' }, { key: KEY, nowMs: 2 });
    expect(verifyNegotiation(s.entries, s.headSig, { key: KEY }).ok).toBe(true);
  });
  it('подмена записи задним числом → verify false', () => {
    let s = { entries: [], headSig: null };
    s = appendNegotiation(s.entries, { taskId: 'task:a', round: 1, side: 'mentor', utterance: 'A', justification: 'j' }, { key: KEY, nowMs: 1 });
    s = appendNegotiation(s.entries, { taskId: 'task:a', round: 2, side: 'mentor', utterance: 'B', justification: 'j' }, { key: KEY, nowMs: 2 });
    s.entries[0].payload.utterance = 'ПОДМЕНА';
    expect(verifyNegotiation(s.entries, s.headSig, { key: KEY }).ok).toBe(false);
  });
  it('обоснование обязательно (ДР-6) — пустое → throw', () => {
    expect(() => appendNegotiation([], { taskId: 'task:a', round: 1, side: 'mentor', utterance: 'X', justification: '' }, { key: KEY, nowMs: 1 }))
      .toThrow(/обоснование/);
  });
  it('roundCount считает максимальный круг для task-id', () => {
    let s = { entries: [], headSig: null };
    s = appendNegotiation(s.entries, { taskId: 'task:a', round: 1, side: 'mentor', utterance: 'A', justification: 'j' }, { key: KEY, nowMs: 1 });
    s = appendNegotiation(s.entries, { taskId: 'task:a', round: 2, side: 'claude', utterance: 'B', justification: 'j' }, { key: KEY, nowMs: 2 });
    s = appendNegotiation(s.entries, { taskId: 'task:b', round: 1, side: 'mentor', utterance: 'C', justification: 'j' }, { key: KEY, nowMs: 3 });
    expect(roundCount(s.entries, 'task:a')).toBe(2);
    expect(roundCount(s.entries, 'task:b')).toBe(1);
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/mentor-journal.test.mjs --reporter dot`
Expected: FAIL — `Cannot find module './mentor-journal.mjs'`.

- [ ] **Step 3: Write minimal implementation**

```js
// tools/mentor-journal.mjs
#!/usr/bin/env node
/**
 * mentor-journal (§6.3 SE-R6-4) — журнал переговоров наставник↔Claude. tamper-evident
 * на хеш-цепи (зеркало М1) — переиспуём action-journal-примитивы. Payload несёт task-id
 * (✅O17), круг, сторону, реплику, обоснование (ДР-6: каждая сторона ОБЯЗАНА обоснование).
 * Журнал ≠ М1 action-journal: своя семантика записи + per-task счётчик кругов.
 */
import { appendEntry, verifyChain } from './action-journal.mjs';

/** Добавить запись переговоров. Обоснование обязательно (ДР-6) → throw при пустом. */
export function appendNegotiation(entries, { taskId, round, side, utterance, justification }, { key, nowMs }) {
  if (!String(justification || '').trim()) throw new Error('обоснование обязательно (ДР-6 симметрия)');
  const payload = {
    task_id: String(taskId || ''), round: Number(round) || 0,
    side: String(side || ''), utterance: String(utterance || ''), justification: String(justification),
  };
  return appendEntry(Array.isArray(entries) ? entries : [], payload, { key, nowMs });
}

/** Проверить цепь переговоров (зеркало verifyChain М1). */
export function verifyNegotiation(entries, headSig, { key }) {
  return verifyChain(entries, headSig, { key });
}

/** Максимальный номер круга для task-id (per-ЗАДАЧЕ счётчик, R2-SE-d). */
export function roundCount(entries, taskId) {
  let max = 0;
  for (const e of (Array.isArray(entries) ? entries : [])) {
    const p = e && e.payload;
    if (p && p.task_id === taskId && Number(p.round) > max) max = Number(p.round);
  }
  return max;
}
```

- [ ] **Step 4: Run test to verify it passes**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/mentor-journal.test.mjs --reporter dot`
Expected: PASS — 4 passed.

- [ ] **Step 5: Commit**

```bash
git add tools/mentor-journal.mjs tools/mentor-journal.test.mjs
git commit -m "feat(mentor-journal): tamper-evident журнал переговоров (§6.3, sub-plan C Task 2)"
```

---

### Task 3: `mentor-verdict.mjs` — СВОИ слоты + substance (§6.1)

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

**§6.1 R2-SE-c/SE-R7-8.** Вердикт наставника СОДЕРЖАТЕЛЕН по СОБСТВЕННЫМ слотам (непустые, адресуют план ПО-ПУНКТАМ) — паттерн-зеркало `validateTrace`, но СВОИ слоты, НЕ судьи `validateVerdictSlots`. **SE-R6-6:** substance = РЕАЛЬНЫЙ заход (wired:true), НЕ stub/degraded (зеркало: судья `{decision:'GO', wired:false}` НЕ суд).

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

```js
// tools/mentor-verdict.test.mjs
import { describe, it, expect } from 'vitest';
import { validateMentorVerdict, isMentorVerdictSubstantive, MENTOR_VERDICT_SLOTS } from './mentor-verdict.mjs';

describe('mentor-verdict (§6.1 СВОИ слоты)', () => {
  it('MENTOR_VERDICT_SLOTS заморожен и СВОЙ (не судейский)', () => {
    expect(Object.isFrozen(MENTOR_VERDICT_SLOTS)).toBe(true);
    expect(MENTOR_VERDICT_SLOTS).toEqual(['plan_points_addressed', 'reasoning', 'recommendation', 'confidence']);
  });
  it('валидный вердикт → ok', () => {
    const v = { plan_points_addressed: ['шаг 1 ок', 'шаг 2 риск'], reasoning: 'разбор', recommendation: 'править шаг 2', confidence: 0.8 };
    expect(validateMentorVerdict(v).ok).toBe(true);
  });
  it('пустой слот → не ok', () => {
    const v = { plan_points_addressed: [], reasoning: 'x', recommendation: 'y', confidence: 0.5 };
    expect(validateMentorVerdict(v).ok).toBe(false);
    expect(validateMentorVerdict(v).missingSlots).toContain('plan_points_addressed');
  });
  it('confidence вне [0,1]/NaN → не ok', () => {
    const base = { plan_points_addressed: ['a'], reasoning: 'r', recommendation: 'rec' };
    expect(validateMentorVerdict({ ...base, confidence: 5 }).ok).toBe(false);
    expect(validateMentorVerdict({ ...base, confidence: NaN }).ok).toBe(false);
  });
  it('substance: wired:true + валиден → содержателен; wired:false → НЕ содержателен (SE-R6-6)', () => {
    const v = { plan_points_addressed: ['a'], reasoning: 'r', recommendation: 'rec', confidence: 0.7 };
    expect(isMentorVerdictSubstantive(v, { wired: true })).toBe(true);
    expect(isMentorVerdictSubstantive(v, { wired: false })).toBe(false);
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/mentor-verdict.test.mjs --reporter dot`
Expected: FAIL — `Cannot find module './mentor-verdict.mjs'`.

- [ ] **Step 3: Write minimal implementation**

```js
// tools/mentor-verdict.mjs
#!/usr/bin/env node
/**
 * mentor-verdict (§6.1) — вердикт наставника со СВОИМИ слотами (НЕ судейские
 * validateVerdictSlots/линзы). Паттерн-зеркало validateTrace, но собственный набор.
 * substance (SE-R6-6): содержателен = валиден ПО слотам И wired:true (реальный router-вызов,
 * не stub/degraded). Зеркало: судья {decision:'GO', wired:false} не суд.
 */

/** СОБСТВЕННЫЕ слоты наставника (адресуют план по-пунктам). */
export const MENTOR_VERDICT_SLOTS = Object.freeze(['plan_points_addressed', 'reasoning', 'recommendation', 'confidence']);

/** Валидация вердикта по СВОИМ слотам. plan_points_addressed — непустой массив;
 *  reasoning/recommendation — непустые строки; confidence — конечное число ∈[0,1]. */
export function validateMentorVerdict(verdict) {
  const missingSlots = [];
  if (!verdict || typeof verdict !== 'object') return { ok: false, missingSlots: [...MENTOR_VERDICT_SLOTS] };
  if (!Array.isArray(verdict.plan_points_addressed) || verdict.plan_points_addressed.length === 0) missingSlots.push('plan_points_addressed');
  for (const slot of ['reasoning', 'recommendation']) {
    if (typeof verdict[slot] !== 'string' || !verdict[slot].trim()) missingSlots.push(slot);
  }
  if (typeof verdict.confidence !== 'number' || !Number.isFinite(verdict.confidence) || verdict.confidence < 0 || verdict.confidence > 1) missingSlots.push('confidence');
  return { ok: missingSlots.length === 0, missingSlots };
}

/** Содержателен ли вердикт: валиден ПО слотам И из РЕАЛЬНОГО router-вызова (wired:true). */
export function isMentorVerdictSubstantive(verdict, { wired = false } = {}) {
  return wired === true && validateMentorVerdict(verdict).ok;
}
```

- [ ] **Step 4: Run test to verify it passes**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/mentor-verdict.test.mjs --reporter dot`
Expected: PASS — 5 passed.

- [ ] **Step 5: Commit**

```bash
git add tools/mentor-verdict.mjs tools/mentor-verdict.test.mjs
git commit -m "feat(mentor-verdict): СВОИ слоты + substance wired (§6.1, sub-plan C Task 3)"
```

---

### Task 3b: `mentor-verdict.mjs` ч.2 — производитель вердикта `buildMentorVerdictPrompt` + `runMentorVerdict` (корень C-1, нах.F4/F5)

**Files:**
- Modify: `tools/mentor-verdict.mjs`
- Test: `tools/mentor-verdict.test.mjs`

**C-1 [КРИТ, ревью 2026-06-10].** Гейт freeze потребляет `mentorVerdict`, но НИ ОДИН код его не ПРОИЗВОДИТ: `runMentorRound` (T5) даёт router-трассу `{candidates,chosen,...}`, промпт T4 просит router-слоты. Вердикт (`{plan_points_addressed,reasoning,recommendation,confidence}`) — это ДРУГОЙ выход (спека R6.3 §6.1). Эта задача строит ПРОИЗВОДИТЕЛЯ: свой verdict-промпт + runner-зеркало `runRouter`, кладёт `plan_hash` (binding нах.F4).

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

```js
// добавить в tools/mentor-verdict.test.mjs
import { buildMentorVerdictPrompt, runMentorVerdict } from './mentor-verdict.mjs';

describe('buildMentorVerdictPrompt (§6.1 verdict-слоты, НЕ router)', () => {
  it('возвращает {system,user}; user — план по-пунктам + проверенный контекст; system просит verdict-слоты', () => {
    const p = buildMentorVerdictPrompt({
      plan: { steps: [{ n: 1, op: 'Write', object: 'a.mjs' }] },
      verifiedContext: [{ kind: 'EXTRACTED', claim: 'есть runRouter', ref: 'router-engine.mjs:140' }],
      negotiationLog: [{ round: 1, side: 'mentor', utterance: 'расширь', justification: 'риск' }],
      graphSection: { layer0: [{ district: 'tools', nodeCount: 3 }] },
    });
    expect(typeof p.system).toBe('string');
    expect(typeof p.user).toBe('string');
    expect(p.system).toMatch(/plan_points_addressed/);          // СВОИ слоты, не router
    expect(p.system).not.toMatch(/candidates|twins_considered/); // НЕ router-слоты
    expect(p.user).toMatch(/runRouter/);                         // проверенный контекст
  });
});

describe('runMentorVerdict (§6.1 производитель + binding нах.F4)', () => {
  const goodVerdict = { plan_points_addressed: ['шаг 1 ок'], reasoning: 'r', recommendation: 'rec', confidence: 0.8 };
  it('валидный вердикт → ok + wired + verdict.plan_hash проставлен (binding)', async () => {
    const r = await runMentorVerdict({ plan: {}, planHash: 'PH1', llmCall: async () => goodVerdict });
    expect(r.ok).toBe(true);
    expect(r.wired).toBe(true);
    expect(r.verdict.plan_hash).toBe('PH1');
  });
  it('пустой слот → ok:false (не содержателен)', async () => {
    const r = await runMentorVerdict({ plan: {}, planHash: 'PH1', llmCall: async () => ({ ...goodVerdict, plan_points_addressed: [] }) });
    expect(r.ok).toBe(false);
  });
  it('сбой вызова → ok:false, wired:false (SE-R6-6: не суд)', async () => {
    const r = await runMentorVerdict({ plan: {}, planHash: 'PH1', llmCall: async () => { throw new Error('сеть'); } });
    expect(r.ok).toBe(false);
    expect(r.wired).toBe(false);
  });
});
```

- [ ] **Step 2: Run — FAIL** (`buildMentorVerdictPrompt is not a function`).

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/mentor-verdict.test.mjs --reporter dot`

- [ ] **Step 3: Write minimal implementation**

```js
// добавить в tools/mentor-verdict.mjs (после isMentorVerdictSubstantive)
import { parseRouterResponse } from './router-engine.mjs';

/**
 * Промпт-производитель вердикта (§6.1): зеркало идиомы buildRouterPrompt {system,user},
 * но system просит ИМЕННО verdict-слоты (plan_points_addressed по-пунктам / reasoning /
 * recommendation / confidence), НЕ router-слоты. user — план по-пунктам + проверенный контекст
 * + журнал переговоров. Граф-секция — для опоры наставника.
 */
export function buildMentorVerdictPrompt({ plan = null, verifiedContext = [], negotiationLog = [], graphSection = null } = {}) {
  const system = [
    'Ты — НАСТАВНИК. Разбери ПЛАН ПО ПУНКТАМ (не выбирай скил — это другой вызов).',
    'ВЫВОД — строго JSON-вердикт со слотами: plan_points_addressed (массив: по КАЖДОМУ пункту плана — статус+замечание), reasoning (разбор), recommendation (что править), confidence (0..1). Пустой слот недопустим.',
  ].join('\n');
  const vc = (Array.isArray(verifiedContext) ? verifiedContext : []).map((e) => `[${e.kind || '?'}] ${e.claim || ''} (${e.ref || ''})`).join('\n');
  const neg = (Array.isArray(negotiationLog) ? negotiationLog : []).map((e) => `круг ${e.round} [${e.side}] ${e.utterance} — ${e.justification || ''}`).join('\n');
  const user = [
    `ПЛАН: ${plan ? JSON.stringify(plan) : '(нет)'}`,
    vc ? `--- ПРОВЕРЕННЫЙ КОНТЕКСТ ---\n${vc}` : '',
    neg ? `--- ПЕРЕГОВОРЫ ---\n${neg}` : '',
    graphSection ? `--- ГРАФ (карта районов) ---\n${JSON.stringify(graphSection.layer0 || [])}` : '',
  ].filter(Boolean).join('\n');
  return { system, user };
}

/**
 * Производитель вердикта (§6.1, зеркало runRouter): промпт → llmCall (инъект) → parse →
 * validateMentorVerdict → при валидном wired:true + verdict с plan_hash (binding нах.F4).
 * Сбой/невалид → wired-флаг по факту захода (SE-R6-6: сбой → wired:false, не суд).
 */
export async function runMentorVerdict({ plan = null, verifiedContext = [], negotiationLog = [], graphSection = null, planHash = null, llmCall }) {
  let v;
  try {
    v = await llmCall({ buildPrompt: () => buildMentorVerdictPrompt({ plan, verifiedContext, negotiationLog, graphSection }) });
  } catch { return { ok: false, wired: false, reason: 'сбой вызова наставника-вердикта', verdict: null }; }
  if (typeof v === 'string') v = parseRouterResponse(v);
  if (!v) return { ok: false, wired: false, reason: 'пустой/неразборный вердикт', verdict: null };
  const chk = validateMentorVerdict(v);
  if (!chk.ok) return { ok: false, wired: true, reason: `несодержательный вердикт: пустые слоты [${chk.missingSlots.join(', ')}]`, verdict: v };
  return { ok: true, wired: true, verdict: { ...v, plan_hash: planHash } };
}
```

- [ ] **Step 4: Run — PASS** (4 новых + 5 прежних mentor-verdict = 9 passed).
- [ ] **Step 5: Commit** `feat(mentor-verdict): производитель runMentorVerdict + binding plan_hash (C-1/F4, sub-plan C Task 3b)`.

---

### Task 4: `mentor-seam.mjs` ч.1 — `buildMentorPrompt` (обогащённый вход, §6.2)

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

**§6.2 CD-R6-G/H.** Обёртка `buildRouterPrompt` `[router-engine.mjs:98]` + ПРОВЕРЕННЫЙ контекст (из A, как данные) + журнал переговоров задачи + граф-секция (выход B `buildGraphSection`, как данные). CD-R6-G: граф — ОТДЕЛЬНАЯ секция (передаётся в `graph`-параметр `buildRouterPrompt`). CD-R6-H: контекст — ПРОВЕРЕННЫЙ артефакт, не сырой чат. Граф-секция и контекст ПЕРЕДАЮТСЯ (НЕ импорт B/A) → автономность C.

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

```js
// tools/mentor-seam.test.mjs
import { describe, it, expect } from 'vitest';
import { buildMentorPrompt } from './mentor-seam.mjs';

describe('buildMentorPrompt (§6.2)', () => {
  const graphSection = { kind: 'project-graph', districtCount: 2, layer0: [{ district: 'tools', nodeCount: 3 }], staleness: { stale: true, commits_behind: 12, uncommitted: 1 } };
  const verifiedContext = [{ id: '1', claim: 'есть runRouter', ref: 'router-engine.mjs:140', kind: 'EXTRACTED' }];
  const negotiationLog = [{ round: 1, side: 'mentor', utterance: 'расширь', justification: 'риск' }];
  it('возвращает {system, user}', () => {
    const p = buildMentorPrompt({ prompt: 'сделай X', graphSection, verifiedContext, negotiationLog, catalog: { nodes: [{ id: 'n1', slug: 's', capabilities: 'c' }] } });
    expect(typeof p.system).toBe('string');
    expect(typeof p.user).toBe('string');
  });
  it('user содержит проверенный контекст и журнал переговоров', () => {
    const p = buildMentorPrompt({ prompt: 'X', graphSection, verifiedContext, negotiationLog });
    expect(p.user).toMatch(/ПРОВЕРЕННЫЙ КОНТЕКСТ/);
    expect(p.user).toMatch(/runRouter/);
    expect(p.user).toMatch(/ПЕРЕГОВОРЫ/);
    expect(p.user).toMatch(/расширь/);
  });
  it('staleness виден наставнику (✅O16 сигнал)', () => {
    const p = buildMentorPrompt({ prompt: 'X', graphSection, verifiedContext, negotiationLog });
    expect(p.user).toMatch(/staleness|устарел|commits_behind/i);
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/mentor-seam.test.mjs --reporter dot`
Expected: FAIL — `Cannot find module './mentor-seam.mjs'`.

- [ ] **Step 3: Write minimal implementation**

```js
// tools/mentor-seam.mjs
#!/usr/bin/env node
/**
 * mentor-seam (§6) — живой шов наставника. ч.1 buildMentorPrompt: обёртка buildRouterPrompt
 * + проверенный контекст (из A, данные) + журнал переговоров + граф-секция (выход B, данные).
 * НЕ импортирует A/B — секция/контекст ПЕРЕДАЮТСЯ (автономность C; боевая проводка передаёт
 * реальные). Граф = ОТДЕЛЬНАЯ секция (CD-R6-G); контекст = ПРОВЕРЕННЫЙ артефакт (CD-R6-H).
 */
import { buildRouterPrompt } from './router-engine.mjs';

function renderVerifiedContext(vc) {
  const list = Array.isArray(vc) ? vc : [];
  if (!list.length) return '';
  const lines = list.map((e) => `[${e.kind || '?'}] ${e.claim || ''} (${e.ref || ''})`);
  return ['--- ПРОВЕРЕННЫЙ КОНТЕКСТ (резолв цитат, НЕ истина — ревью владельца обязателен) ---', ...lines].join('\n');
}

function renderNegotiation(log) {
  const list = Array.isArray(log) ? log : [];
  if (!list.length) return '';
  const lines = list.map((e) => `круг ${e.round} [${e.side}] ${e.utterance} — обоснование: ${e.justification || ''}`);
  return ['--- ПЕРЕГОВОРЫ ЗАДАЧИ (история кругов) ---', ...lines].join('\n');
}

function renderStaleness(graphSection) {
  const st = graphSection && graphSection.staleness;
  if (!st) return '';
  return `--- СВЕЖЕСТЬ ГРАФА (staleness) --- stale=${!!st.stale} commits_behind=${st.commits_behind ?? '?'} uncommitted=${st.uncommitted ?? '?'}`;
}

/**
 * Построить промпт наставника = buildRouterPrompt(graph=граф-секция B) + проверенный
 * контекст + журнал переговоров + явный staleness-сигнал в user (волатильное). {system,user}.
 */
export function buildMentorPrompt({ prompt = '', plan = null, graphSection = null, verifiedContext = [], negotiationLog = [], catalog = null, contracts = [] } = {}) {
  const base = buildRouterPrompt({ prompt, plan, graph: graphSection, catalog, contracts });
  const extras = [renderStaleness(graphSection), renderVerifiedContext(verifiedContext), renderNegotiation(negotiationLog)].filter(Boolean).join('\n');
  return { system: base.system, user: [base.user, extras].filter(Boolean).join('\n') };
}
```

- [ ] **Step 4: Run test to verify it passes**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/mentor-seam.test.mjs --reporter dot`
Expected: PASS — 3 passed.

- [ ] **Step 5: Commit**

```bash
git add tools/mentor-seam.mjs tools/mentor-seam.test.mjs
git commit -m "feat(mentor-seam): buildMentorPrompt обогащённый вход (§6.2, sub-plan C Task 4)"
```

---

### Task 5: `mentor-seam.mjs` ч.2 — `runMentorRound` + петля кругов (§4/6.1/6.4)

**Files:**
- Modify: `tools/mentor-seam.mjs`
- Test: `tools/mentor-seam.test.mjs`

**§4/6.1.** Живой вызов поверх `runRouter`-пайплайна `[router-engine.mjs:140-155]` (validateTrace/groundTrace/chosenIsCandidate/abstain), но промпт = `buildMentorPrompt`. `MENTOR_ROUND_CAP=3`; тупик → владелец (симметрия ДР-6, §6.4). `wired:true` при реальном вызове (SE-R6-6), `wired:false` при сбое/stub. `llmCall` инъектируется (живой транспорт — активация владельца).

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

```js
// добавить в tools/mentor-seam.test.mjs
import { runMentorRound, MENTOR_ROUND_CAP } from './mentor-seam.mjs';

describe('runMentorRound (§4/6.1)', () => {
  const graph = { nodes: [{ id: 'writing-plans' }] };
  const trace = { candidates: ['writing-plans'], chosen: 'writing-plans', why_chosen: 'подходит', twins_considered: 'не brainstorming', confidence: 0.9 };
  it('MENTOR_ROUND_CAP = 3', () => { expect(MENTOR_ROUND_CAP).toBe(3); });
  it('валидный трейс → ok, wired:true', async () => {
    const r = await runMentorRound({ prompt: 'X', graph, graphSection: { layer0: [] }, llmCall: async () => trace });
    expect(r.ok).toBe(true);
    expect(r.wired).toBe(true);
    expect(r.abstain).toBe(false);
  });
  it('низкая уверенность → abstain (5.2)', async () => {
    const lo = { ...trace, confidence: 0.2 };
    const r = await runMentorRound({ prompt: 'X', graph, graphSection: { layer0: [] }, llmCall: async () => lo });
    expect(r.abstain).toBe(true);
  });
  it('сбой вызова → ok:false, wired:false (НЕ суд, SE-R6-6)', async () => {
    const r = await runMentorRound({ prompt: 'X', graph, graphSection: { layer0: [] }, llmCall: async () => { throw new Error('сеть'); } });
    expect(r.ok).toBe(false);
    expect(r.wired).toBe(false);
  });
  it('выдуманный узел → ok:false (заземление)', async () => {
    const bad = { candidates: ['ВЫДУМКА'], chosen: 'ВЫДУМКА', why_chosen: 'w', twins_considered: 't', confidence: 0.9 };
    const r = await runMentorRound({ prompt: 'X', graph, graphSection: { layer0: [] }, llmCall: async () => bad });
    expect(r.ok).toBe(false);
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/mentor-seam.test.mjs --reporter dot`
Expected: FAIL — `runMentorRound is not a function`.

- [ ] **Step 3: Write minimal implementation**

```js
// добавить в tools/mentor-seam.mjs
import { parseRouterResponse, validateTrace, groundTrace, chosenIsCandidate } from './router-engine.mjs';

/** §6.3 SE-7: потолок кругов наставника per-ЗАДАЧЕ (×MENTOR_PROBE_CAP probe в D). */
export const MENTOR_ROUND_CAP = 3;

/**
 * Один круг наставника: buildMentorPrompt → llmCall (инъект; живой транспорт — владелец) →
 * пайплайн runRouter (validateTrace/groundTrace/chosenIsCandidate/abstain). wired:true только
 * при реальном завершённом вызове (SE-R6-6: сбой/stub → wired:false, НЕ суд).
 */
export async function runMentorRound({ prompt, plan = null, graph, graphSection = null, verifiedContext = [], negotiationLog = [], catalog = null, contracts = [], llmCall, confidenceThreshold = 0.5 }) {
  let trace;
  try {
    trace = await llmCall({ buildPrompt: () => buildMentorPrompt({ prompt, plan, graphSection, verifiedContext, negotiationLog, catalog, contracts }) });
  } catch {
    return { ok: false, wired: false, abstain: false, reason: 'сбой вызова наставника', trace: null };
  }
  if (typeof trace === 'string') trace = parseRouterResponse(trace);
  if (!trace) return { ok: false, wired: false, abstain: false, reason: 'пустой/неразборный ответ наставника', trace: null };
  const v = validateTrace(trace);
  if (!v.ok) return { ok: false, wired: true, abstain: false, reason: `невалидная трасса: [${v.missingSlots.join(', ')}]`, trace };
  const g = groundTrace(trace, graph);
  if (!g.grounded) return { ok: false, wired: true, abstain: false, reason: `выдуманные узлы: [${g.invented.join(', ')}]`, trace };
  if (!chosenIsCandidate(trace)) return { ok: false, wired: true, abstain: false, reason: 'выбор не среди кандидатов (§6.3)', trace };
  if (trace.confidence < confidenceThreshold) return { ok: true, wired: true, abstain: true, reason: 'низкая уверенность — воздержание (5.2): переспрос/эскалация', trace };
  return { ok: true, wired: true, abstain: false, trace };
}

/**
 * Петля кругов (§4/6.4): крутит runMentorRound пока не согласовано ЛИБО потолок кругов
 * (→ эскалация владельцу, симметрия ДР-6 в ОБА направления). resolved=когда ok && !abstain.
 * Каждый круг — забота вызывающего записать в mentor-journal (вне чистой петли). nowRound —
 * текущий счётчик кругов задачи (из roundCount). @returns {{resolved, escalate, round, last}}
 */
export async function mentorLoop({ runRoundImpl, nowRound = 0, cap = MENTOR_ROUND_CAP }) {
  let round = nowRound;
  let last = null;
  while (round < cap) {
    round += 1;
    last = await runRoundImpl(round);
    if (last && last.ok && !last.abstain) return { resolved: true, escalate: false, round, last };
  }
  return { resolved: false, escalate: true, round, last, reason: `${cap} круга без согласия → к владельцу (ДР-6)` };
}
```

- [ ] **Step 4: Run test to verify it passes + ч.1 тесты**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/mentor-seam.test.mjs --reporter dot`
Expected: PASS — 3 (ч.1) + 5 (ч.2) = 8 passed.

- [ ] **Step 5: Commit**

```bash
git add tools/mentor-seam.mjs tools/mentor-seam.test.mjs
git commit -m "feat(mentor-seam): runMentorRound + петля кругов (§4/6.1, sub-plan C Task 5)"
```

---

### Task 6: `freeze-gate.mjs` — предусловие ПЕРЕД печатью (CD-3 / VA-8 / ✅O2)

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

**§5.6/6.1 CD-3.** ОТДЕЛЬНОЕ предусловие ПЕРЕД `freezePlan`/`freezeArtifact` (plan-lock НЕ трогаем). Проверяет: (а) содержательный mentor-вердикт (VA-8/SE-R6-6 — `validateMentorVerdict`+`wired`); (б) verity — артефакт БЕЗ неразрешённой EXTRACTED (✅O2 — `verityImpl` = `artifactHasUnresolvedExtracted` из A, **инъектируется** → автономность C). fail-CLOSED (SE-3): провал → блок → owner-escape. error (mentor-сбой) vs block (legit) — caller различает retry/escape (R2-SE-h).

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

```js
// tools/freeze-gate.test.mjs
import { describe, it, expect } from 'vitest';
import { freezeGate } from './freeze-gate.mjs';

const goodVerdict = { plan_points_addressed: ['a'], reasoning: 'r', recommendation: 'rec', confidence: 0.8 };
// verityImpl: true = есть неразрешённая EXTRACTED (печать запрещена). Стаб вместо A.
const verityClean = () => false;
const verityDirty = () => true;

describe('freezeGate (CD-3 предусловие)', () => {
  it('валидный вердикт (wired) + чистый контекст → pass', () => {
    const r = freezeGate({ mentorVerdict: goodVerdict, mentorWired: true, verifiedContextArtifact: [{ kind: 'EXTRACTED' }], verityImpl: verityClean });
    expect(r.pass).toBe(true);
  });
  it('нет/невалидный вердикт → блок (VA-8)', () => {
    const r = freezeGate({ mentorVerdict: { reasoning: '' }, mentorWired: true, verifiedContextArtifact: [], verityImpl: verityClean });
    expect(r.pass).toBe(false);
    expect(r.reason).toMatch(/вердикт/i);
  });
  it('вердикт wired:false → блок (SE-R6-6 не суд)', () => {
    const r = freezeGate({ mentorVerdict: goodVerdict, mentorWired: false, verifiedContextArtifact: [], verityImpl: verityClean });
    expect(r.pass).toBe(false);
  });
  it('неразрешённая EXTRACTED в контексте → блок печати артефакта (✅O2)', () => {
    const r = freezeGate({ mentorVerdict: goodVerdict, mentorWired: true, verifiedContextArtifact: [{ kind: 'EXTRACTED' }], verityImpl: verityDirty });
    expect(r.pass).toBe(false);
    expect(r.reason).toMatch(/EXTRACTED|verity|цитат/i);
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/freeze-gate.test.mjs --reporter dot`
Expected: FAIL — `Cannot find module './freeze-gate.mjs'`.

- [ ] **Step 3: Write minimal implementation**

```js
// tools/freeze-gate.mjs
#!/usr/bin/env node
/**
 * freeze-gate (§5.6/6.1 CD-3) — ОТДЕЛЬНОЕ предусловие ПЕРЕД freezePlan/freezeArtifact.
 * plan-lock/seal/М6 НЕ трогаются (CD-3): caller сначала зовёт freezeGate, и ТОЛЬКО при
 * pass:true вызывает freezePlan/freezeArtifact. Проверки: (а) содержательный mentor-вердикт
 * (VA-8/SE-R6-6); (б) verity — нет неразрешённой EXTRACTED (✅O2). verityImpl инъектируется
 * (в проде artifactHasUnresolvedExtracted из A) → C автономна. fail-CLOSED: провал → блок.
 */
import { validateMentorVerdict, isMentorVerdictSubstantive } from './mentor-verdict.mjs';

/** @returns {{pass, reason}} — pass:true только когда ОБА условия выполнены. */
export function freezeGate({ mentorVerdict, mentorWired = false, verifiedContextArtifact = [], verityImpl }) {
  if (!isMentorVerdictSubstantive(mentorVerdict, { wired: mentorWired })) {
    const v = validateMentorVerdict(mentorVerdict);
    return { pass: false, reason: !mentorWired
      ? 'mentor-вердикт не из реального захода (wired:false) — не суд (SE-R6-6)'
      : `mentor-вердикт несодержателен: пустые слоты [${v.missingSlots.join(', ')}] (VA-8)` };
  }
  let dirty;
  try { dirty = typeof verityImpl === 'function' ? !!verityImpl(verifiedContextArtifact) : true; }
  catch { dirty = true; } // fail-CLOSED: сбой verity → считаем грязным
  if (dirty) return { pass: false, reason: 'неразрешённая EXTRACTED-цитата в контексте — печать артефакта отказана (✅O2)' };
  return { pass: true, reason: 'mentor-вердикт содержателен + verity чист' };
}
```

- [ ] **Step 4: Run test to verify it passes**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/freeze-gate.test.mjs --reporter dot`
Expected: PASS — 4 passed.

- [ ] **Step 5: Commit**

```bash
git add tools/freeze-gate.mjs tools/freeze-gate.test.mjs
git commit -m "feat(freeze-gate): предусловие mentor-вердикт + verity (CD-3/VA-8/O2, sub-plan C Task 6)"
```

---

### Task 7: Регрессия tools-vitest GREEN

**Files:** (нет правок — проверка)

- [ ] **Step 1: Прогнать ВЕСЬ tools-набор**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs --reporter dot`
Expected: PASS — весь tools-набор зелёный, 5 новых файлов C включены, 0 регрессий в машинах М1-М7 и sub-plan A/B/D/E. Зафиксировать число passed.

- [ ] **Step 2: Если RED — systematic-debugging**

При падении — НЕ править наугад: `superpowers:systematic-debugging`, root cause, точечный фикс, повтор Step 1.

- [ ] **Step 3: Commit (если были фиксы Step 2; иначе пропустить)**

```bash
git add tools/
git commit -m "fix(mentor-seam): регрессия tools-vitest GREEN (sub-plan C Task 7)"
```

---

## R6.3 amendments (ревью 5-скил цепочки 2026-06-10)

Поправки к задачам выше по итогам ревью (исполнять ВМЕСТЕ с задачами; OLD→NEW контракты):

**A1 (Task 6 freezeGate — binding нах.F4 + rename SE3).** Сигнатура OLD `freezeGate({mentorVerdict, mentorWired, verifiedContextArtifact, verityImpl})` → NEW `freezeGate({mentorVerdict, mentorWired, planHash, verifiedContextArtifact, hasUnresolvedExtractedImpl})`. (а) ДО substance-проверки: `if (planHash != null && mentorVerdict?.plan_hash !== planHash) return {pass:false, reason:'вердикт не привязан к замораживаемому плану — нах.F4'}` (stale/чужой вердикт не проходит). (б) `verityImpl` → `hasUnresolvedExtractedImpl` ВЕЗДЕ + JSDoc-полярность «true=ЕСТЬ неразрешённая EXTRACTED=ГРЯЗНО=блок (зеркало artifactHasUnresolvedExtracted из A)». Тесты Task 6: обновить вызовы под новую сигнатуру + NEW-тест «verdict.plan_hash≠planHash → pass:false».

**A2 (Task 5 runMentorRound — rename SE2).** Параметр `graph` (нод-граф для groundTrace) → `groundingGraph`; `graphSection` (B-секция для промпта) — оставить. Внутри `groundTrace(trace, groundingGraph)`. JSDoc: «groundingGraph = ПОЛНЫЙ нод-граф (заземление, CD-R6-G против полного); graphSection = прунённая карта районов (в промпт). Своп ломает заземление (SE2)». Тесты Task 5 — передают `groundingGraph`.

**A3 (Task 4 buildMentorPrompt — assert districts нах.F1/C-2).** В тест T4 добавить `expect(p.user).toMatch(/tools|app-backend/)` (имена районов из `graphSection.layer0`). Нота: «districts рендерятся через CD-R6-G-правку buildRouterPrompt (C2-W1); без неё layer0 теряется (нах.F1) — buildMentorPrompt лишь передаёт `graph:graphSection`».

**A4 (Task 2 appendNegotiation — SE10).** Нота: throw на пустом обосновании ОБЯЗАН ловиться вызывающим (onPlanWrite/контроллер) try/catch → НЕ крах круга; пустое обоснование → требовать заново (переспрос), не падать. Производитель (C2-W3) оборачивает.

**A5 (scope/«Боевая проводка» — onPlanWrite в C2-W3).** `onPlanWrite` — задача C2 (манифест), НЕ C. В «Боевой проводке» A0-строка: «onPlanWrite строит C2-W3; здесь — регистрация в settings.json (owner)». freezeGate-строка использует `hasUnresolvedExtractedImpl` (не `verityImpl`), `readFileImpl` per C2-W7/C-3.

**A6 (нах.F7 — дозапрос соседнего района, под-задача Task 5+).** `runMentorRound` распознаёт в трассе `request_district:<name>` → сигнал `escalateDistrict`; контроллер фетчит `districtDetail(neighbor)` (B) → в `graphSection` следующего круга; `DISTRICT_PROBE_CAP=2/круг` (дешёвое чтение КАРТЫ, не файлов; ДР-P1). Тест: трасса с request_district → escalateDistrict; сверх cap → отказ.

**A7 (Self-review C — обновить).** +Task 3b (производитель вердикта), +binding plan_hash (нах.F4), +renames (groundingGraph/hasUnresolvedExtractedImpl), +F7 escalateDistrict. Type consistency: `runMentorVerdict`→`{ok,wired,verdict,reason?}`; `freezeGate`→`{pass,reason}` (новые поля planHash/hasUnresolvedExtractedImpl).

**A8 (нах.F6 — ДР-1 гранулярность, lightweight).** В `buildMentorPrompt` (Task 4) И `buildMentorVerdictPrompt` (Task 3b) system добавить строку: «единица плана = растяжка до следующей НЕИЗВЕСТНОСТИ (ДР-1); громкость церемонии по риску — `detectHighRisk` `[router-engine.mjs:19]` (atomic/trivial → тихо; прод/многошаг/разрушительное → громко)». Поведенческая инъекция в промпт, не код-логика.

---

## Боевая проводка (НЕ в scope C — после реализации A+B + активации владельца)

> Этот раздел — карта интеграции, НЕ задачи C. Реализуется отдельной фазой/sub-plan C2 после A+B.
- `freezeGate(...)` зовётся с `verityImpl = (art) => artifactHasUnresolvedExtracted(art, fs.readFileSync)` (A Task 5).
- `runMentorRound(...)` получает `graphSection = buildGraphSection({ districtMap: buildDistrictMap(...), staleness: readStaleness(...) })` (B Task 4/6/7).
- `llmCall` = реальный `callAnthropicAPI` транспорт (ключ из keychain, флаг) — активация владельца.
- A0-хук: регистрация `onPlanWrite` в `.claude/settings.json` (PostToolUse на запись плана) — шаг владельца; пишет/читает task-id (Task 1) + инициирует первый круг наставника.
- mentor-журнал персист: `appendNegotiation` + atomic-write в `~/.claude/runtime/mentor-journal-<sess>.jsonl` (зеркало action-journal персиста) — добавить тонкую I/O-обёртку при проводке.

---

## Self-Review (writing-plans)

**1. Spec coverage:**
- ✅O17 task-id persisted first-plan-anchor → Task 1. ✅
- §6.3 SE-R6-4 tamper-evident журнал переговоров (хеш-цепь зеркало М1, task-id-bound, per-task счётчик) → Task 2. ✅
- §6.1 R2-SE-c/SE-R7-8/SE-R6-6 mentor-вердикт СВОИ слоты + substance (wired) → Task 3. ✅
- §6.2 CD-R6-G/H обогащённый вход (проверенный контекст A + журнал + граф-секция B, staleness-сигнал) → Task 4. ✅
- §4/6.1/6.4 живой runMentorRound + петля кругов ≤3 + тупик→владелец (ДР-6) → Task 5. ✅
- §5.6/6.1 CD-3 freeze-gate предусловие (VA-8 mentor-вердикт + ✅O2 verity, plan-lock не трогаем) → Task 6. ✅
- Регрессия → Task 7. ✅
- **Вне C (боевая проводка):** verityImpl=A, graphSection=B, живой llmCall, A0-хук регистрация, журнал-персист — раздел «Боевая проводка» (после A+B + активация владельца).

**2. Placeholder scan:** реальный код в каждом шаге; зависимости A/B РАЗВЯЗАНЫ инъекцией (`verityImpl`, `graphSection`/`verifiedContext` как данные) → модули+тесты C автономны; команды Run с ожидаемым выводом; нет «TBD». ✅

**3. Type consistency:** `deriveTaskId`→string|null; `appendNegotiation`→`{entries,headSig}`; `verifyNegotiation`→`{ok,brokenAt}`; `roundCount`→number; `validateMentorVerdict`→`{ok,missingSlots}`; `isMentorVerdictSubstantive`→boolean; `buildMentorPrompt`→`{system,user}`; `runMentorRound`→`{ok,wired,abstain,trace,reason?}`; `mentorLoop`→`{resolved,escalate,round,last}`; `freezeGate`→`{pass,reason}`. Имена согласованы. ✅

**Известное ограничение (честно):** C НЕ модифицирует `plan-lock.mjs`/`enforce-supreme-gate.mjs` (CD-3) — freeze-gate работает как ПРЕДУСЛОВИЕ, которое caller обязан соблюсти (вызвать freezeGate перед freezePlan/freezeArtifact). Энфорсмент этого порядка (что caller не обойдёт freezeGate) — задача БОЕВОЙ ПРОВОДКИ/активации (owner регистрирует шов так, что freeze идёт только через gate), не самого freeze-gate. Это согласуется с asymptote floor (behavioral-residual): машины энфорсят чекпойнты, оркестрацию диалога ведёт контроллер (CD-R6-A). Живой `llmCall` и A0-хук — активация владельца; до неё шов инертен.
