diff --git a/docs/superpowers/plans/2026-06-09-router-mentor-blockB-classes-1-2.md b/docs/superpowers/plans/2026-06-09-router-mentor-blockB-classes-1-2.md new file mode 100644 index 00000000..e29e8112 --- /dev/null +++ b/docs/superpowers/plans/2026-06-09-router-mentor-blockB-classes-1-2.md @@ -0,0 +1,746 @@ +# Блок 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 "/app" --config "/app/vitest.config.tools.mjs" <фильтр> --reporter dot` — БЕЗ `cd app`, явный `--root`. После 1-го Bash cwd залипает в `app/`. +- git — только PowerShell, `git -C ""`, литеральные пути. +- 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 хука в 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.