# Блок B (вшивка) Классы 1+2 — Implementation Plan

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

**Goal:** Доставить 4 observation-only сигнала Блока B в read-only STATUS-генератор +
инертный reconcile-reader, не трогая гейт-решения и hard-wall mains машин М1/М2/М4/М5/М6.

**Architecture:** Реюз паттерна `guardBoardBlock` — чистые `compute*Block(...)` (markdown из
данных, ноль I/O) + fail-quiet wiring в `status-md-generator.main()` (try/catch→null, блок
вставляется в `renderStatus` через `inputs.*Block`). Пункт 4 (reconcile) — чистый
`reconcileEvent` + тонкая I/O-обёртка `enforce-reconcile.main`, **инертна** до регистрации
хука владельцем (owner-шаг A3). Все примитивы готовы: `loadQueue`/`statusSignal` (R-09),
`auditDoors`/`auditExempt` (R-24), `verifyChain`/`loadJournal` (R-30), `reconcileAction` (п.4).

**Tech Stack:** Node ESM (`tools/*.mjs`), vitest (tools-only config), TDD RED→GREEN инлайн.

**Границы (жёсткое):** п.1-3 — только `tools/status-md-generator.mjs` (вывод, не гейт) +
read-only примитивы; п.4 — только `tools/enforce-reconcile.mjs` (инертен). Единственная правка
в hard-wall-файле — **аддитивный `export` константы `SEED_TOOLS`** в
`tools/enforce-supreme-gate.mjs` (Задача 2): экспорт уже-существующей `const` для read-only
аудита R-24, гейт-решения **байт-в-байт неизменны** (пиннинг-тест инвариантности). Это НЕ
ослабление стены М2. Стена/пол/судья/escape логически не трогаются.

**Регрессия-цель:** tools-only ≥ **3189 GREEN** (baseline после E/G warm-up).

**Quirks сессии (из handoff 2026-06-09):**
- vitest: `npx vitest run --root "<wt>/app" --config "<wt>/app/vitest.config.tools.mjs" <фильтр> --reporter dot` — БЕЗ `cd app`, явный `--root`. После 1-го Bash cwd залипает в `app/`.
- git — только PowerShell, `git -C "<wt>"`, литеральные пути.
- PowerShell-гейт режет тело коммита (`>`/`>=` = redirect, редкие латинские слова = cmdlet) → короткие single `-m` ASCII-сообщения; стейдж отдельным `git add`.
- `enforce-tdd-real-test-verifier`: каждая правка `*.test.mjs` через Edit обязана сама содержать `expect`+`it` со ссылкой на prod-файл → новый импорт + тесты ОДНОЙ правкой.
- Список файлов — Grep (Glob ломается на junction worktree).
- commit-not-push: пуш ТОЛЬКО по слову «пуш» через finishing-a-development-branch.

---

## File Structure

| Файл | Ответственность | Действие |
|---|---|---|
| `tools/router-learning-queue.mjs` | R-09 примитивы очереди (`loadQueue`/`statusSignal`/`pendingCount`) | реюз (НЕ трогаем) |
| `tools/door-coverage.mjs` | R-24 примитивы (`auditDoors`/`auditExempt`) + новые `extractGateMatcher`/`CANONICAL_MUTATING_TOOLS`/`isMutatingTool` | расширяем |
| `tools/action-journal.mjs` | R-30 примитивы (`verifyChain`/`loadJournal`) | реюз (НЕ трогаем) |
| `tools/receipt-key-config.mjs` | `resolveReceiptKey()` для R-30 | реюз (НЕ трогаем) |
| `tools/enforce-supreme-gate.mjs` | M2 стена; `SEED_SKILLS` (экспортирован), `actionOf` (экспортирован), `SEED_TOOLS` | **+`export` у `SEED_TOOLS`** (аддитивно) |
| `tools/status-md-generator.mjs` | 3 новых `compute*Block` (R-09/R-24/R-30) + fail-quiet wiring + renderStatus | расширяем |
| `tools/enforce-reconcile.mjs` | `reconcileAction`/`findOrphanIntents` (готовы) + новый `reconcileEvent` + main-reader | расширяем |
| `tools/status-md-generator.test.mjs` | тесты 3 новых блоков + renderStatus wiring | расширяем |
| `tools/door-coverage.test.mjs` | тесты `extractGateMatcher`/`CANONICAL_MUTATING_TOOLS`/`isMutatingTool` | расширяем |
| `tools/enforce-supreme-gate.test.mjs` (или существующий) | пиннинг-инвариантность `SEED_TOOLS` export | расширяем |
| `tools/enforce-reconcile.test.mjs` | тесты `reconcileEvent` | расширяем |

---

## Task 1: R-09 — блок «Очередь обучения роутера» в STATUS

**Files:**
- Modify: `tools/status-md-generator.mjs` (импорт из router-learning-queue + новый `computeLearningQueueBlock` + wiring в main + renderStatus template)
- Test: `tools/status-md-generator.test.mjs`

- [ ] **Step 1: Написать падающий тест (pure + renderStatus wiring)**

