# Sub-plan C2 — манифест интеграции (боевая проводка) Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans (или subagent-driven-development) для исполнения task-by-task. Steps — чек-боксы (`- [ ]`).

**Goal:** Закрыть V-1 (нет манифеста интеграции) — собрать в ОДНОМ месте КАЖДЫЙ отложенный «→ C» wiring из sub-plan'ов A/B/C/D/E с ЯВНЫМ владельцем (задача C2 vs активация владельца), реально провести модули A/B/D в живой шов C, и добавить кросс-модульный интеграционный тест (V-2) + контракты продакшен-инъекций (C-3).

**Architecture:** C2 — фаза БОЕВОЙ ПРОВОДКИ поверх готовых чистых модулей A/B/C/D/E. Большинство W-пунктов трогают существующие движки М2/М3 АДДИТИВНО (CD-3 «встраиваемся поверх»). Правка `router-engine.mjs` (М3 — наставник-движок, который мы оживляем) — в scope. Правка `enforce-supreme-gate.mjs` (М2) — аддитивный предусловный слой, согласовать с владельцем. Регистрация хуков в settings.json — шаг владельца (вне scope). `llmCall`-транспорт — активация владельца.

**Tech Stack:** Node ESM `tools/*.mjs`, vitest (tools-config, ТОЛЬКО из `app/`: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs <фильтр> --reporter dot`).

**Источник истины:** спека `docs/superpowers/specs/2026-06-10-router-mentor-live-rebuild-design.md` (R6.3) §5.3/§5.8/§6.1/§6.2 + CD-R6-G/CD-3; ревью 2026-06-10 (V-1/V-2/нах.F1/F3/F5/C-2/C-3).

**NB git:** git зажат стеной у Claude — коммиты выполняет владелец вне стены.

**Зависимости:** требует РЕАЛИЗОВАННЫХ A (`artifactHasUnresolvedExtracted`), B (`buildGraphSection`/`districtDetail`), C (`buildMentorPrompt`/`runMentorRound`/`runMentorVerdict`/`freezeGate`/`deriveTaskId`/`appendNegotiation`), D (`decideReadEvent`). Порядок сборки — W5.

---

## Реестр деферралов (V-1 — каждый «→ C» имеет владельца)

| ID | Что отложено | Источник (план/self-review) | Владелец | Закрывает |
|----|--------------|------------------------------|----------|-----------|
| W1 | renderDistricts + «(100%)»-rewrite (catalog≠graph) | B self-review «переписать (100%) → C» | C2-task (правка router-engine, М3) | нах.F1 / C-2 / CD-R6-G |
| W2 | decideReadEvent→стена + contentType→роутер + planAuthorizesPath/probeCount продюсеры | D self-review «→ C» | C2-task (правка enforce-supreme-gate, М2, аддитивно) | нах.F3 / SE5 |
| W3 | onPlanWrite (A0): task-id + runMentorVerdict + персист журнала | C scope «C даёт onPlanWrite» (не построен) | C2-task (чистая ф-я) + регистрация owner | нах.F5 / C-1 |
| W4 | warn→owner-резюме гейта-1 | E self-review «→ C» | C2-task / owner-activation | нах.F3-adjacent / ✅O18 |
| W5 | мастер-порядок сборки sub-plan'ов | (нет — новое) | C2-doc | V-1 |
| W6 | кросс-модульный интеграционный тест | (нет — новое) | C2-task | V-2 |
| W7 | контракты продакшен-инъекций (полярность/форма/источник/дефолт) + readFileImpl | C «Боевая проводка» (неполна) | C2-doc | V-2 / C-3 |

---

### Task W1: renderDistricts + «(100%)»-rewrite — buildRouterPrompt разделяет catalog и graph (нах.F1/C-2/CD-R6-G)

**Files:** Modify: `tools/router-engine.mjs` (`buildRouterPrompt` `[:98-124]`). Test: `tools/router-engine.test.mjs`.

**Корень нах.F1/C-2:** `nodesForCtx = (catalog&&catalog.nodes) || (graph&&graph.nodes) || []` `[:99]` — граф используется лишь как фолбэк каталога; `graphSection` из B (`{kind,districtCount,layer0,staleness}`) НЕ имеет `.nodes` → игнорируется; единая секция «(100%)» `[:114]`. Спека CD-R6-G требует: каталог-скилов и project-граф — РАЗНЫЕ секции; «(100%)» переписать.

- [ ] **Step 1: Write the failing test.** При `graph` с `Array.isArray(graph.layer0)` — `buildRouterPrompt(...).system` содержит секцию «КАРТА РАЙОНОВ» с именами районов (напр. `tools`); каталог берётся ТОЛЬКО из `catalog.nodes`; строки «(100%)» больше нет. Существующий тест (catalog-only, без graph.layer0) остаётся зелёным.
- [ ] **Step 2: Run — FAIL** (секция районов не рендерится).
- [ ] **Step 3: Implement.** В `buildRouterPrompt`: (а) `nodesForCtx = (catalog && catalog.nodes) || []` (граф больше НЕ фолбэк каталога); (б) `[:114]` `'--- ГРАФ+КАТАЛОГ УЗЛОВ (100%) ---'` → `'--- КАТАЛОГ УЗЛОВ (skill-каталог) ---'`; (в) при `graph && Array.isArray(graph.layer0)` — добавить ОТДЕЛЬНУЮ секцию `'--- ГРАФ ПРОЕКТА: КАРТА РАЙОНОВ (прунённая; заземление против полного каталога) ---'` + рендер `graph.layer0` (`district`/`nodeCount`/`topNodes`); (г) inline staleness из `graph.staleness` если есть. `groundTrace` `[:73-79]` продолжает заземлять против переданного `graph` (полного нод-графа в runMentorRound — отдельный параметр `groundingGraph`, см. C T5 после SE2).
- [ ] **Step 4: Run — PASS** (районы в system; каталог отдельно; «(100%)» нет; старые router-тесты зелены).
- [ ] **Step 5: Commit** `feat(router-engine): разделить catalog и project-граф + карта районов (W1, нах.F1/CD-R6-G)`.

---

### Task W2: reading-discipline wiring — decideReadEvent в стену + продюсеры предикатов (нах.F3/SE5)

**Files:** Modify: `tools/enforce-supreme-gate.mjs` (`decide` `[:135-181]`). Test: `tools/enforce-supreme-gate.test.mjs`.

**Корень нах.F3/SE5:** D `decideReadEvent` не подключён; `planAuthorizesPath`/`probeCountThisRound` без продюсеров (always-true/0 — тихо отключают гейт ДР-1/probe-cap).

- [ ] **Step 1: Write the failing test.** В impl-режиме (frozenPlan валиден): авторское сырьё-чтение (Read файла кода ВНЕ шага плана) → `decide` блокирует через reading-gate ДР-1; чтение графа (graphify-out путь) / harness-авторизованного-шагом пути → пропуск. `planAuthorizesPath` строится из frozenPlan+stepPtr (НЕ always-true).
- [ ] **Step 2: Run — FAIL.**
- [ ] **Step 3: Implement.** (а) Продюсер `planAuthorizesPath(path)` = есть ли лист плана с object===path через `actionMatchesStep` `[plan-lock.mjs:86]` (НЕ always-true, SE5). (б) В `decide` ДО observe-only пропуска Read `[:137]`: если frozenPlan валиден И tool=Read/Grep — звать `decideReadEvent({path, frozenPlan:true, planAuthorizesPath, isCriticProbe, probeCountThisRound, ...})` (D); при `gate.block` → block с reason ДР-1. (в) `contentType` из `decideReadEvent.content` прокинуть для выбора reading-скила (роутер); (г) `probeCountThisRound` — реальный per-round счётчик из состояния кругов (W3/onPlanWrite). **NB:** аддитивный предусловный слой ПЕРЕД observe-only (CD-3) — согласовать как затрагивающий М2.
- [ ] **Step 4: Run — PASS** (авторское сырьё в impl-режиме блокируется; граф/probe/harness — нет; старые gate-тесты зелены).
- [ ] **Step 5: Commit** `feat(supreme-gate): wire decideReadEvent + planAuthorizesPath продюсер (W2, нах.F3/SE5)`.

---

### Task W3: onPlanWrite (A0) — task-id + производитель вердикта + персист (нах.F5/C-1)

**Files:** Create: `tools/on-plan-write.mjs`. Test: `tools/on-plan-write.test.mjs`.

**Корень нах.F5/C-1:** C заявил `onPlanWrite`, но ни одна задача не построила; это оркестратор, производящий mentor-вердикт при записи плана.

- [ ] **Step 1: Write the failing test.** Первый план задачи → `onPlanWrite` присваивает task-id (`deriveTaskId`, C T1) + персистит; зовёт `runMentorVerdict` (C T5b) → вердикт с `plan_hash` + persist журнала (`appendNegotiation`, C T2, try/catch). Re-issue плана с новым хешем НЕ сбрасывает task-id. Сбой журнала не крашит (best-effort).
- [ ] **Step 2: Run — FAIL** (модуль не существует).
- [ ] **Step 3: Implement.** Контракт: `onPlanWrite({planSteps, sessionId, existingTaskId, runMentorVerdictImpl, persistImpl, llmCall, ...inj}) → {taskId, verdict}`. Поток: `taskId = deriveTaskId({existingTaskId, firstPlanHash: planId(planSteps)})` `[plan-lock.mjs:15-17]` + persist (идемпотентно); `verdict = await runMentorVerdictImpl({plan, planHash: planId(planSteps), ...})`; журнал `appendNegotiation` обёрнут try/catch (SE10 — не крашить). Чистое ядро (I/O/llmCall инъектируются). Регистрация PostToolUse-хука на запись плана — owner-activation.
- [ ] **Step 4: Run — PASS.**
- [ ] **Step 5: Commit** `feat(on-plan-write): A0 task-id + производитель вердикта + персист (W3, нах.F5/C-1)`.

---

### Task W4: warn→owner-резюме гейта-1 (✅O18, нах.F3-adjacent)

**Files:** Modify: место сборки owner-резюме гейта-1 (определяется при сборке потока; если резюме формирует владелец — owner-activation, тогда только документировать). Test: соответствующий.

- [ ] **Step 1.** Прокинуть `decideMode(...).warn/warnReason` `[enforce-supreme-gate.mjs:211-216 после E T4]` в текст owner-резюме гейта-1 (§7.0): строка «энфорсмент off: judge_mode рассинхрон план≠артефакт». Тест: при warn:true резюме содержит предупреждение.
- [ ] **Step 2: Commit / либо пометить owner-activation** если резюме вне кода.

---

### Task W5: мастер-порядок сборки (V-1)

**Files:** (doc — этот раздел)

Порядок реализации sub-plan'ов: **A, B, D, E** (чистые автономные модули, параллельно) → **C** (живой шов, инъекция A/B/D) → **C2** (боевая проводка, эта) → **F** (радары). Регрессия каждого включает соседей. Активация (settings.json/ключ/флаг/`llmCall`) — владелец ПОСЛЕ всех.

---

### Task W6: кросс-модульный интеграционный тест (V-2)

**Files:** Create: `tools/router-mentor-integration.test.mjs`.

**Корень V-2:** per-модульные тесты на стабах прячут ошибки полярности/формы/состояния на РЕАЛЬНОЙ проводке (SE2/SE3/SE5).

- [ ] **Step 1: Write the test.** Провести РЕАЛЬНЫЕ `artifactHasUnresolvedExtracted` (A) как `hasUnresolvedExtractedImpl`, `buildGraphSection`(B) как `graphSection`, `decideReadEvent`(D) в C `buildMentorPrompt`/`runMentorRound`/`freezeGate` с ВЕРНОЙ полярностью/формой. End-to-end: (а) районы B видны в промпте (W1); (б) грязный verity-артефакт → freeze блок (A↔C полярность); (в) `graph` vs `graphSection` не перепутаны (SE2); (г) авторское сырьё в impl блокируется (D↔W2).
- [ ] **Step 2: Run — GREEN.**
- [ ] **Step 3: Commit** `test(router-mentor): кросс-модульный интеграционный тест (W6, V-2)`.

---

### Task W7: контракты продакшен-инъекций (V-2/C-3)

**Files:** (doc — этот раздел; ссылается на код W1-W3/C)

Таблица — обязательна при боевой активации:

| Инъекция | Полярность/форма | Продакшен-источник | Дефолт (поведение) |
|----------|------------------|--------------------|--------------------|
| `hasUnresolvedExtractedImpl` (freeze-gate) | `true=ЕСТЬ неразрешённая EXTRACTED=ГРЯЗНО=блок` | A `artifactHasUnresolvedExtracted(art, readFileImpl)` | undefined → блок (fail-closed) |
| `readFileImpl` (внутри ↑) | строка-контент файла | **C-3:** `(f)=>fs.readFileSync(path.resolve(REPO_ROOT, f),'utf8')` — НЕ голый `fs.readFileSync` (Buffer + относит. путь от CWD хука → промах → вечный блок) | бросок → not-resolved → downgrade |
| `planHash` (freeze-gate) | `planId(замораживаемых steps)` `[plan-lock.mjs:15-17]` | producer `onPlanWrite` (W3) кладёт его и в вердикт | **F-C4 (ужесточение A1):** null/пустой → БЛОК — печать без binding запрещена |
| `groundingGraph` (runMentorRound) | ПОЛНЫЙ нод-граф для `groundTrace` (форма `buildNodeGraph`: byId/bySlug/byName) | реальный project нод-граф | — (не путать с `graphSection`, SE2; своп ломает заземление с грохотом — W6 тест) |
| `graphSection` (buildMentorPrompt) | `{kind,layer0,staleness}` для промпта | B `buildGraphSection(...)` | нет/пустой layer0 → явный маркер «КАРТА РАЙОНОВ ОТСУТСТВУЕТ» (F-C6), staleness fail-safe (B T7) |
| `planAuthorizesPath` (decideReadEvent) | `(path)=>bool` из листьев frozenPlan | **построен:** `buildPlanAuthorizesPath(frozenPlan, {stepPtr, normalize})` (W2; op-АГНОСТИЧЕН по нормализованному object — Д-С2-1, Bash-шаги не авторизуют; **F-C2-2 scope:** ТЕКУЩИЙ лист + ПРОЙДЕННЫЕ (префикс до stepPtr) — будущие шаги НЕ авторизованы, фронт-ран закрыт; указатель не резолвится → fail-closed) | always-true ОТКЛЮЧАЕТ гейт — запрещено |
| `isCriticProbe` (decideReadEvent) | mentor-derived bool (F-D3) | вердикт/трасса наставника (шов C, контроллер-оркестрация) | **в СТЕНЕ всегда false (Д-С2-2)** — probe-ветка в стене недостижима; критик-чтение живёт в шве |
| `probeCountThisRound` (decideReadEvent) | реальный per-round счётчик | состояние кругов C/W3 | 0 ОТКЛЮЧАЕТ probe-cap — в стене неактуально (isCriticProbe=false); в шве C дозапросы районов уже secure-default |
| `districtProbesUsed` (runMentorRound) | реальный per-round счётчик дозапросов районов | состояние кругов C/W3 | **F-C5 (secure default):** омиссия = `DISTRICT_PROBE_CAP` = дозапрос отклоняется |
| `llmCall` (runMentorRound/Verdict) | `({buildPrompt})→trace\|verdict` | `callAnthropicAPI` (ключ/флаг) | активация владельца |

**Резолюция W4 (2026-06-11):** код-часть СДЕЛАНА — `runGate` дописывает `warnReason` (judge_mode рассинхрон O18) в message вывода хука (раньше warn ронялся). Полное owner-резюме гейта-1 — поведенческая сборка контроллера → owner-activation/дисциплина, не код.

**sharp-edges C2 (2026-06-11, «чиним все» владельца):**
- **F-C2-1 [заметка]:** reading-gate в `decide` использует `frozenPlan.steps` ДО seal-проверки (:139) — направление безопасно: подделанный план СУЖАЕТ чтения (не расширяет границы за пределы плана), мутаторы всё равно упрутся в verify; запись план-файла защищена runtime-write-deny/М6.
- **F-C2-2 [починен]:** scope авторизации — см. строку `planAuthorizesPath` выше.
- **F-C2-3 [починен]:** пустой каталог в `buildRouterPrompt` → явный маркер «КАТАЛОГ ПУСТ... воздержание 5.2» (зеркало F-C6, сигнал-всегда).
- **F-C2-4 [контракт]:** `journalEntries` в `onPlanWrite` ОБЯЗАН быть загруженной цепью переговоров задачи — омиссия (дефолт `[]`) ломает нумерацию кругов (всегда круг 1), не безопасность; персист-слой — owner-activation.
- **F-C2-5 [by-design]:** в impl-режиме Grep без `path` и Read файлов вне (текущий+пройденные) шагов плана → блок ДР-1 — интент спеки §5.8/ДР-1; легальные пути: шаг плана / граф-карта / critic-probe через шов / owner-escape. До регистрации хука стена инертна.
- **F-C2-6 [by-design, финревью 2026-06-11]:** Glob вне гейта ДР-1 (W2 гейтит только `Read|Grep` [enforce-supreme-gate decide]) — Glob отдаёт ИМЕНА файлов, не контент → не «авторское сырьё» по §5.8; зеркально F-C2-5 (Grep контент читает → гейтится).

**Финревью Фазы B (2026-06-11, проходы 1-2: audit-context-building + sharp-edges, «чиним все» владельца):**
- **FR-1 [починен]:** SE-A3-обязательство (context-verity:93-94) закрыто В ГЕЙТЕ — `freezeGate` inline-чеками блокирует INFERRED без `derivation_ref` (VF-1), неизвестный `kind` (SE-A1) и битую запись артефакта; «1 честная EXTRACTED + немаркированные утверждения» печать больше не проходит.
- **FR-2 [починен]:** двойной рендер районов/staleness (system W1 + user Д-1а) снят — канон: рендерит БАЗА `buildRouterPrompt` в system; seam-extras рендерят ТОЛЬКО при пустом/отсутствующем layer0 (маркер F-C6 + staleness ✅O16); в router-engine пустой `layer0:[]` больше не рендерит пустой заголовок секции; W6/seam-тесты переведены на канон (no-dup ассерт).
- **FR-4 [починен]:** `parseRouterResponse` — балансный срез ПЕРВОГО JSON-объекта как фолбэк (жадный `{…}`-regex ронял в null текст с двумя объектами).

---

## Self-Review (writing-plans)

**1. Spec coverage:** V-1 (манифест) → реестр + W1-W7. нах.F1/C-2 → W1. нах.F3/SE5 → W2 + W7. нах.F5/C-1 → W3. ✅O18 → W4. V-2 → W6 + W7. C-3 → W7. Мастер-порядок → W5. ✅

**2. Placeholder scan:** W1/W2/W3/W6 — TDD-шаги + контракты с цитатами (верифицированы в ревью); W4 — условный (owner-activation если резюме вне кода, честно помечено); W5/W7 — doc-таблицы. Нет «TBD».

**3. Type consistency:** `onPlanWrite`→`{taskId,verdict}`; `planAuthorizesPath`→bool; `decideReadEvent`→`{readKind,content,gate,probe}` (D); `hasUnresolvedExtractedImpl`→bool (полярность true=грязно). Имена согласованы с C/D/A.

**Известное ограничение (честно):** W2/W4 трогают М2 (`enforce-supreme-gate`) аддитивно — согласовать с владельцем как затрагивающие верховную стену (CD-3 «поверх», не пересмотр). Регистрация хуков + `llmCall`-транспорт + ключ/флаг — активация владельца, вне scope. До активации шов инертен (asymptote behavioral-residual, §15 CD-R6-A).
