# E/G warm-up batch — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: исполнять **инлайн** через `superpowers:executing-plans` (субагенты/Task/Workflow ЗАПРЕЩЕНЫ в этой сессии). Шаги — checkbox (`- [ ]`).

**Goal:** Закрыть 6 хвостов E/G (точность наблюдателя + деталь-доска + косметика + DOC-1) без дыр в машинах М1–М7.

**Architecture:** Чистые функции наблюдателя (Машина 0 / dashboard). Затронут единственный гейт — observer routing-gate (Stop-block); правка сохраняет его FP-смещение. Жёсткие стены М2/М5/М4/М6 не трогаются. Все правки прошли adversarial-анализ (spec §«Анализ-фаза», 4 правки безопасности SE-1..SE-4).

**Tech Stack:** Node ESM (`tools/*.mjs`), vitest tools-config.

**Источник:** spec `docs/superpowers/specs/2026-06-08-router-mentor-eg-warmup-batch-design.md`.

---

## Общие правила исполнения

- **Дерево:** `c:\моя\проекты\портал crm\Документация\.claude\worktrees\brainrepo` (далее `<wt>`).
- **Тесты (квирк):** `npx vitest run --root "<wt>/app" --config "<wt>/app/vitest.config.tools.mjs" <фильтр> --reporter dot` — БЕЗ `cd app`. Фильтр — подстрока имени тест-файла.
- **Полная регрессия:** та же команда без фильтра; baseline **3174 passed / 2 skipped**, после батча — больше (новые тесты), 0 failed.
- **Git:** только PowerShell, `git -C "<wt>" ...`, по одной логической команде. Коммит — `git -C "<wt>" commit -m "..." -m "..."` (НЕ через `.scratch/*.txt` — Write туда мис-триггерит `enforce-tdd-real-test-verifier`). В тексте коммита **не использовать символ `>`** (PowerShell-гейт примет за redirect).
- **commit-not-push** — push только по слову «пуш».
- **Порядок** — простое-первым (P3 → P2 → P1 → P4 → P5 → P6). Каждый код-таск: RED → GREEN → регрессия → атомарный коммит.

## File Structure

| Файл | Ответственность | Таск |
|---|---|---|
| `tools/observer-transcript-parser.mjs` | observer task-классификатор (эвристика) — убрать `commit` из release | T1 (P3) |
| `tools/observer-transcript-parser.test.mjs` | +тест release-класса | T1 |
| `tools/observer-pii-filter.mjs` | PII sanitize+count — единый конвейер (count==редакции) | T2 (P2) |
| `tools/observer-pii-filter.test.mjs` | +тесты overlap/distinct | T2 |
| `tools/observer-routing-detector.mjs` | +`stripPastedContext` (узкий: fenced+blockquote) перед детекцией | T3 (P1) |
| `tools/observer-routing-detector.test.mjs` | +тесты PASTED-guard | T3 |
| `tools/status-md-generator.mjs` | деталь-доска + `escapeCell` (anti-injection) | T4 (P4) |
| `tools/status-md-generator.test.mjs` | +тесты деталь/escape | T4 |
| `tools/node-graph.mjs` | docstring-уточнение | T5 (P5) |
| `tools/node-graph.test.mjs` | +пиннинг plain-литерал | T5 |
| `tools/brain-retro-opus-reviewer.mjs` | docstring-уточнение caching no-op | T5 |
| `tools/brain-retro-opus-reviewer.test.mjs` | +пиннинг непустой system | T5 |
| `docs/superpowers/specs/2026-06-08-router-mentor-machine-7-design.md` | DOC-1 callout | T6 (P6) |
| `docs/superpowers/plans/2026-06-08-router-mentor-loose-ends-registry.md` | пометить E/G ✅ | T7 |

---

### Task 1 (P3): release-class — убрать голый `commit`

**Files:**
- Modify: `tools/observer-transcript-parser.mjs:215`
- Test: `tools/observer-transcript-parser.test.mjs` (рядом с 947-951)

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

Добавить в `describe`-блок release (после строки 951):

```js
  it('does NOT classify a plain commit (no push/release) as release', () => {
    expect(classifyTask('commit the auth changes')).not.toBe('release');
  });
```

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

Run: `npx vitest run --root "<wt>/app" --config "<wt>/app/vitest.config.tools.mjs" observer-transcript-parser --reporter dot`
Expected: FAIL — `commit the auth changes` сейчас матчит `\bcommit\b` → `'release'`, ассерт `.not.toBe('release')` красный.