Добавить в `tools/status-md-generator.test.mjs` (новый импорт `computeLearningQueueBlock` + тесты ОДНОЙ правкой — verifier требует expect+it):

```js
import { computeLearningQueueBlock } from './status-md-generator.mjs';

describe('computeLearningQueueBlock (R-09 очередь обучения)', () => {
  it('пустая очередь → «очередь пуста»', () => {
    const md = computeLearningQueueBlock({ queue: [] });
    expect(md).toContain('## Очередь обучения роутера');
    expect(md).toContain('Очередь пуста');
  });
  it('нет аргумента (undefined) → не падает, очередь пуста', () => {
    const md = computeLearningQueueBlock({});
    expect(md).toContain('Очередь пуста');
  });
  it('есть pending → показывает «ждут одобрения: N» и список', () => {
    const queue = [
      { id: 'c1', kind: 'example', summary: 'кейс A', why_proposed: 'p', status: 'pending' },
      { id: 'c2', kind: 'example', summary: 'кейс B', why_proposed: 'p', status: 'approved' },
    ];
    const md = computeLearningQueueBlock({ queue });
    expect(md).toContain('ждут одобрения: 1');
    expect(md).toContain('c1');
    expect(md).not.toContain('кейс B'); // approved не показываем
  });
  it('экранирует pipe в summary (anti-injection)', () => {
    const queue = [{ id: 'x', kind: 'k', summary: 'a | b', why_proposed: '', status: 'pending' }];
    const md = computeLearningQueueBlock({ queue });
    expect(md).toContain('a \\| b');
  });
});

describe('renderStatus — learningQueueBlock (R-09)', () => {
  const base = {
    now: '2026-06-09T10:00:00Z',
    c1: { status: 'ok', detail: 'OK' }, c2: { status: 'ok', detail: 'OK' },
    c3: { status: 'ok', detail: 'OK' }, c5: { status: 'ok', detail: 'OK' },
    observer: { episodeCount: 0, observerErrors: 0, piiMatches: 0 },
  };
  it('вставляет блок строкой при наличии', () => {
    const md = renderStatus({ ...base, learningQueueBlock: '## Очередь обучения роутера\n\nОчередь пуста.' });
    expect(md).toContain('## Очередь обучения роутера');
  });
  it('опускает при отсутствии (backward compat)', () => {
    const md = renderStatus(base);
    expect(md).not.toContain('## Очередь обучения роутера');
  });
});
```

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

Run: `npx vitest run --root "c:/моя/проекты/портал crm/Документация/.claude/worktrees/brainrepo/app" --config "c:/моя/проекты/портал crm/Документация/.claude/worktrees/brainrepo/app/vitest.config.tools.mjs" status-md-generator --reporter dot`
Expected: FAIL — `computeLearningQueueBlock is not a function` / `is not exported`.

- [ ] **Step 3: Реализация — импорт + функция + wiring + template**

В `tools/status-md-generator.mjs`:

(a) После существующих импортов (после строки 13 `import { judgeGateMode }...`) добавить:

```js
import { loadQueue, statusSignal, pendingCount } from './router-learning-queue.mjs';
```

(b) После `computeGuardBoardBlock` (после строки 82) добавить функцию (использует существующий `escapeCell`):

```js
/**
 * R-09 (Блок B Класс 1) — read-only блок очереди обучения роутера в STATUS.
 * Реюз `statusSignal`/`pendingCount` из router-learning-queue. Наполнение фонда —
 * ТОЛЬКО по явному «да» владельца (hard-rule модуля); здесь только показываем pending.
 */
export function computeLearningQueueBlock({ queue } = {}) {
  const q = Array.isArray(queue) ? queue : [];
  if (pendingCount(q) === 0) {
    return `## Очередь обучения роутера\n\nОчередь пуста — нет кандидатов на одобрение.`;
  }
  const lines = q.filter((e) => e && e.status === 'pending')
    .map((e) => `- [${escapeCell(e.id)}] (${escapeCell(e.kind)}) ${escapeCell(e.summary)}`)
    .join('\n');
  return `## Очередь обучения роутера\n\n${statusSignal(q)} — наполнение фонда ТОЛЬКО по явному «да» владельца (см. \`/brain-retro\`).\n\n${lines}`;
}
```

(c) В `renderStatus` template (строка 439, длинная цепочка `${inputs.X ? ... : ''}`) — добавить в конец цепочки, перед `\n## Алерт-индикаторы`:

```js
${inputs.learningQueueBlock ? `\n${inputs.learningQueueBlock.trim()}\n` : ''}
```

(d) В main-блоке (`if (process.argv[1]...`), рядом с другими fail-quiet блоками (после строки 591 `inputs.systemHealthBlock = systemHealthBlock;`) добавить:

```js
let learningQueueBlock = null;
try {
  const qPath = join(homedir(), '.claude', 'runtime', 'router-learning-queue.json');
  const queue = existsSync(qPath) ? loadQueue({ path: qPath }) : [];
  learningQueueBlock = computeLearningQueueBlock({ queue });
} catch (err) { console.warn('[status-md-generator] learningQueueBlock skipped:', err.message); learningQueueBlock = null; }
inputs.learningQueueBlock = learningQueueBlock;
```

