# Машина 5 — «Укрепление защиты-пола» — план реализации

> **Для исполнителя:** план гоняется **ИНЛАЙН** через `superpowers:executing-plans` (ограничение владельца — суб-агенты/Task НЕ используются). Шаги — чекбоксы `- [ ]`.

**Goal:** Построить несущий архитектурный пол М5 — вето-до-плана на необратимое, изоляцию ключа (шов под HSM), критерий-носитель GREEN, 7 усилений и сквозные fail-close инварианты — поверх готовых M1-M4.

**Architecture:** Чистое ядро + тонкая обёртка (как M1-M4). Новый пол-хук с matcher `*`, зарегистрированный **отдельно** от верховной стены. Единый двухуровневый классификатор разрушительности (`floor`/`suspicious`), который потребляют пол / стена / судья. Подписи (HMAC) остаются; ценность подписанта — анти-казуальная утечка + шов под железо.

**Tech Stack:** Node ESM (`tools/*.mjs`), vitest (config `vitest.config.tools.mjs`), HMAC-SHA256 (`receipt-sign.mjs`), keytar (OS-keychain), Claude Code PreToolUse/PostToolUse/SessionStart хуки.

**Источник:** spec `docs/superpowers/specs/2026-06-07-router-mentor-machine-5-floor-design.md` (§3 блоки + §8 инвентарь + §10 амендмент 2026-06-07).

**Ревизия R1 (2026-06-07, факт-аудит плана по реальному коду):** учтены 7 правок — F1 (+`format` в suspicious-набор, его терял rewire detectHighRisk), F2 (длинно-флаговый `rm --recursive --force` теперь floor), F3 (Step 1.7 — заменяется только строка router-engine:21, не вся функция), F4 (удалить `router-engine.mjs:12` DESTRUCTIVE_RE, иначе Δ9(б) падёт), F5 (floor читает одобрение **read-only**, не consume — гонка с существующим `enforce-branch-switch`), §2-точность (floor инспектирует и Write `file_path`, не только Bash), скил-правки (audit-context-building только на Пакетах 1,2,4,5,7; +`systematic-debugging`; линзы focus+по риску).

---

## Жёсткие правила исполнения (для всех задач)

- **commit-not-push:** коммитим каждую задачу, **пушим только по слову владельца «пуш»**.
- **TDD:** RED → GREEN, реальный `expect` в изменённом фрагменте теста (не заглушка).
- **Инлайн, без суб-агентов:** все три адверсариальные линзы (грабли/корректность/атака) — инлайн, не Task.
- **Базовая регрессия tools-only:** `2560 passed + 2 skip`. Команда (одна абсолютная, без `cd`, без `&&`):
  `node "<wt>\app\node_modules\vitest\vitest.mjs" run --root "<wt>\app" --config "<wt>\app\vitest.config.tools.mjs" [фильтр]`
  где `<wt>` = `c:\моя\проекты\портал crm\Документация\.claude\worktrees\brainrepo`. Сводка — `--reporter dot`.