- [ ] **Step 3: Минимальная правка**

В `tools/observer-transcript-parser.mjs:215` убрать `\bcommit\b|`:

```js
  if (/\bpush\b|\bmerge\b|\bdeploy\b|\brelease\b|релиз|тегни/.test(t)) return 'release';
```

- [ ] **Step 4: Прогнать — зелёно**

Run: тот же фильтр.
Expected: PASS (новый тест + 947-951 положительные `push/merge/release` остаются зелёными).

- [ ] **Step 5: Регрессия + коммит**

Run: `npx vitest run --root "<wt>/app" --config "<wt>/app/vitest.config.tools.mjs" --reporter dot` → не ниже 3174 passed, 0 failed.
```
git -C "<wt>" add tools/observer-transcript-parser.mjs tools/observer-transcript-parser.test.mjs
git -C "<wt>" commit -m "fix(observer): release-класс не ловит голый commit (P3 E/G)" -m "classifyTask: убран \bcommit\b из release-регекса (коммит != релиз). push/merge/deploy/release/релиз/тегни остаются. Аналитика-only, гейтов-потребителей нет."
```

---

### Task 2 (P2): PII count — единый конвейер (count == редакции)

**Files:**
- Modify: `tools/observer-pii-filter.mjs:41-94` (sanitizeString / countString / sanitizeWithCount + новый PIPELINE)
- Test: `tools/observer-pii-filter.test.mjs` (в `describe('sanitizeWithCount …')`, после 151)

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

Добавить в `describe('sanitizeWithCount (Task 3)', …)`:

```js
  it('counts an overlapping Bearer+OpenAI span once (not twice)', () => {
    const { counts } = sanitizeWithCount('auth: Bearer sk-AAAAAAAAAAAAAAAAAAAA done');
    expect(counts.OPENAI_TOKEN + counts.GENERIC_BEARER).toBe(1);
  });
  it('counts two distinct adjacent leaks separately', () => {
    const { counts } = sanitizeWithCount('a@b.co +71234567890');
    expect(counts.EMAIL).toBe(1);
    expect(counts.RU_PHONE).toBe(1);
  });
```

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

Run: `npx vitest run --root "<wt>/app" --config "<wt>/app/vitest.config.tools.mjs" observer-pii-filter --reporter dot`
Expected: FAIL — сейчас `countString` гоняет OPENAI_TOKEN и GENERIC_BEARER независимо по сырой строке → оба матчат → сумма 2, ассерт ждёт 1.

- [ ] **Step 3: Рефактор на единый конвейер**

В `tools/observer-pii-filter.mjs` заменить блок от `function sanitizeString(s) {` (строка 41) до конца `function countString(...)` (строка 73) — на единый `PIPELINE` + переписанные функции. Регекс-консты (27-39) НЕ трогать. Удалить `const PATTERNS = {...}` (61-64). Итог:

```js
// Единый конвейер замен (порядок = безопасность: специфичные первыми).
// sanitize и count идут по ОДНОМУ конвейеру → count == число реальных редакций
// (под-счёт утечки невозможен; sharp-edges SE-2).
const PIPELINE = [
  { name: 'RU_PHONE', re: RU_PHONE, repl: () => '+7XXXXXXXXXX' },
  { name: 'EMAIL', re: EMAIL, repl: () => '***@***' },
  { name: 'JWT', re: JWT, repl: () => '[REDACTED:jwt]' },
  { name: 'AWS_KEY', re: AWS_KEY, repl: () => '[REDACTED:aws]' },
  { name: 'YC_STATIC', re: YC_STATIC, repl: () => '[REDACTED:yandex]' },
  { name: 'YC_SESSION', re: YC_SESSION, repl: () => '[REDACTED:yandex]' },
  { name: 'YC_OAUTH', re: YC_OAUTH, repl: () => '[REDACTED:yandex]' },
  { name: 'SENTRY_TOKEN', re: SENTRY_TOKEN, repl: () => '[REDACTED:sentry]' },
  { name: 'OPENAI_TOKEN', re: OPENAI_TOKEN, repl: () => '[REDACTED:openai]' },
  { name: 'GENERIC_BEARER', re: GENERIC_BEARER, repl: () => '[REDACTED:bearer]' },
  { name: 'IPV4', re: IPV4, repl: () => '[REDACTED:ipv4]' },
  { name: 'WIN_USER_PATH', re: WIN_USER_PATH, repl: (_m, p1) => p1 + '***' },
  { name: 'POSIX_HOME', re: POSIX_HOME, repl: (_m, p1) => p1 + '***' },
];

function sanitizeString(s) {
  if (typeof s !== 'string') return s;
  let out = s;
  for (const { re, repl } of PIPELINE) out = out.replace(re, repl);
  return out;
}

function countString(s, counts) {
  if (typeof s !== 'string') return;
  let cur = s;
  for (const { name, re, repl } of PIPELINE) {
    cur = cur.replace(re, (...args) => {
      counts[name] = (counts[name] || 0) + 1;
      return repl(...args);
    });
  }
}
```