- [ ] **Step 4: Запустить тест — GREEN**

Run: `npx vitest run --root "c:/моя/проекты/портал crm/Документация/.claude/worktrees/brainrepo/app" --config "c:/моя/проекты/портал crm/Документация/.claude/worktrees/brainrepo/app/vitest.config.tools.mjs" status-md-generator --reporter dot`
Expected: PASS (все блоки status-md-generator зелёные).

- [ ] **Step 5: Commit**

```
git -C "c:\моя\проекты\портал crm\Документация\.claude\worktrees\brainrepo" add tools/status-md-generator.mjs tools/status-md-generator.test.mjs
git -C "c:\моя\проекты\портал crm\Документация\.claude\worktrees\brainrepo" commit -m "feat(status): R-09 learning queue block (read-only)"
```

---

## Task 2: R-24 (часть 1) — door-coverage helpers + export SEED_TOOLS

**Files:**
- Modify: `tools/door-coverage.mjs` (+`CANONICAL_MUTATING_TOOLS`, `isMutatingTool`, `extractGateMatcher`)
- Modify: `tools/enforce-supreme-gate.mjs` (строка 25 — `const SEED_TOOLS` → `export const SEED_TOOLS`, аддитивно)
- Test: `tools/door-coverage.test.mjs`

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

Добавить в `tools/door-coverage.test.mjs` (новый импорт + тесты ОДНОЙ правкой):

```js
import { CANONICAL_MUTATING_TOOLS, isMutatingTool, extractGateMatcher } from './door-coverage.mjs';
import { SEED_TOOLS as GATE_SEED_TOOLS } from './enforce-supreme-gate.mjs';

describe('CANONICAL_MUTATING_TOOLS / isMutatingTool (R-24)', () => {
  it('канонический список включает ключевые мутирующие инструменты', () => {
    for (const t of ['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'Bash', 'Task', 'Skill']) {
      expect(CANONICAL_MUTATING_TOOLS).toContain(t);
    }
  });
  it('isMutatingTool: мутирующий → true, observe-only → false', () => {
    expect(isMutatingTool('Write')).toBe(true);
    expect(isMutatingTool('Read')).toBe(false);
    expect(isMutatingTool('EnterPlanMode')).toBe(false);
  });
});

describe('extractGateMatcher (R-24)', () => {
  it('хук не найден → []', () => {
    expect(extractGateMatcher({}, 'enforce-supreme-gate.mjs')).toEqual([]);
    expect(extractGateMatcher({ hooks: { PreToolUse: [] } }, 'enforce-supreme-gate.mjs')).toEqual([]);
  });
  it('matcher "*" → ["*"]', () => {
    const settings = { hooks: { PreToolUse: [{ matcher: '*', hooks: [{ type: 'command', command: 'node tools/enforce-supreme-gate.mjs' }] }] } };
    expect(extractGateMatcher(settings, 'enforce-supreme-gate.mjs')).toEqual(['*']);
  });
  it('matcher "Edit|Write|Bash" → ["Edit","Write","Bash"]', () => {
    const settings = { hooks: { PreToolUse: [{ matcher: 'Edit|Write|Bash', hooks: [{ command: 'node tools/enforce-supreme-gate.mjs' }] }] } };
    expect(extractGateMatcher(settings, 'enforce-supreme-gate.mjs')).toEqual(['Edit', 'Write', 'Bash']);
  });
  it('пустой matcher "" → [] (аудит требует явного *)', () => {
    const settings = { hooks: { PreToolUse: [{ matcher: '', hooks: [{ command: 'node tools/enforce-supreme-gate.mjs' }] }] } };
    expect(extractGateMatcher(settings, 'enforce-supreme-gate.mjs')).toEqual([]);
  });
});

describe('SEED_TOOLS export (R-24 anti-drift, инвариантность)', () => {
  it('экспортирован и содержит seed-инструменты стены', () => {
    expect(GATE_SEED_TOOLS.has('EnterPlanMode')).toBe(true);
    expect(GATE_SEED_TOOLS.has('AskUserQuestion')).toBe(true);
  });
  it('seed-инструменты НЕ числятся мутирующими (иначе auditExempt их флагует)', () => {
    for (const t of GATE_SEED_TOOLS) expect(isMutatingTool(t)).toBe(false);
  });
});
```

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

Run: `npx vitest run --root "c:/моя/проекты/портал crm/Документация/.claude/worktrees/brainrepo/app" --config "c:/моя/проекты/портал crm/Документация/.claude/worktrees/brainrepo/app/vitest.config.tools.mjs" door-coverage --reporter dot`
Expected: FAIL — `CANONICAL_MUTATING_TOOLS` / `extractGateMatcher` не экспортированы; `SEED_TOOLS` не экспортирован из enforce-supreme-gate (import undefined).

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

(a) В `tools/enforce-supreme-gate.mjs` строка 25 — добавить `export` (аддитивно, логика стены неизменна):