- **git-квирк:** коммит через PowerShell-инструмент, БЕЗ переменных, по одной команде с литеральным путём:
  `git -C "<wt>" add <files>` затем отдельной командой `git -C "<wt>" commit -F "<wt>\.scratch\msg.txt"`.
  («Can't find lefthook in PATH» — неблокирующий шум.)
- **Активация хуков в `.claude/settings.json` — шаг ВЛАДЕЛЬЦА**, не код (Claude'у settings.json заблокирован). До регистрации новые хуки инертны. План доводит до «готово к регистрации», регистрацию выполняет владелец.

---

## Цепочка скилов (согласована с владельцем 2026-06-07)

**Костяк:** `using-git-worktrees` (подтвердить изоляцию) → `writing-plans` (этот файл) → `executing-plans` (инлайн).

**Петля на пакет:**
`audit-context-building` (**только Пакеты 1,2,4,5,7 — трогают существующий код M1-M4/хуки; на net-new Пакетах 3,6 ПРОПУСТИТЬ — понимать нечего, скил для существующего кода**) → `test-driven-development` (RED→GREEN) → **линзы инлайн** (focus-линза пакета из таблицы — ОБЯЗАТЕЛЬНА; две другие из `sharp-edges`/корректность/`agentic-actions-auditor` — по риску, не вслепую) → `requesting-code-review` → `verification-before-completion` → `regression`.

**Скил при непредвиденном красном** (тест падает НЕ RED-by-design): `superpowers:systematic-debugging` — систематическая отладка, не угадывание патча.

**Спец:** Пакет 4 (node-whitelist) — дополнительно `verify` (живой прогон тулчейна после сужения). Финал — `finishing-a-development-branch` под «пуш» владельца.

---

## Карта файлов

**Новые:**
- `tools/classify-destructive.mjs` (+`.test.mjs`) — единый двухуровневый классификатор (Пакет 1).
- `tools/floor-decide.mjs` (+`.test.mjs`) — чистое ядро вето-до-плана (Пакет 2).
- `tools/enforce-floor.mjs` (+`.test.mjs`) — обёртка пол-хука, matcher `*`, fail-close (Пакет 2).
- `tools/floor-signer.mjs` (+`.test.mjs`) — процесс-подписант GREEN/печатей + occurrence-счётчик (Пакет 3).
- `tools/criterion-green.mjs` (+`.test.mjs`) — по-критерийный производитель GREEN `{criterion_id, green, code_fingerprint, coverage_of_changed}` (Пакет 5).
- `tools/floor-manifest-check.mjs` (+`.test.mjs`) — SessionStart WARN-самопроверка регистрации (Пакет 6).
- `tools/m5-floor-invariants.test.mjs` — сквозные CI-инварианты N5/Δ9 (Пакет 6).

**Модифицируемые:**
- `tools/judge-orchestrator.mjs:84` — `a2CaseSelect` → `classifyDestructive(...).suspicious` (Пакет 1).
- `tools/router-engine.mjs` — `detectHighRisk` → `classifyDestructive(...).suspicious` (Пакет 1).
- `tools/enforce-supreme-gate.mjs` — Δ7 defense-in-depth: на allow-пути звать `classifyDestructive(...).floor`, не двигать указатель на разрушительном (Пакет 2).
- `tools/receipt-key-config.mjs` / `judge-key-config.mjs` — Δ4 запрет env-fallback (Пакет 4).
- `tools/enforce-router-gate.mjs:90` — Δ4 сужение `node`-whitelist (Пакет 4, RISK).
- `tools/enforce-router-gate.mjs:94` — `migrate:fresh` уходит из whitelist в floor (Пакет 2).
- `tools/judge-gate-floor.mjs` — Δ2 fingerprint в `criteriaGreenMatched`; Δ5 + подпись; значимость механически (Пакет 5).
- `tools/plan-lock.mjs` — sealed `criterion_id` в `freezePlan` (Пакет 5).
- `tools/judge-orchestrator.mjs` — Δ6: 4 шага критерий-гейта в `runGateLadder` (Пакет 5).
- `tools/receipt-sign.mjs` — `RECEIPT_DOMAINS.M5_GREEN` (Пакет 3).
- `tools/enforce-read-path-deny.mjs` (+контент-скан секретов), `enforce-normative-content-rules.mjs` (детерм+fail-close), `enforce-mcp-classification.mjs` (egress-скан), `enforce-workflow-gate.mjs` (inline-script+register), `enforce-parallel-session-lock.mjs`+`parallel-session-lock.mjs` (pid-liveness+register), `enforce-tdd-gate.mjs` (мутация/покрытие) — Пакет 7 (7 заплаток).

---

## Пакеты (порядок строительства)

| # | Пакет | Зависит | Главный риск | Линзы-фокус |
|---|-------|---------|--------------|-------------|
| 1 | `classifyDestructive` двухуровневый (§4, N1) | — | over-block голым `migrate` | sharp-edges |
| 2 | Блок 1: floor-decide + enforce-floor + дверь владельца Δ1 + Δ7 | 1 | пол сильнее владельца / обход цепочкой | атака |
| 3 | Блок 2: floor-signer + `M5_GREEN` + occurrence (Δ4 честно) | — | над-обещание секретности | атака |
| 4 | Блок 2 RISK: запрет env + сужение node-whitelist | 3 | ломает весь `node tools/*.mjs` | verify (живой прогон) |
| 5 | Блок 3: критерий-носитель (Δ2/Δ5/Δ6) | 3 | хрупкость fingerprint / частичный пол | корректность |
| 6 | Блок 5: сквозные инварианты N5/Δ9 + манифест-WARN Δ8 | 1-5 | ложная гарантия регистрации | корректность |
| 7 | Блок 4: 7 заплаток существующих хуков | 1 | разъезд токенайзера с полом | все три |

Пакеты 3 и 5 независимы от 2/4 — можно строить параллельно по времени, но инлайн идём по порядку. Пакет 4 — когда 1-3 зелёные (точечный откат легче).

---

## Пакет 1 — `tools/classify-destructive.mjs` (единый двухуровневый классификатор)

**Контракт:** `classifyDestructive(command: string) → { floor: boolean, suspicious: boolean, reason: string }`.
Инвариант: `floor ⇒ suspicious` (floor-набор ⊂ suspicious-набор).
- `floor:true` — точное необратимое по КОМАНДЕ: `git push --force`/`--force-with-lease`/`push -f`/`push +`, `reset --hard`, `php artisan migrate:fresh|migrate:reset|migrate:refresh`, `db:wipe`, `rm -rf` (любой `rm` с `-r`+`-f`). НЕ включает голый `migrate`/`migrate:rollback`, голый `rm` без `-rf`, голый `drop`/`delete` (это уровень suspicious).
- `suspicious:true` — грубый набор для голосов судьи (как сейчас `a2CaseSelect`): всё floor + голый `migrate`, `drop`, `delete`, `truncate`, `rm`, `rmdir`, любой `--force`, любой `reset --hard`.

**Files:**
- Create: `tools/classify-destructive.mjs`
- Test: `tools/classify-destructive.test.mjs`
- Modify (rewire): `tools/judge-orchestrator.mjs:84-89`, `tools/router-engine.mjs` (detectHighRisk)

- [ ] **Step 1.0: audit-context-building на 3 существующих regex (контекст, не код)**

Прочитать построчно и записать ТОЧНОЕ текущее поведение трёх источников, чтобы консолидация его сохранила:
- `tools/judge-orchestrator.mjs:84` (`DESTRUCTIVE_RE` + `a2CaseSelect`),
- `tools/router-engine.mjs` (`detectHighRisk` — прочитать целиком, зафиксировать сигнатуру и что считает high-risk),
- `tools/shell-content-rules.mjs` (force-push паттерн).
Выписать список команд, которые каждый сейчас ловит → это характеризующие тест-кейсы parity для Step 1.5/1.6.

- [ ] **Step 1.1: RED — floor-уровень**

Создать `tools/classify-destructive.test.mjs`:

```js
import { describe, it, expect } from 'vitest';
import { classifyDestructive } from './classify-destructive.mjs';

describe('classifyDestructive — floor-уровень (точный необратимый набор)', () => {
  it.each([
    'git push --force',
    'git push --force-with-lease origin main',
    'git push -f',
    'git reset --hard HEAD~3',
    'php artisan migrate:fresh',
    'php artisan migrate:reset',
    'php artisan migrate:refresh',
    'php artisan db:wipe',
    'rm -rf build',
    'rm -fr node_modules',
    'rm --recursive --force build',
    'rm --force --recursive /tmp/x',
  ])('floor:true для %s', (cmd) => {
    const r = classifyDestructive(cmd);
    expect(r.floor).toBe(true);
    expect(r.suspicious).toBe(true); // инвариант floor ⇒ suspicious
  });
});
```

- [ ] **Step 1.2: Run RED**

Run: `node "<wt>\app\node_modules\vitest\vitest.mjs" run --root "<wt>\app" --config "<wt>\app\vitest.config.tools.mjs" classify-destructive`
Expected: FAIL — «classifyDestructive is not a function» (модуль не существует).

- [ ] **Step 1.3: GREEN — минимальная реализация**

Создать `tools/classify-destructive.mjs`:

```js
#!/usr/bin/env node
/**
 * classify-destructive (§4, N1) — единый двухуровневый классификатор разрушительности
 * по СУТИ команды. floor — точный необратимый набор (hard-block пола). suspicious —
 * грубый набор для голосов судьи. Инвариант: floor ⇒ suspicious. Один источник правды
 * (Δ9-б: другого regex разрушительных команд в tools/ быть не должно).
 */

// F2: rm разрушителен (floor) только если есть И рекурсия (-r/-R/--recursive), И force
// (-f/--force) — слитно (-rf), раздельно, короткими ИЛИ длинными флагами. Длинно-флаговый
// `rm --recursive --force` так же необратим → floor (старый regex его терял).
function rmIsFloor(cmd) {
  if (!/\brm\b/i.test(cmd)) return false;
  const rec = /-[a-z]*r/i.test(cmd) || /--recursive\b/i.test(cmd);
  const force = /-[a-z]*f/i.test(cmd) || /--force\b/i.test(cmd);
  return rec && force;
}

// Точный необратимый набор — то, что пол блокирует НАГЛУХО (rm — через rmIsFloor).
const FLOOR_RE = [
  /\bgit\s+push\b[^\n]*\s(?:--force\b|--force-with-lease\b|-f\b|\+\S)/i, // force-push/перепись
  /\breset\s+--hard\b/i,
  /\bartisan\s+migrate:(?:fresh|reset|refresh)\b/i,
  /\bartisan\s+db:wipe\b/i,
];

// Грубый набор для судьи (надмножество floor) — лишний голос не вредит.
// F1: +format (его содержал detectHighRisk М3 — иначе rewire потеряет триггер форматирования диска).
const SUSPICIOUS_RE = [
  /\b(?:rm|rmdir|drop|delete|truncate|migrate|format)\b/i,
  /--force\b/i,
  /\breset\s+--hard\b/i,
  /\bartisan\s+migrate:(?:fresh|reset|refresh)\b/i,
  /\bartisan\s+db:wipe\b/i,
];

export function classifyDestructive(command) {
  const cmd = String(command || '');
  const floor = rmIsFloor(cmd) || FLOOR_RE.some((re) => re.test(cmd));
  const suspicious = floor || SUSPICIOUS_RE.some((re) => re.test(cmd));
  const reason = floor ? 'необратимая команда (floor)' : suspicious ? 'подозрительная команда (suspicious)' : 'не разрушительная';
  return { floor, suspicious, reason };
}
```

- [ ] **Step 1.4: Run GREEN**

Run: тот же vitest-фильтр `classify-destructive`.
Expected: PASS (floor-кейсы).

- [ ] **Step 1.5: RED+GREEN — N1 (голый migrate НЕ floor) + suspicious-уровень**

Добавить в тест блок:

```js
describe('classifyDestructive — suspicious без floor (N1: пол не ломает деплой)', () => {
  it.each([
    'php artisan migrate',
    'php artisan migrate:rollback',
    'php artisan migrate --force',
  ])('suspicious:true, floor:false для %s', (cmd) => {
    const r = classifyDestructive(cmd);
    expect(r.suspicious).toBe(true);
    expect(r.floor).toBe(false); // КРИТИЧНО: пол НЕ блокирует обычную миграцию
  });

  it.each(['rm file.txt', 'rm -r dir', 'DROP TABLE x', 'truncate logs', 'format D:', 'npm run format'])('suspicious:true, floor:false для %s', (cmd) => {
    const r = classifyDestructive(cmd);
    expect(r.suspicious).toBe(true);
    expect(r.floor).toBe(false);
  });

  it('не разрушительная команда → оба false', () => {
    const r = classifyDestructive('git status');
    expect(r.floor).toBe(false);
    expect(r.suspicious).toBe(false);
  });
});
```

NB: `migrate --force` обязан быть `floor:false` — `--force` к обычной миграции не делает её необратимой; FLOOR_RE намеренно требует `git push ... --force`, а не любой `--force`. Run RED → при необходимости уточнить FLOOR_RE (force только в контексте `git push`/`reset --hard`) → GREEN.

- [ ] **Step 1.6: Rewire `a2CaseSelect` (M4) на классификатор — parity-тест**

Modify `tools/judge-orchestrator.mjs:81-89`: убрать локальный `DESTRUCTIVE_RE`, импортировать `classifyDestructive`, `a2CaseSelect` возвращает `'destructive'` при `.suspicious`, иначе `'divergence'`. Перед правкой — RED-характеризующий тест в `judge-orchestrator.test.mjs` на parity (кейсы из Step 1.0), после правки GREEN.

```js
// judge-orchestrator.mjs (фрагмент)
import { classifyDestructive } from './classify-destructive.mjs';
export function a2CaseSelect(action) {
  const cmd = String((action && (action.object || action.command)) || '');
  return classifyDestructive(cmd).suspicious ? 'destructive' : 'divergence';
}
```

- [ ] **Step 1.7: Rewire `detectHighRisk` (M3) — ТОЛЬКО строку проверки команды**

`tools/router-engine.mjs` (подтверждено: `detectHighRisk` на стр.19, свой `DESTRUCTIVE_RE` на стр.12): заменить **только строку 21** `if (DESTRUCTIVE_RE.test(command)) reasons.push('разрушительная операция')` → `if (classifyDestructive(command).suspicious) reasons.push('разрушительная операция')`. Остальное (`prodDeploy`/`stepCount`/`sensitive`, стр.22-24) НЕ трогать. Импортировать `classifyDestructive`; **удалить локальный `DESTRUCTIVE_RE` (стр.12)** — иначе Δ9(б) CI-инвариант (Step 1.8) упадёт. NB: `DESTRUCTIVE_RE` М3 содержит `format` → `classifyDestructive(...).suspicious` обязан его покрывать (F1, см. Step 1.3 — покрыт). Parity-тест в `router-engine.test.mjs` на кейсах Step 1.0 (включая `format` + бывшие срабатывания) → RED→GREEN.

- [ ] **Step 1.8: Δ9(б) CI-инвариант «единственный источник»**

**Создать** `tools/m5-floor-invariants.test.mjs` (файл создаётся здесь, наполняется далее в Пакете 6): grep по `tools/*.mjs`, что regex разрушительных команд (`migrate:fresh`, `push --force`, `reset --hard`, `format`) встречается ТОЛЬКО в `classify-destructive.mjs`. RED (сейчас встречается в `judge-orchestrator.mjs:84` И `router-engine.mjs:12` до rewire) → после Step 1.6/1.7 (оба `DESTRUCTIVE_RE` удалены) GREEN.

- [ ] **Step 1.9: 3 линзы инлайн (фокус — sharp-edges)**
  - **Грабли:** обфускация floor — `git push  --force` (двойной пробел), `git push origin main --force`, `--force=true`? кавычки `git push "--force"`? Проверить, что FLOOR_RE ловит; добавить тест-кейсы.
  - **Корректность:** инвариант `floor ⇒ suspicious` доказан тестом; голый `migrate` floor:false доказан.
  - **Атака:** может ли разрушительная команда уйти за `suspicious` но мимо `floor` и потом пролезть? (floor — только hard-block; suspicious уходит судье — это ОК). Зафиксировать: цепочки (`cat x && git push --force`) — это зона floor-decide (Пакет 2, посегментно), не классификатора одиночной строки.

- [ ] **Step 1.10: Регрессия + commit**

Run: полный tools-only vitest (`--reporter dot`). Expected: `2560+ passed` (новые тесты добавлены, старые не сломаны).
Commit: `git -C "<wt>" add tools/classify-destructive.mjs tools/classify-destructive.test.mjs tools/judge-orchestrator.mjs tools/judge-orchestrator.test.mjs tools/router-engine.mjs tools/router-engine.test.mjs tools/m5-floor-invariants.test.mjs` → `git -C "<wt>" commit -F "<wt>\.scratch\msg.txt"` (msg: `feat(m5): classifyDestructive двухуровневый + rewire a2CaseSelect/detectHighRisk (§4, N1)`).

---

## Пакет 2 — Блок 1: несущий пол (floor-decide + enforce-floor + дверь Δ1 + Δ7)

> Бит-сайз шаги пишутся just-in-time перед началом пакета (после Пакета 1). Ниже — задачи, файлы, критерии приёмки, фокус-линза. Каждая задача = TDD RED→GREEN→commit.

**Задачи:**
- **2.1 `floor-decide.mjs` ядро** — `floorDecide({ toolUse, ownerApproval })` срабатывает ПЕРВЫМ (до seed/observe/плана). Жёсткий список в КОДЕ, инспектирует **ОБА типа инструментов** (§2-точность): (а) **Bash** — `classifyDestructive(...).floor` посегментно (`tokenizeBash`, как `isObserveOnly` в supreme-gate:49 — переиспользовать тот же токенайзер, чтобы не разъехаться); (б) **Write/Edit/NotebookEdit** — `file_path`/`path`/`notebook_path` через `pathNormalize`: запись секрета (`.env`, ключи, имена keychain-сервисов) + `~/.claude/runtime` (NB: `enforce-runtime-write-deny` уже прикрывает runtime для Write — floor дублирует как defense-in-depth, secret-paths — новое). **Критерий:** `cat x && git push --force` → block (за читающим сегментом не прячется); Write в `.env` → block; `php artisan migrate` → allow (N1); `unknown`-сессия → максимальный запрет. fail-CLOSED.
- **2.2 Дверь владельца Δ1** — `floorDecide` пускает разрушительную операцию ТОЛЬКО при валидном `ownerApproval` из канала `APPROVAL` (читать через существующий `askuser-answer-parser` + `verifyReceipt(..., RECEIPT_DOMAINS.APPROVAL)`; контроллер канал не пишет). **F5 — НЕ consume:** существующий `enforce-branch-switch.mjs` (подтверждён, S8) уже one-shot-потребляет тот же `approve_git_operation` → два потребителя = гонка, одно действие ложно заблокируется. Floor проверяет одобрение **read-only** (наличие валидного подписанного одобрения точной команды во временном окне), consume оставляем branch-switch'у. **Критерий:** без одобрения — block; с валидным подписанным одобрением точной команды — allow; одобрение чужой команды — block; **floor + branch-switch не конфликтуют на одном одобрении** (тест на двойное потребление: branch-switch consume'ит, floor после этого всё ещё видит/не видит по политике окна — без взаимной поломки).
- **2.3 `enforce-floor.mjs` обёртка** — matcher `*`, читает event, зовёт `floorDecide`, fail-CLOSED. **Критерий:** не импортирует `plan-lock` (Δ9 floor-первее-плана); регистрируется ОТДЕЛЬНО от supreme-gate.
- **2.4 `migrate:fresh` из whitelist → floor** — убрать `migrate:fresh|migrate:refresh|migrate:reset` из router-gate:94 whitelist (оставить `migrate`/`migrate:rollback`); floor их ловит. **Критерий:** router-gate больше не whitelist'ит fresh/reset/refresh; floor блокирует.
- **2.5 Δ7 defense-in-depth М2** — в `enforce-supreme-gate.decide` allow-путь: при `classifyDestructive(actionOf(toolUse).object).floor === true` НЕ продвигать указатель (вернуть block с пояснением «разрушительное in-plan — нужна дверь владельца»). **Критерий:** даже при незарегистрированном floor-хуке стена не благословляет снос.

**Линза-фокус:** атака (обход пола цепочкой/обфускацией/подделкой ownerApproval).
**Приёмка пакета:** floor блокирует 5 классов из §3 независимо от плана; дверь владельца работает только на подписанное одобрение; регрессия зелёная.

---

## Пакет 3 — Блок 2: подписант (floor-signer + M5_GREEN + occurrence), Δ4 честно

**Задачи:**
- **3.1 `RECEIPT_DOMAINS.M5_GREEN`** — добавить домен в `receipt-sign.mjs:14`. **Критерий:** подпись M5_GREEN не принимается за другой домен (тест кросс-домен).
- **3.2 `floor-signer.mjs`** — `signGreen({criterion_id, code_fingerprint, occurrence})` подписывает в домене `M5_GREEN` ключом подписанта; `occurrence` монотонный (один зелёный нельзя пере-зачесть на два критерия). **Критерий:** один и тот же green с разным `criterion_id` без нового occurrence → отклоняется.
- **3.3 Δ4 честная документация в коде** — JSDoc-шапка `floor-signer.mjs`: «подписант = анти-казуальная утечка + шов под HSM, НЕ защита ключа от контроллера (читается через `node tools/X.mjs`, §7)». **Критерий:** шапка явная; занижение претензий зафиксировано.

**Линза-фокус:** атака (можно ли подделать GREEN без ключа подписанта → нет; можно ли пере-зачесть occurrence → нет).
**Приёмка:** GREEN подписывается только подписантом; occurrence-защита работает; претензии Блока 2 honest.

---

## Пакет 4 — Блок 2 RISK: запрет env-fallback + сужение node-whitelist (Δ4)

> **Самый рискованный пакет.** Доп-скил `verify` — живой прогон тулчейна после правки.

**Задачи:**
- **4.1 Запрет env-fallback** — `receipt-key-config.mjs:28` + `judge-key-config.mjs:29`: убрать `envKey`-ветку (только keychain). **Критерий:** ключ из `ROUTER_MENTOR_*_KEY` env больше не читается; тест «env задан, keychain пуст → null».
- **4.2 Сужение node-whitelist** — `enforce-router-gate.mjs:90`: `node <любой-файл>` → allowlist известных проектных скриптов (`tools/*.mjs` своих гейтов/тестов) ИЛИ через план. **Критерий:** `node tools/<known>.mjs` проходит; `node <чужой-путь>.mjs` блок. NB §7: это анти-казуальная мера, НЕ полный close.
- **4.3 `verify` живой прогон** — реально запустить: тест-раннер (`node vitest.mjs ...`), сам `enforce-router-gate` на типовых командах, post-commit хуки. **Критерий:** тулчейн не сломан; если сломан — точечный откат 4.2, переоценка allowlist.

**Линза-фокус:** verify (живой) + атака (остаётся ли путь чтения ключа — да, фиксируем как §7-остаток).
**Приёмка:** env-канал закрыт; node сужен без поломки тулчейна (доказано живым прогоном); остаток честно записан.

---

## Пакет 5 — Блок 3: критерий-носитель (Δ2 fingerprint / Δ5 / Δ6 лесенка)

**Задачи:**
- **5.1 sealed `criterion_id` в `plan-lock.freezePlan`** — каждому значимому шагу детерминированный `criterion_id` из запечатанного содержимого. **Критерий:** id воспроизводим из sealed plan; нельзя выдумать на демо-этапе.
- **5.2 Значимость механически (C3/F18)** — заменить контроллер-флаги `trivial`/`significant` в `k5CriterionCheck` (judge-gate-floor.mjs:40) на вывод из `classifyDestructive`/класса действия: мутирующий шаг → значим → обязан нести критерий; пустой → fail-CLOSE. **Критерий:** контроллер не может обнулить K5 флагом.
- **5.3 Δ2 fingerprint в `criteriaGreenMatched`** — green засчитывается только при совпадении `code_fingerprint` с текущим (изменённые файлы шага + тесты). **Критерий:** правка файла после прогона → green аннулирован.
- **5.4 Δ5 подпись-подписанта в сверке** — `criteriaGreenMatched` требует И id ∈ sealed И валидную подпись подписанта (не одно совпадение id). **Критерий:** green с верным id но без подписи → не засчитан.
- **5.5 Δ6 — 4 шага в лесенку** — зарегистрировать `criteriaFromSealedPlan` ∧ `criteriaGreenMatched` ∧ fingerprint ∧ signer-sig как floorSteps `runGateLadder` критерий-гейта + структурный тест «ровно эти 4 шага». **Критерий:** забыть один шаг невозможно (тест ловит).
- **5.6 `criterion-green.mjs` производитель** — эмитит `{criterion_id, green, code_fingerprint, coverage_of_changed}`, подписан подписантом (Пакет 3); мутация P18 как условие настоящего green. **Критерий:** green только если мутация подтвердила, что тест реально проверяет.

**Линза-фокус:** корректность (4-шаговый пол, fingerprint-свежесть, id≠auth).
**Приёмка:** GREEN неподделываем без подписанта; правка после прогона аннулирует; значимость не обнуляется флагом.

---

## Пакет 6 — Блок 5: сквозные инварианты (N5/Δ9) + манифест-WARN (Δ8)

**Задачи:**
- **6.1 N5 «строгая проверка пола»** — структурный тест в `m5-floor-invariants.test.mjs`: ни один потребитель пола не пишет `if (result.ok)` (truthy), только `=== true`. **Критерий:** регресс паттерна падает в CI.
- **6.2 Δ9(а) floor-суперсет** — тест: удаление строки из floor-набора `classify-destructive.mjs` без ADR падает в CI (snapshot floor-набора + сверка). **Критерий:** усыхание пола ловится.
- **6.3 Δ9(б)** — уже в Пакете 1 (Step 1.8), подтвердить в этом файле.
- **6.4 escape≠protection** — тест: защитный список enforce'ится только fail-close хуками БЕЗ override-вокабуляра; правка, переносящая защиту под override, падает. **Критерий:** перенос защиты под override ловится.
- **6.5 Δ8 манифест-WARN** — `floor-manifest-check.mjs` (SessionStart): читает settings.json, проверяет регистрацию пол-хука + защитных; не зарегистрированы → громкий WARN (не block, проблема черепах). **Критерий:** WARN при отсутствии регистрации; не блокирует сессию.

**Линза-фокус:** корректность.
**Приёмка:** 4 CI-инварианта зелёные; манифест кричит при пустом посте.

---

## Пакет 7 — Блок 4: 7 заплаток существующих хуков

> Каждая — точечный TDD-патч (ядро+обёртка). Бит-сайз just-in-time.

- **7.1 `read-path-deny` +контент-скан** секрет-паттернов (токен/ключ/PEM/строка подключения) на каналах чтения; путь-деналист = грубый пре-фильтр; fail-close.
- **7.2 `normative-content` детерм-only + fail-CLOSE** для защищаемой нормативки (CLAUDE.md/memory/Pravila): LLM-деградация → fail-CLOSE; «память = совет, НИКОГДА не разрешение».
- **7.3 `mcp-classification` +egress-скан** (секрет-паттерны+размер; SQL/URL при неуверенном разборе → fail-close; блок `data:`/`file:`/IP-литерал).
- **7.4 `workflow-gate` inline-script + register** — inline-`script` гейтить как `scriptPath` (хеш+одобрение+скан); `DANGEROUS_PATTERNS` +`keytar`/`getPasswordSync`/имена keychain-сервисов.
- **7.5 `parallel-session-lock` pid-liveness + register** — проверка «жив ли держатель» перед перехватом.
- **7.6 `tdd` мутация + покрытие изменённого** — мутация вшита в по-критерийный GREEN (Пакет 5); `tdd-gate` остаётся дисциплиной (fail-open+override), мутация/GREEN-пол — защита (без override) — в разных хуках.
- **7.7** — неподделываемый GREEN/RED закрыт Пакетами 3+5.

**Линза-фокус:** все три (заплатки касаются exfil-каналов).
**Приёмка:** 6 заплаток зелёные; разъезд токенайзера с полом исключён (общий `tokenizeBash`).

---

## Завершение

- Полная регрессия `regression full` (Pest --parallel + Larastan + Vitest + Vite + lychee + gitleaks) — НЕ требуется (М5 = tools-only); достаточно tools-only vitest `2560+ → новый baseline GREEN` + lefthook-гейты.
- `finishing-a-development-branch` — решение merge/PR **под «пуш» владельца** (commit-not-push).
- **Активация владельцем (НЕ код):** регистрация `enforce-floor` + защитных хуков в `.claude/settings.json`; провижининг ключа подписанта вне доступного контроллеру окружения; сперва тихий режим (D28) на 1 сессию, затем hard-block.
- Память (`direct:memory-sync`) — отдельным ходом в конце эпика.

---

## Self-review плана (против спека)

- **Покрытие §3:** Блок 1 → Пакет 2; Блок 2 → Пакеты 3+4; Блок 3 → Пакет 5; Блок 4 → Пакет 7; Блок 5 → Пакет 6. §4 консолидация → Пакет 1. ✅
- **Покрытие §10 амендмента:** Δ1→2.2, Δ2→5.3, Δ3→reconcile (вынесен в Пакет 6 escape≠protection + журнал — **GAP**, см. ниже), Δ4→Пакеты 3+4, Δ5→5.4, Δ6→5.5, Δ7→2.5, Δ8→6.5, Δ9→6.2/6.4/1.8, N1→Пакет 1, N5→6.1.
- **GAP найден:** Δ3 (честный двухтакт reconcile: пред-запись намерения ДО allow + PostToolUse-сверка) НЕ имеет своей задачи. **Добавить Пакет 8 (или задачу в Пакет 2):** перенести `journalAppend` в `runGate` ПЕРЕД allow + новый PostToolUse-реконсилер. Зафиксировано как Пакет 8 ниже.
- **Плейсхолдеры:** Пакеты 2-7 на уровне задач — осознанно (бит-сайз код RED-first, пред-написание = призрак). Пакет 1 полностью бит-сайз. ✅
- **Консистентность типов:** `classifyDestructive → {floor, suspicious, reason}` используется единообразно (Пакет 1/2/5). ✅

## Пакет 8 — Δ3: честный двухтакт reconcile

- **8.1 Пред-запись намерения** — `enforce-supreme-gate.runGate`: `journalAppend` намерения ПЕРЕД `saveStep`/allow (нет пред-записи → block). **Критерий:** действие без журнальной пред-записи не разрешается.
- **8.2 PostToolUse-реконсилер** — новый хук: сверяет, что реально произошло ровно записанное; расхождение → WARN/флаг. **Критерий:** post-сверка ловит «действие без записи» и «запись без действия».
- Формулировку W3 в спеке (§5) обновить с «атомарно на исполнении» на двухтактную (уже сделано в §10 Δ3).