И в `sanitizeWithCount` (строка 89-94) заменить инициализацию ключей:

```js
export function sanitizeWithCount(input) {
  const counts = {};
  for (const { name } of PIPELINE) counts[name] = 0;
  walkAndCount(input, counts);
  return { sanitized: sanitize(input), counts };
}
```

- [ ] **Step 4: Прогнать — зелёно**

Run: `npx vitest run --root "<wt>/app" --config "<wt>/app/vitest.config.tools.mjs" observer-pii-filter --reporter dot`
Expected: PASS — overlap=1, distinct=2; существующие тесты (counts.RU_PHONE/EMAIL, 13 ключей, recursion, sanitize byte-identical 108-110) зелёные. (sanitizeString даёт побайтно тот же вывод — те же регексы/замены/порядок.)

- [ ] **Step 5: Регрессия + коммит**

Run: полная регрессия — не ниже 3174 passed, 0 failed.
```
git -C "<wt>" add tools/observer-pii-filter.mjs tools/observer-pii-filter.test.mjs
git -C "<wt>" commit -m "fix(observer): PII count по конвейеру sanitize, overlap считается один раз (P2 E/G)" -m "Единый PIPELINE для sanitizeString+countString — count == число реальных редакций (sharp-edges SE-2). sanitize byte-identical. Bearer+OpenAI overlap: 1, не 2."
```

---

### Task 3 (P1): routing-detector — узкий `stripPastedContext`

**Files:**
- Modify: `tools/observer-routing-detector.mjs` (новый export `stripPastedContext` + применить в `detectMethodDirected:42`)
- Test: `tools/observer-routing-detector.test.mjs` (import + новый describe)

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

В `tools/observer-routing-detector.test.mjs` заменить строку 2 импорта на:

```js
import { detectMethodDirected, loadKnownNodes, stripPastedContext } from './observer-routing-detector.mjs';
```

Добавить в конец файла (после `describe('loadKnownNodes' …)`):

```js
describe('stripPastedContext (узкий: fenced + blockquote)', () => {
  it('removes fenced blocks', () => {
    expect(stripPastedContext('a\n```\n/x here\n```\nb')).not.toContain('/x');
  });
  it('removes blockquote lines', () => {
    expect(stripPastedContext('> /x cited\nkeep')).not.toContain('/x');
  });
  it('keeps inline backticks (FP-safe bias)', () => {
    expect(stripPastedContext('use `/x` now')).toContain('/x');
  });
  it('is non-string safe', () => {
    expect(stripPastedContext(null)).toBe('');
  });
});

describe('detectMethodDirected — PASTED-context guard (4-й FP-класс)', () => {
  it('does NOT flag a /node inside a fenced code block', () => {
    const t = 'смотри лог:\n```\nуже использовал /brainstorming тут\n```\nчто думаешь?';
    expect(detectMethodDirected(t, NODES).directed).toBe(false);
  });
  it('does NOT flag a blockquote-cited directive', () => {
    const t = '> используй systematic-debugging здесь\nэто цитата из старого чата';
    expect(detectMethodDirected(t, NODES).directed).toBe(false);
  });
  it('STILL flags an inline /slash directive (FP-safe bias preserved)', () => {
    expect(detectMethodDirected('сделай через `/brainstorming`', NODES).directed).toBe(true);
  });
  it('STILL flags a plain-prose directive', () => {
    expect(detectMethodDirected('запусти discovery-interview', NODES).directed).toBe(true);
  });
});
```

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

Run: `npx vitest run --root "<wt>/app" --config "<wt>/app/vitest.config.tools.mjs" observer-routing-detector --reporter dot`
Expected: FAIL — `stripPastedContext` не экспортирован; fenced/blockquote-тесты красные (сейчас `/brainstorming` в блоке ловится).

- [ ] **Step 3: Реализация**