```js
export const SEED_TOOLS = new Set(['EnterPlanMode', 'AskUserQuestion']);
```

(b) В `tools/door-coverage.mjs` (после существующих `auditDoors`/`auditExempt`) добавить:

```js
/** Канонический набор мутирующих инструментов (по способности, выровнен с supreme-gate
 *  default-deny: всё, что не observe-only и не seed). MCP-писатели динамические — не статичны. */
export const CANONICAL_MUTATING_TOOLS = ['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'Bash', 'Task', 'Skill'];

export function isMutatingTool(name) {
  return CANONICAL_MUTATING_TOOLS.includes(name);
}

/**
 * Извлечь, какие инструменты покрывает matcher хука <basename> в settings.PreToolUse.
 * '*' → ['*'] (покрывает всё); "A|B" → ['A','B']; хук не найден / пустой matcher → []
 * (пустой = аудит требует явного '*', иначе сигналим «дверь не покрыта»).
 */
export function extractGateMatcher(settings, hookBasename) {
  const pre = settings && settings.hooks && settings.hooks.PreToolUse;
  if (!Array.isArray(pre)) return [];
  for (const entry of pre) {
    const inner = entry && entry.hooks;
    if (!Array.isArray(inner)) continue;
    const has = inner.some((h) => h && typeof h.command === 'string' && h.command.includes(hookBasename));
    if (!has) continue;
    const m = typeof entry.matcher === 'string' ? entry.matcher : '';
    if (m === '*') return ['*'];
    return m.split('|').map((s) => s.trim()).filter(Boolean);
  }
  return [];
}
```

- [ ] **Step 4: Запустить тест — GREEN**

Run: `npx vitest run --root "c:/моя/проекты/портал crm/Документация/.claude/worktrees/brainrepo/app" --config "c:/моя/проекты/портал crm/Документация/.claude/worktrees/brainrepo/app/vitest.config.tools.mjs" door-coverage enforce-supreme-gate --reporter dot`
Expected: PASS (door-coverage новые тесты + enforce-supreme-gate существующие тесты без регрессий — export не меняет поведение).

- [ ] **Step 5: Commit**

```
git -C "c:\моя\проекты\портал crm\Документация\.claude\worktrees\brainrepo" add tools/door-coverage.mjs tools/door-coverage.test.mjs tools/enforce-supreme-gate.mjs
git -C "c:\моя\проекты\портал crm\Документация\.claude\worktrees\brainrepo" commit -m "feat(door-coverage): matcher extractor + canonical mutating tools; export SEED_TOOLS"
```

---

## Task 3: R-24 (часть 2) — блок «Покрытие дверей» в STATUS

**Files:**
- Modify: `tools/status-md-generator.mjs` (импорты door-coverage + SEED_TOOLS + `computeDoorCoverageBlock` + wiring + template)
- Test: `tools/status-md-generator.test.mjs`

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

Добавить в `tools/status-md-generator.test.mjs`:

```js
import { computeDoorCoverageBlock } from './status-md-generator.mjs';

describe('computeDoorCoverageBlock (R-24 покрытие дверей)', () => {
  it('matcher "*" → все двери покрыты (✅)', () => {
    const settings = { hooks: { PreToolUse: [{ matcher: '*', hooks: [{ command: 'node tools/enforce-supreme-gate.mjs' }] }] } };
    const md = computeDoorCoverageBlock({ settings });
    expect(md).toContain('## Покрытие дверей');
    expect(md).toContain('все двери покрыты');
  });
  it('хук не зарегистрирован → ⚠️ забытые двери (все мутирующие)', () => {
    const md = computeDoorCoverageBlock({ settings: {} });
    expect(md).toContain('забытые двери');
    expect(md).toContain('Write');
    expect(md).toContain('Bash');
  });
  it('частичный matcher → флагует непокрытые', () => {
    const settings = { hooks: { PreToolUse: [{ matcher: 'Edit|Write', hooks: [{ command: 'node tools/enforce-supreme-gate.mjs' }] }] } };
    const md = computeDoorCoverageBlock({ settings });
    expect(md).toContain('забытые двери');
    expect(md).toContain('Bash'); // не покрыт
    expect(md).not.toMatch(/непокрыт[а-я]*:.*Edit/i); // Edit покрыт
  });
  it('не падает при пустом settings undefined', () => {
    const md = computeDoorCoverageBlock({});
    expect(md).toContain('## Покрытие дверей');
  });
});

describe('renderStatus — doorCoverageBlock (R-24)', () => {
  const base = {
    now: '2026-06-09T10:00:00Z',
    c1: { status: 'ok', detail: 'OK' }, c2: { status: 'ok', detail: 'OK' },
    c3: { status: 'ok', detail: 'OK' }, c5: { status: 'ok', detail: 'OK' },
    observer: { episodeCount: 0, observerErrors: 0, piiMatches: 0 },
  };
  it('вставляет блок при наличии', () => {
    const md = renderStatus({ ...base, doorCoverageBlock: '## Покрытие дверей\n\nвсе двери покрыты.' });
    expect(md).toContain('## Покрытие дверей');
  });
  it('опускает при отсутствии', () => {
    expect(renderStatus(base)).not.toContain('## Покрытие дверей');
  });
});
```

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

Run: `npx vitest run --root "c:/моя/проекты/портал crm/Документация/.claude/worktrees/brainrepo/app" --config "c:/моя/проекты/портал crm/Документация/.claude/worktrees/brainrepo/app/vitest.config.tools.mjs" status-md-generator --reporter dot`
Expected: FAIL — `computeDoorCoverageBlock is not a function`.

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

(a) В `tools/status-md-generator.mjs` импорты (рядом с Task-1 импортом):

```js
import { auditDoors, auditExempt, extractGateMatcher, CANONICAL_MUTATING_TOOLS, isMutatingTool } from './door-coverage.mjs';
import { SEED_TOOLS } from './enforce-supreme-gate.mjs';
```

(b) Функция (после `computeLearningQueueBlock`):

```js
/**
 * R-24 (Блок B Класс 2) — read-only блок покрытия дверей. Инструмент, не покрытый matcher'ом
 * верховной стены и не семя = «забытая дверь» (урок F1: PowerShell мимо Bash). auditExempt
 * страхует: семя, чья способность мутирующая, = опасное исключение. Гейт не трогается.
 */
export function computeDoorCoverageBlock({ settings } = {}) {
  const s = settings || {};
  const matcher = extractGateMatcher(s, 'enforce-supreme-gate.mjs');
  const seeds = [...SEED_TOOLS];
  const doors = auditDoors({ tools: CANONICAL_MUTATING_TOOLS, matcher, seeds });
  const exempt = auditExempt({ exempt: seeds, isMutating: isMutatingTool });
  const matcherStr = matcher.includes('*') ? '*' : (matcher.join(', ') || '(хук НЕ зарегистрирован)');
  if (doors.ok && exempt.ok) {
    return `## Покрытие дверей\n\n✅ Все двери покрыты верховной стеной М2 (matcher: ${escapeCell(matcherStr)}).`;
  }
  const parts = [`## Покрытие дверей\n\n⚠️ Есть забытые двери (matcher: ${escapeCell(matcherStr)}).`];
  if (!doors.ok) {
    parts.push(`\nНепокрытые мутирующие инструменты: ${doors.uncovered.map(escapeCell).join(', ')}`);
  }
  if (!exempt.ok) {
    parts.push(`\n🔴 Опасные исключения (семя с мутирующей способностью): ${exempt.flagged.map(escapeCell).join(', ')}`);
  }
  return parts.join('\n');
}
```

(c) В `renderStatus` template добавить (после `learningQueueBlock`):

```js
${inputs.doorCoverageBlock ? `\n${inputs.doorCoverageBlock.trim()}\n` : ''}
```

(d) В main-блоке добавить (после Task-1 wiring):

```js
let doorCoverageBlock = null;
try {
  let settings = {};
  try { settings = JSON.parse(readFileSync('.claude/settings.json', 'utf-8')); } catch { settings = {}; }
  doorCoverageBlock = computeDoorCoverageBlock({ settings });
} catch (err) { console.warn('[status-md-generator] doorCoverageBlock skipped:', err.message); doorCoverageBlock = null; }
inputs.doorCoverageBlock = doorCoverageBlock;
```

- [ ] **Step 4: Запустить тест — GREEN**

Run: `npx vitest run --root "c:/моя/проекты/портал crm/Документация/.claude/worktrees/brainrepo/app" --config "c:/моя/проекты/портал crm/Документация/.claude/worktrees/brainrepo/app/vitest.config.tools.mjs" status-md-generator --reporter dot`
Expected: PASS.

- [ ] **Step 5: Commit**

```
git -C "c:\моя\проекты\портал crm\Документация\.claude\worktrees\brainrepo" add tools/status-md-generator.mjs tools/status-md-generator.test.mjs
git -C "c:\моя\проекты\портал crm\Документация\.claude\worktrees\brainrepo" commit -m "feat(status): R-24 door coverage block (read-only)"
```

---

## Task 4: R-30 — блок «Целостность журналов» (verifyChain live в read-only STATUS)

**Files:**
- Modify: `tools/status-md-generator.mjs` (импорты action-journal + receipt-key + `computeJournalIntegrityBlock` + sweep-wiring + template)
- Test: `tools/status-md-generator.test.mjs`

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

Добавить в `tools/status-md-generator.test.mjs`:

```js
import { computeJournalIntegrityBlock } from './status-md-generator.mjs';