В `tools/observer-routing-detector.mjs` добавить экспорт перед `detectMethodDirected` (после `loadKnownNodes`, ~строка 37):

```js
/**
 * Снять ТОЛЬКО PASTED-контекст: fenced-блоки и markdown-blockquote-строки.
 * НАМЕРЕННО НЕ трогает inline-backticks/кавычки/guillemets — в отличие от
 * stripQuotedContext (tools/enforce-rationalization-audit.mjs), у которого
 * ПРОТИВОПОЛОЖНОЕ безопасное направление (FN-safe). Здесь гейт routingGate
 * FP-safe: inline-форма `/x` должна оставаться директивой (over-detection
 * дешевле тихой потери governance-сигнала; sharp-edges SE-1).
 */
export function stripPastedContext(text) {
  if (typeof text !== 'string') return '';
  let s = text;
  s = s.replace(/```[\s\S]*?```/g, ' ');            // fenced blocks
  s = s.replace(/^[ \t]*(?:>|&gt;).*$/gm, ' ');      // markdown blockquote lines
  return s;
}
```

И в `detectMethodDirected` (строка 42) поменять первую строку:

```js
export function detectMethodDirected(promptText, knownNodes) {
  const text = stripPastedContext(String(promptText || '')).toLowerCase();
```

- [ ] **Step 4: Прогнать — зелёно**

Run: тот же фильтр.
Expected: PASS — fenced/blockquote → directed:false; inline + проза → directed:true; существующие 6 тестов `detectMethodDirected` зелёные.

- [ ] **Step 5: Регрессия + коммит**

Run: полная регрессия — не ниже 3174 passed, 0 failed (особенно `observer-stop-hook` — routingGateDecision потребитель).
```
git -C "<wt>" add tools/observer-routing-detector.mjs tools/observer-routing-detector.test.mjs
git -C "<wt>" commit -m "fix(observer): routing-detector игнорит PASTED-цитаты /node (P1 E/G, 4-й FP-класс)" -m "Новый узкий stripPastedContext (fenced+blockquote only) перед детекцией. НЕ reuse stripQuotedContext (тот стрипает inline -> FN). Сохранено FP-смещение routingGate (sharp-edges SE-1)."
```

---

### Task 4 (P4): деталь-доска + `escapeCell` (anti-injection)

**Files:**
- Modify: `tools/status-md-generator.mjs` (новый `escapeCell` + деталь-рендер в `computeGuardBoardBlock:42-67`)
- Test: `tools/status-md-generator.test.mjs` (в `describe('computeGuardBoardBlock …')`, после 540)

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

Добавить в `describe('computeGuardBoardBlock (М7 Фаза 6 §7 …)')` (после теста 538-540):

```js
  it('рендерит деталь-таблицу при непустых событиях с экранированием', () => {
    const md = computeGuardBoardBlock({
      manifest: { registered: [], missing: [], ok: true },
      judgeMode: 'inert',
      recentEscapes: [{ ts: '2026-06-08T10:00Z', action: 'push', reason: 'a | b\nc' }],
      recentBlocks: [],
    });
    expect(md).toMatch(/Недавние escape владельца \(детали\)/);
    expect(md).toContain('a \\| b c');
  });
  it('пустые массивы → нет деталь-таблицы (byte-identical-путь)', () => {
    const md = computeGuardBoardBlock({
      manifest: { registered: [], missing: [], ok: true },
      judgeMode: 'inert', recentEscapes: [], recentBlocks: [],
    });
    expect(md).not.toMatch(/\(детали\)/);
  });
```

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

Run: `npx vitest run --root "<wt>/app" --config "<wt>/app/vitest.config.tools.mjs" status-md-generator --reporter dot`
Expected: FAIL — `escapeCell`/деталь-рендера нет, «(детали)» не появляется.

- [ ] **Step 3: Реализация**

В `tools/status-md-generator.mjs` добавить helper перед `computeGuardBoardBlock` (после `GUARD_LABELS`, ~строка 35):

```js
/** Экранировать значение для одной ячейки markdown-таблицы (anti-injection, sharp-edges SE-3). */
function escapeCell(v) {
  return String(v ?? '').replace(/\r?\n/g, ' ').replace(/\|/g, '\\|').slice(0, 120);
}
```

В `computeGuardBoardBlock` перед `return` (после строк `const esc = …; const blk = …;`, ~строка 54) добавить:

```js
  const detailTable = (events, title) => {
    if (!Array.isArray(events) || events.length === 0) return '';
    const rows = events
      .map((e) => `| ${escapeCell(e && e.ts)} | ${escapeCell(e && e.action)} | ${escapeCell(e && e.reason)} |`)
      .join('\n');
    return `\n**${title}:**\n\n| Время | Действие | Причина |\n|---|---|---|\n${rows}\n`;
  };
  const detail =
    detailTable(recentEscapes, 'Недавние escape владельца (детали)') +
    detailTable(recentBlocks, 'Недавние блоки (детали)');
```

И в шаблоне `return` поменять последнюю содержательную строку — было:

```js
Недавние escape владельца: ${esc} · Недавние блоки: ${blk}
`;
```

стало (добавить `${detail}` после строки-счётчика — при пустых массивах `detail===''` → байт-в-байт прежний вывод):

```js
Недавние escape владельца: ${esc} · Недавние блоки: ${blk}
${detail}`;
```

- [ ] **Step 4: Прогнать — зелёно**

Run: тот же фильтр.
Expected: PASS — деталь+escape рендерятся; `[]`-тест без «(детали)»; существующие тесты 511-540 (включая `[{},{}]`/`[{}]` счётчики и `computeGuardBoardBlock({})` not.toThrow) зелёные.

- [ ] **Step 5: Регрессия + коммит**

Run: полная регрессия — не ниже 3174 passed, 0 failed.
```
git -C "<wt>" add tools/status-md-generator.mjs tools/status-md-generator.test.mjs
git -C "<wt>" commit -m "feat(status): деталь-рендер доски обороны + escapeCell anti-injection (P4 E/G)" -m "computeGuardBoardBlock рендерит таблицу недавних escape/блоков при непустых массивах; поля экранируются (| и newline, cap 120) — sharp-edges SE-3. []-путь byte-identical. Live-источник остаётся [] (граница B-блока)."
```

---

### Task 5 (P5): косметика docstring + пиннинг-тесты

> **TDD-gate note:** правки T5 — comment-only, поведения не меняют → честного «наблюдаемого RED» нет. Пиннинг-тесты пинят документируемый инвариант (expect+it, ссылаются на правимый prod-файл) — этого достаточно для `enforce-tdd-real-test-verifier`. Тесты ЗЕЛЁНЫЕ сразу (не фабрикуем фальш-RED). Если `enforce-tdd-gate` всё же жёстко блокирует comment-only edit — зафиксировать в логе и закрыть T5 как deferred (косметика, низкая ценность), не обходить обманом.

**Files:**
- Modify: `tools/node-graph.mjs:7` (docstring), `tools/brain-retro-opus-reviewer.mjs` (docstring ~31-43)
- Test: `tools/node-graph.test.mjs`, `tools/brain-retro-opus-reviewer.test.mjs`

- [ ] **Step 1: Добавить пиннинг-тест node-graph**

В `tools/node-graph.test.mjs`, в `describe('buildNodeGraph' …)` (после теста 25):

```js
  it('accepts a plain registry literal (not loadRegistry-bound)', () => {
    const g = buildNodeGraph({ nodes: [{ id: '#1', slug: 'x', name: 'X' }], chains: {} });
    expect(g.bySlug.get('x').id).toBe('#1');
  });
```

- [ ] **Step 2: Прогнать — фиксируем GREEN (поведение уже есть)**

Run: `npx vitest run --root "<wt>/app" --config "<wt>/app/vitest.config.tools.mjs" node-graph --reporter dot`
Expected: PASS (документирует существующий контракт; RED тут невозможен — это comment-фикс).

- [ ] **Step 3: Уточнить docstring node-graph**

`tools/node-graph.mjs:7` — заменить «Потребляет registry от loadRegistry.» на:

```js
 * Потребляет registry-объект {nodes, chains} — обычно от loadRegistry, но
 * принимает любой совместимый литерал (развязка для тестов/инвариантов).
```

- [ ] **Step 4: Добавить пиннинг-тест reviewer**

В `tools/brain-retro-opus-reviewer.test.mjs` (импорт `buildReviewPromptStructured` — добавить в существующий import-блок, если ещё нет), добавить:

```js
  it('structured prompt returns a non-empty system block (caching target)', () => {
    const { system, user } = buildReviewPromptStructured({ schema_version: 2 });
    expect(typeof system).toBe('string');
    expect(system.length).toBeGreaterThan(0);
    expect(typeof user).toBe('string');
  });
```