describe('computeJournalIntegrityBlock (R-30 целостность журналов)', () => {
  it('ключ не provisioned → проверка недоступна (не «битые»)', () => {
    const md = computeJournalIntegrityBlock({ keyAvailable: false });
    expect(md).toContain('## Целостность журналов действий');
    expect(md).toContain('Ключ подписанта не provisioned');
    expect(md).not.toContain('🔴');
  });
  it('журналов нет → так и пишем', () => {
    const md = computeJournalIntegrityBlock({ keyAvailable: true, results: [] });
    expect(md).toContain('Журналов сессий не найдено');
  });
  it('все цепи целы → ✅', () => {
    const md = computeJournalIntegrityBlock({ keyAvailable: true, results: [
      { sessionId: 'a', ok: true, brokenAt: null },
      { sessionId: 'b', ok: true, brokenAt: null },
    ] });
    expect(md).toContain('✅');
    expect(md).toContain('2 сессий');
  });
  it('битая цепь → 🔴 с seq', () => {
    const md = computeJournalIntegrityBlock({ keyAvailable: true, results: [
      { sessionId: 'a', ok: true, brokenAt: null },
      { sessionId: 'bad', ok: false, brokenAt: 7 },
    ] });
    expect(md).toContain('🔴');
    expect(md).toContain('bad');
    expect(md).toContain('7');
  });
});

describe('renderStatus — journalIntegrityBlock (R-30)', () => {
  const base = {
    now: '2026-06-09T10:00:00Z',
    c1: { status: 'ok', detail: 'OK' }, c2: { status: 'ok', detail: 'OK' },
    c3: { status: 'ok', detail: 'OK' }, c5: { status: 'ok', detail: 'OK' },
    observer: { episodeCount: 0, observerErrors: 0, piiMatches: 0 },
  };
  it('вставляет при наличии', () => {
    const md = renderStatus({ ...base, journalIntegrityBlock: '## Целостность журналов действий\n\n✅ ок.' });
    expect(md).toContain('## Целостность журналов действий');
  });
  it('опускает при отсутствии', () => {
    expect(renderStatus(base)).not.toContain('## Целостность журналов действий');
  });
});
```

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

Run: `npx vitest run --root "c:/моя/проекты/портал crm/Документация/.claude/worktrees/brainrepo/app" --config "c:/моя/проекты/портал crm/Документация/.claude/worktrees/brainrepo/app/vitest.config.tools.mjs" status-md-generator --reporter dot`
Expected: FAIL — `computeJournalIntegrityBlock is not a function`.

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

(a) В `tools/status-md-generator.mjs` импорты + добавить `readdirSync` к fs-импорту строки 2:

```js
import { readFileSync, writeFileSync, existsSync, readdirSync } from 'fs';
```
И новые модульные импорты:
```js
import { loadJournal, verifyChain } from './action-journal.mjs';
import { resolveReceiptKey } from './receipt-key-config.mjs';
```

(b) Функция (после `computeDoorCoverageBlock`):

```js
/**
 * R-30 (Блок B Класс 3) — read-only блок целостности журналов действий. Требование
 * «verifyChain живьём» выполнено observation-only: sweep по ~/.claude/runtime/action-journal-*
 * делает main, сюда приходят уже посчитанные results. Без новых block-путей, fail-CLOSE
 * примитива verifyChain не понижается. keyAvailable=false → не «битые», а «проверка недоступна».
 */
export function computeJournalIntegrityBlock({ results, keyAvailable } = {}) {
  if (keyAvailable === false) {
    return `## Целостность журналов действий\n\nКлюч подписанта не provisioned — проверка цепи недоступна (ключ — owner-шаг A3).`;
  }
  const r = Array.isArray(results) ? results : [];
  if (r.length === 0) {
    return `## Целостность журналов действий\n\nЖурналов сессий не найдено (\`~/.claude/runtime/action-journal-*.jsonl\`).`;
  }
  const broken = r.filter((x) => x && !x.ok);
  if (broken.length === 0) {
    return `## Целостность журналов действий\n\n✅ Все цепочки целы (${r.length} сессий, verifyChain live).`;
  }
  const rows = broken.map((x) => `| \`${escapeCell(x.sessionId)}\` | ${x.brokenAt === null || x.brokenAt === undefined ? '—' : escapeCell(x.brokenAt)} |`).join('\n');
  return `## Целостность журналов действий\n\n🔴 Битые цепочки (${broken.length} из ${r.length}):\n\n| session | broken at seq |\n|---|---|\n${rows}`;
}
```

(c) В `renderStatus` template (после `doorCoverageBlock`):

```js
${inputs.journalIntegrityBlock ? `\n${inputs.journalIntegrityBlock.trim()}\n` : ''}
```

(d) В main-блоке (после Task-3 wiring):

```js
let journalIntegrityBlock = null;
try {
  const runtimeDir = join(homedir(), '.claude', 'runtime');
  const key = resolveReceiptKey();
  if (key === null || key === undefined) {
    journalIntegrityBlock = computeJournalIntegrityBlock({ keyAvailable: false });
  } else {
    const files = existsSync(runtimeDir) ? readdirSync(runtimeDir) : [];
    const results = [];
    for (const f of files) {
      const m = /^action-journal-(.+)\.jsonl$/.exec(f);
      if (!m) continue;
      const sessionId = m[1];
      try {
        const { entries, headSig } = loadJournal({ sessionId, runtimeDir });
        const v = verifyChain(entries, headSig, { key });
        results.push({ sessionId, ok: v.ok, brokenAt: v.brokenAt });
      } catch { results.push({ sessionId, ok: false, brokenAt: null }); }
    }
    journalIntegrityBlock = computeJournalIntegrityBlock({ results, keyAvailable: true });
  }
} catch (err) { console.warn('[status-md-generator] journalIntegrityBlock skipped:', err.message); journalIntegrityBlock = null; }
inputs.journalIntegrityBlock = journalIntegrityBlock;
```

- [ ] **Step 4: Запустить тест — GREEN**

Run: `npx vitest run --root "c:/моя/проекты/портал crm/Документация/.claude/worktrees/brainrepo/app" --config "c:/моя/проекты/портал crm/Документация/.claude/worktrees/brainrepo/app/vitest.config.tools.mjs" status-md-generator --reporter dot`
Expected: PASS.

- [ ] **Step 5: Commit**

```
git -C "c:\моя\проекты\портал crm\Документация\.claude\worktrees\brainrepo" add tools/status-md-generator.mjs tools/status-md-generator.test.mjs
git -C "c:\моя\проекты\портал crm\Документация\.claude\worktrees\brainrepo" commit -m "feat(status): R-30 journal integrity block via live verifyChain (read-only)"
```

---

## Task 5: enforce-reconcile.main() reader (инертный)

**Files:**
- Modify: `tools/enforce-reconcile.mjs` (+`reconcileEvent` pure + main-wiring через loadJournal; остаётся WARN-only, инертен)
- Test: `tools/enforce-reconcile.test.mjs`

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

Добавить в `tools/enforce-reconcile.test.mjs`:

```js
import { reconcileEvent } from './enforce-reconcile.mjs';

describe('reconcileEvent (R-30/reconcile reader, pure)', () => {
  it('действие совпало с пред-записью → null (нет WARN)', () => {
    const event = { tool_name: 'Write', tool_input: { file_path: '/x/y.txt' } };
    const journalEntries = [{ op: 'Write', object: '/x/y.txt' }];
    expect(reconcileEvent({ event, journalEntries })).toBe(null);
  });
  it('действие без пред-записи → WARN-строка', () => {
    const event = { tool_name: 'Write', tool_input: { file_path: '/x/y.txt' } };
    const w = reconcileEvent({ event, journalEntries: [] });
    expect(typeof w).toBe('string');
    expect(w).toContain('без журнальной пред-записи');
  });
  it('нет имени инструмента → null (не кричим)', () => {
    expect(reconcileEvent({ event: {}, journalEntries: [] })).toBe(null);
    expect(reconcileEvent({ event: null, journalEntries: [] })).toBe(null);
  });
  it('Bash action матчится по команде', () => {
    const event = { tool_name: 'Bash', tool_input: { command: 'git status' } };
    const journalEntries = [{ op: 'Bash', object: 'git status' }];
    expect(reconcileEvent({ event, journalEntries })).toBe(null);
  });
});
```

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

Run: `npx vitest run --root "c:/моя/проекты/портал crm/Документация/.claude/worktrees/brainrepo/app" --config "c:/моя/проекты/портал crm/Документация/.claude/worktrees/brainrepo/app/vitest.config.tools.mjs" enforce-reconcile --reporter dot`
Expected: FAIL — `reconcileEvent is not a function`.

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

В `tools/enforce-reconcile.mjs`:

(a) Импорты (после `import { fileURLToPath }...`):

```js
import { actionOf } from './enforce-supreme-gate.mjs';
import { loadJournal } from './action-journal.mjs';
import { resolveReceiptKey } from './receipt-key-config.mjs';
import { homedir } from 'node:os';
```

(b) Чистая функция (после `findOrphanIntents`):

```js
/**
 * Pure: harness-событие PostToolUse + загруженные журнальные пред-записи (payload'ы намерений)
 * → WARN-строка (если действие исполнено без пред-записи) или null. Никогда не бросает, не блок.
 * actionOf реюзится из supreme-gate — единый критерий {op, object}.
 */