- [ ] **Step 5: Прогнать reviewer — GREEN + уточнить docstring**

Run: `npx vitest run --root "<wt>/app" --config "<wt>/app/vitest.config.tools.mjs" brain-retro-opus-reviewer --reporter dot` → PASS.
Затем в `tools/brain-retro-opus-reviewer.mjs` docstring `buildReviewPromptStructured` (~31-43) добавить строку:

```js
 * NB: на Opus (<4096-токенный кешируемый префикс) cache_control — намеренный
 * no-op (cache_creation_input_tokens=0, без ошибки); активируется при свиче
 * на Sonnet ИЛИ росте system-блока выше порога.
```

- [ ] **Step 6: Регрессия + коммит**

Run: полная регрессия — не ниже 3174 passed, 0 failed.
```
git -C "<wt>" add tools/node-graph.mjs tools/node-graph.test.mjs tools/brain-retro-opus-reviewer.mjs tools/brain-retro-opus-reviewer.test.mjs
git -C "<wt>" commit -m "docs(tools): уточнить docstring node-graph (plain registry) + reviewer caching no-op (P5 E/G)" -m "Пиннинг-инварианты: buildNodeGraph принимает plain {nodes,chains}; structured-prompt отдаёт непустой system. Comment-only, поведение не меняется (sharp-edges SE-4 — не трюизм)."
```

---

### Task 6 (P6): DOC-1 escape-awareness callout

**Files:**
- Modify: `docs/superpowers/specs/2026-06-08-router-mentor-machine-7-design.md` (рядом с §10 правило 7)

- [ ] **Step 1: Найти якорь §10 правило 7**

Run: `Grep` по `правило 7|§10|escape-awareness|растворит` в `docs/superpowers/specs/2026-06-08-router-mentor-machine-7-design.md` — найти точное место §10 (правило 7 о растворении/научении зоопарка).

- [ ] **Step 2: Вставить callout**

Добавить рядом с §10 правило 7:

```markdown
> **⚠️ Перед активацией (DOC-1):** законный escape владельца (М6) чтут ТОЛЬКО
> стена М2 / пол М5 / egress-страж. Параллельные стражи зоопарка
> (`enforce-router-gate`, `enforce-runtime-write-deny`, судья М4) про escape НЕ
> знают → реальный `force-push` / запись в `.env` НЕ пройдут, пока зоопарк
> зарегистрирован. Полностью «дверь» открывается лишь когда Фаза 8 растворит/
> научит зоопарк (это правило 7). До Ф8 escape — частичный по конструкции.
```

- [ ] **Step 3: Коммит (doc-only)**

```
git -C "<wt>" add docs/superpowers/specs/2026-06-08-router-mentor-machine-7-design.md
git -C "<wt>" commit -m "docs(router-mentor): DOC-1 escape-awareness callout перед активацией М6 (P6 E/G)"
```

---

### Task 7: закрыть E/G в реестре хвостов

**Files:**
- Modify: `docs/superpowers/plans/2026-06-08-router-mentor-loose-ends-registry.md`

- [ ] **Step 1: Пометить закрытое**

В блоке E пометить ✅: DOC-1 (T6), M3 node-graph docstring + reviewer no-op (T5), деталь-доска (T4). В блоке G пометить ✅: 4-й FP-класс (T3), `\bcommit\b` release-нить (T1), двойной счёт PII (T2). Оставить открытым: доска live-источник (B-блок), Фаза 2 observer, embedding-index, `observer-self-assessment-api.test.mjs:258` pre-existing.

- [ ] **Step 2: Коммит (doc-only)**

```
git -C "<wt>" add docs/superpowers/plans/2026-06-08-router-mentor-loose-ends-registry.md
git -C "<wt>" commit -m "docs(router-mentor): E/G warm-up batch закрыт в реестре хвостов (P1-P6)"
```

---

## Гейт закрытия батча

- [ ] Полная регрессия tools-only — не ниже 3174 passed, 0 failed; зафиксировать число.
- [ ] `superpowers:verification-before-completion` — сверить заявленное с реальным выводом тестов.
- [ ] Сводка владельцу: 6 пунктов, N коммитов `62d3352d..<HEAD>` (commit-not-push), регрессия, что осталось открытым (B-блок live-источник доски).
- [ ] НЕ `requesting-code-review` (запрещён хэндоффом). При желании — `sharp-edges`/`variant-analysis` уже отработаны на спеке.
- [ ] Push — только по слову «пуш» (через `finishing-a-development-branch`).