export function reconcileEvent({ event, journalEntries = [] }) {
  if (!event) return null;
  const name = event.tool_name || (event.tool_use && event.tool_use.name);
  if (!name) return null;
  const action = actionOf({ name, input: event.tool_input || (event.tool_use && event.tool_use.input) || {} });
  const r = reconcileAction({ action, journalEntries });
  return r.matched ? null : `[reconcile] ⚠️ ${r.reason}`;
}
```

(c) Заменить тело `main()` на реальный (но инертный — хук не зарегистрирован) reader:

```js
async function main() {
  try {
    let input = '';
    for await (const chunk of process.stdin) input += chunk;
    let event; try { event = JSON.parse(input); } catch { process.exit(0); }
    // Загрузка журнала намерений сессии (пред-записи стены 8.1). Без ключа/файла — нет данных,
    // нет сигнала (fail-quiet). Реконсилер НИКОГДА не блокирует (PostToolUse не отменяет исполненное).
    const key = resolveReceiptKey();
    const sessionId = event.session_id || event.sessionId;
    let journalEntries = [];
    if (key && sessionId) {
      try {
        const runtimeDir = `${homedir()}/.claude/runtime`;
        const { entries } = loadJournal({ sessionId, runtimeDir });
        journalEntries = entries.map((e) => e && e.payload).filter(Boolean);
      } catch { journalEntries = []; }
    }
    const warn = reconcileEvent({ event, journalEntries });
    if (warn) process.stderr.write(warn + '\n');
  } catch { /* fail-quiet: реконсилер — сигнал, не блок */ }
  process.exit(0);
}
```

- [ ] **Step 4: Запустить тест — GREEN**

Run: `npx vitest run --root "c:/моя/проекты/портал crm/Документация/.claude/worktrees/brainrepo/app" --config "c:/моя/проекты/портал crm/Документация/.claude/worktrees/brainrepo/app/vitest.config.tools.mjs" enforce-reconcile --reporter dot`
Expected: PASS.

- [ ] **Step 5: Commit**

```
git -C "c:\моя\проекты\портал crm\Документация\.claude\worktrees\brainrepo" add tools/enforce-reconcile.mjs tools/enforce-reconcile.test.mjs
git -C "c:\моя\проекты\портал crm\Документация\.claude\worktrees\brainrepo" commit -m "feat(reconcile): wire reconcileEvent reader in main (inert, WARN-only)"
```

---

## Task 6: Полная регрессия + verification

**Files:** нет правок (только прогон).

- [ ] **Step 1: Полный прогон tools-only**

Run: `npx vitest run --root "c:/моя/проекты/портал crm/Документация/.claude/worktrees/brainrepo/app" --config "c:/моя/проекты/портал crm/Документация/.claude/worktrees/brainrepo/app/vitest.config.tools.mjs" --reporter dot`
Expected: PASS, total ≥ **3189** (baseline) + новые тесты Задач 1-5. 0 failed.

- [ ] **Step 2: Smoke генератора STATUS (не падает с новыми блоками)**

Запуск генератора напрямую (через vitest нельзя — это CLI; альтернатива — убедиться, что main-блок покрыт тестами renderStatus). Если возможно безопасно: проверить, что `renderStatus` с пустыми inputs не содержит новых заголовков (backward compat — уже покрыто в Задачах 1/3/4 Step 1). Реальный запуск `status-md-generator.mjs` пишет `docs/observer/STATUS.md` — НЕ запускать вручную (чужая авто-регенерируемая грязь дерева). Достаточно зелёных unit-тестов wiring.

- [ ] **Step 3: Verification-before-completion**

Зафиксировать в ответе: точное число тестов (verbatim из vitest summary), 0 failed, дельта от 3189. Никаких claim'ов «готово» без verbatim-вывода.

- [ ] **Step 4: Commit (если остались незакоммиченные правки — иначе пропустить)**

Все правки уже закоммичены в Задачах 1-5. commit-not-push: НЕ пушить (пуш ТОЛЬКО по слову «пуш» через finishing-a-development-branch).

---

## Self-Review

**1. Spec coverage** (4 пункта дизайна Блока B Классы 1+2):
- R-09 очередь обучения → Task 1 ✅
- R-24 покрытие дверей → Task 2 (helpers) + Task 3 (block) ✅
- R-30 целостность журналов (verifyChain live в read-only STATUS) → Task 4 ✅
- enforce-reconcile.main reader (инертный) → Task 5 ✅
- Регрессия ≥ 3189 → Task 6 ✅

**2. Placeholder scan:** все code-steps содержат полный код; нет TBD/«handle edge cases»/«similar to».

**3. Type consistency:**
- `computeLearningQueueBlock({queue})` / `computeDoorCoverageBlock({settings})` / `computeJournalIntegrityBlock({results, keyAvailable})` / `reconcileEvent({event, journalEntries})` — сигнатуры консистентны между определением (Step 3) и тестами (Step 1) каждой задачи.
- `extractGateMatcher(settings, hookBasename)` → массив; `auditDoors({tools, matcher, seeds})` принимает массив matcher — согласовано.
- `SEED_TOOLS` (Set) — `[...SEED_TOOLS]` для seeds-массива в Task 3; `GATE_SEED_TOOLS.has(...)` в тесте Task 2 — согласовано.
- `verifyChain(entries, headSig, {key})` / `loadJournal({sessionId, runtimeDir})` / `resolveReceiptKey()` — сигнатуры из реального кода (action-journal.mjs:53/93, receipt-key-config.mjs:23) — согласовано.
- `actionOf({name, input})` — реальная сигнатура (enforce-supreme-gate.mjs:65) — согласовано.

**4. Границы (жёсткое):** единственная правка в M2-файле — `export` у `SEED_TOOLS` (Task 2, аддитивно, инвариантность пинится тестом). Стена/пол/судья/escape логика не меняется. Все 4 пункта observation-only / инертны.

---

## Execution Handoff

План сохранён в `docs/superpowers/plans/2026-06-09-router-mentor-blockB-classes-1-2.md`.

**Исполнение — ИНЛАЙН через `superpowers:executing-plans`** (субагенты/Task/Workflow запрещены
решением владельца). Batch execution с чекпоинтами после каждой задачи (наблюдаемый RED до
prod-правки `tools/*.mjs`). НЕ requesting-code-review. commit-not-push.
