# Машина 6 — «Аварийный выход + авто-снимок» — дизайн

**Дата:** 2026-06-07 · **Статус:** утверждён к реализации (brainstorming) · **Ветка:** `worktree-brainrepo`
**Кодовая фраза эпика:** «роутер-наставник».
**Предшественник:** Машина 5 (несущий пол — вето-до-плана) собрана, переаудичена, закоммичена (HEAD `849723bc`+handoff, baseline tools-only **2789 passed + 2 skip**). М6 — **новая фаза дизайна**, не продолжение плана М5. Опирается на шов М5 (spec `2026-06-07-router-mentor-machine-5-floor-design.md` §5).

**Ревизия после факт-аудита кода (2026-06-07):** дизайн прогнан тремя линзами — `audit-context-building` (сверка утверждений с реальными модулями М5), `sharp-edges` (footgun/лазейки), `agentic-actions-auditor` (рассмотрена и признана неприменимой — она про CI-workflow AI-агентов, не про локальный hook-escape; агентная безопасность escape покрыта первыми двумя). Внесены правки F1–F4 / F-S1 / F-S2 / I1–I4 (см. §11).

---

## 1. Зачем (что закрывает М6)

М5 поставила **жёсткую стену-пол**: классифицирует суть действия и ветирует необратимое **до** проверки членства в плане — даже при мёртвом судье. У жёсткой стены два эксплуатационных провала:

1. **Стена может «закирпичить» легитимную срочную работу.** Нынешняя «дверь владельца» (`floor-decide.approvalOpen`) слабая, **узкая (только Bash-ветка)** и **частично мёртвая** (см. §4 F4); нужен полноценный, неподделываемый **аварийный выход (escape)** на один конкретный заблокированный шаг.
2. **У разрушительных действий, которые пол пропустил (штатно или через escape), нет отмены.** Нужна автоматическая **точка возврата (авто-снимок)**.

М6 достраивает **безопасность самого пола**: законная дверь + кнопка отмены. Остальное из §6 спека М5 (портативный канал правок нормативки / растворение старого зоопарка хуков / доска видимости) — **остаётся в М7**.

---

## 2. Решения (зафиксированы с владельцем, brainstorming 2026-06-07)

| # | Развилка | Решение |
|---|---|---|
| Р-М6-1 | Декомпозиция §6 | **A — «безопасность пола»**: М6 = escape + авто-снимок. М7 = портативный normative-канал + растворение зоопарка + доска видимости. |
| Р-М6-2 | Механизм escape | **A — всплывающий вопрос** (AskUserQuestion side-channel): пол блокирует → контроллер обязан спросить владельца → выбор владельца пишет защищённый **одноразовый** пропуск (пишет среда/парсер, не контроллер). Тот же приём, что закрыл одобрение git-операций (S8 М5). |
| Р-М6-3 | Класс пола под escape | **B — escape на весь floor-набор** (один класс). floor-набор разнесён на 3 локуса (см. §4): Bash-команды + Write-ветка (runtime/.env/секрет) + egress. escape покрывает все три. Принятый остаточный риск соц-инженерии снижается показом сырого действия + точным совпадением (см. §4, §8). |
| Р-М6-4 | Содержание снимка | **A — git-состояние** (точка возврата git-репо: код/доки/нормативка/память). Файлы вне git (B) и дамп БД (C) — не входят. |
| Р-М6-5 | Структурный подход | **A — escape в ядро пола + отдельный снимок-хук**: чистые ядра + тонкие обёртки + TDD, как М1–М5. Отклонены B (всё одним хуком) и C (минимум). |

---

## 3. Архитектура — два блока

Подход A: escape встроен в ядро пола (это его разрешающий путь во **всех** ветках), авто-снимок — изолированный хук.

| Модуль | Тип | Назначение |
|---|---|---|
| `tools/escape-grant.mjs` | новое, чистое ядро | читает/валидирует **одноразовый** escape-пропуск `kind:"floor_escape"`: точное совпадение канонической строки действия + свежесть (окно + нижняя граница времени) + один-раз + сессия. **Отдельный reader**, не пересекается с `approve_git_operation`. |
| `tools/floor-decide.mjs` | **правка существующего** | заменить `approvalOpen`-дверь на escape-проверку через `escape-grant` **в обеих ветках** (Bash-floor И Write-floor). |
| `tools/enforce-floor.mjs` | **правка существующего** | подгрузить escape-пропуски (как сейчас `loadApprovedGitOps`) и передать в `floorDecide`. |
| `tools/askuser-answer-parser.mjs` | **правка существующего (§4.5 ядро)** | +`toFloorEscapeRecord(answer)` — построить запись `kind:"floor_escape"` с канонической строкой действия (переиспользует `normalizeCommand`/`buildApprovalRecord`/опц. `signApprovalRecord`). |
| `tools/enforce-askuser-answer-parser.mjs` | **правка существующего (обёртка PostToolUse)** | писать `floor_escape`-пропуск в `~/.claude/runtime/askuser-decisions-<sess>.jsonl` (fail-open, как сейчас approve_git_operation). |
| `tools/enforce-mcp-classification.mjs` | **правка существующего** | escape-проверка для egress-floor (третий локус, Р-М6-3=B). |
| `tools/floor-escape-consume.mjs` | новое, чистое ядро | одноразовое погашение: по исполненному действию + грантам/consumed → отметка-погашение. |
| `tools/enforce-floor-escape-consume.mjs` | новое, обёртка-хук (PostToolUse) | после **реального исполнения** floor-действия пишет отметку-погашение (one-shot); снимок-fail (PreToolUse-блок) → не исполнилось → пропуск не гасит. |
| `tools/enforce-supreme-gate.mjs` | **правка существующего (М2)** | **G-1 сквозной escape:** при свежем совпавшем floor_escape → allow (Δ7-разрушительное in-plan И мутатор в разговорном режиме), **без** продвижения указателя плана. Без этого стена М2 затеняет escape (оба гейта PreToolUse `*` — блок любого = блок). |
| `tools/snapshot-decide.mjs` | новое, чистое ядро | по `classifyDestructive` решает: нужен ли снимок + его описание; различает «чистое дерево» и «ошибка git». |
| `tools/enforce-snapshot.mjs` | новое, обёртка-хук (PreToolUse) | делает git-точку возврата ДО разрушительного действия, пишет журнал восстановления; fail-close на ошибке git (не на чистом дереве). |

**Порядок хуков (PreToolUse):** `enforce-floor` (вердикт) → **затем** `enforce-snapshot` (точка возврата для пропущенного). Снимок регистрируется после пола.

**Принцип-инвариант:** контроллер-ИИ пробить пол не может никогда; escape — только через реальный клик владельца во всплывающем вопросе, пропуск пишет среда (обёртка PostToolUse), не контроллер. Снимок и escape — изолированные модули (чистое ядро + тонкая обёртка), тестируются раздельно.

---

## 4. Блок 1 — Аварийный выход (escape)

**Заменяет** нынешнюю «дверь владельца» `floor-decide.approvalOpen`.

**floor-набор разнесён на 3 локуса (факт кода) — escape покрывает все (Р-М6-3=B):**
- **Bash-команды** — `classify-destructive.FLOOR_RE` (force-push / `reset --hard` / `migrate:fresh|reset|refresh` / `db:wipe` / `rm -rf`). Сейчас дверь `approvalOpen` есть только здесь.
- **Write/Edit-ветка** — запись в `~/.claude/runtime` / `.env` / ключ-cert (`floor-decide` строки 112-121). **Двери нет вообще** — блок безусловный. escape М6 добавляет её сюда.
- **Egress** — необратимая отправка наружу (`enforce-mcp-classification.scanEgress`). escape М6 добавляет проверку и сюда.

**F4 (пре-существующий разрыв, закрывается дизайном):** нынешний писатель `toApprovalRecord` — **git-only** (ловит только git-глаголы). Поэтому `migrate:fresh`/`db:wipe`/`.env` через дверь одобрить **нельзя** (дверь для них мёртвая, хотя `floor-decide:25` заявляет обратное). Новый `floor_escape`-грант покрывает не-git floor-действия — разрыв закрыт.

**Поток (пример `git push --force`):**

1. Контроллер пытается выполнить действие.
2. `enforce-floor`: совпадение с floor-набором → проверка escape-пропуска. Пропуска нет → **блок**; сообщение контроллеру: «заблокировано полом; чтобы выполнить — спроси владельца всплывающим вопросом, показав точное действие `<каноническая строка действия>`».
3. Контроллер вызывает AskUserQuestion. Вариант, который выберет владелец, **должен содержать каноническую строку действия** (буквальную команду/путь/цель).
4. Владелец жмёт «разрешить». §4.5 парсер (среда, не контроллер) распознаёт escape-одобрение, строит из выбранного варианта `floor_escape`-запись с **канонической строкой действия** и пишет её в защищённый `askuser-decisions-<sess>.jsonl`.
5. Контроллер повторяет действие. `escape-grant` берёт каноническую строку **живого** действия и ищет свежий **непогашенный** `floor_escape`-пропуск с **точно совпадающей** строкой → **разрешить + погасить** (отметка в защищённом канале).

**Сердце надёжности — точное совпадение канонической строки (binding).** Привязка = **точное совпадение нормализованной строки действия** (реальный механизм М5: `normalizeCommand`/`matchesApproval`/floor `normCmd`), **не** хеш над `classifyDestructive` (тот возвращает только булевы). Каноническая строка:
- Bash-floor → `normalizeCommand(command)`;
- Write-floor → `${tool}:${pathNormalize(path)}`;
- egress-floor → канонизированная цель отправки.

Пол строит строку из **живого** `tool_input`, парсер — из **показанного владельцу** варианта. Контроллер показал одно, исполняет другое → строки **не совпадут** → блок. Получить «да» на одно и подсунуть другое нельзя.

**F-S1 (sharp-edges, критично) — одноразовость и отдельный тип.** Пропуск — **отдельного `kind:"floor_escape"`** с **отдельным reader**, НЕ переиспользование `approve_git_operation` (то — переиспользуемое 5-мин окно, `approvalOpen` лишь проверяет наличие, не гасит). Иначе одно «да» = неограниченное число разрушительных за окно + git-одобрение и floor-escape перекрёстно удовлетворяли бы друг друга. floor_escape **гасится после первого применения** (отметка-консум в защищённом канале); это **строже** нынешней git-двери (осознанно). Погашение выполняет **отдельный PostToolUse-консьюмер** (`floor-escape-consume` + обёртка) — строго **после реального исполнения**, поэтому сбой снимка (PreToolUse-блок) пропуск не тратит (§5, «escape-пропуск при сбое снимка не сгорает»).

**Свежесть пропуска (три замка):** одноразовый (погашен → недействителен); короткое окно (≈5 мин, как у одобрения git); нижняя граница `now - ts >= 0` (приём F-6 М5 — пропуск «из будущего» не свеж). `unknown`-сессия → пропуск недействителен.

**fail-CLOSE** на любую свою ошибку (нет канала / битый пропуск / двусмысленность) → блок.

**G-1 (сквозная М2↔М5) — escape это СКВОЗНОЙ override владельца, не только дверь пола.** Верховная стена `enforce-supreme-gate` (М2, matcher `*`) **независимо** блокирует разрушительное in-plan (Δ7, `enforce-supreme-gate.mjs:157`) и любой мутатор в разговорном режиме. Оба хука PreToolUse → блок ЛЮБОГО = блок действия. Значит floor_escape, открывающий только пол, **не работает сквозь стену**. Поэтому floor_escape консультируется **каждым защитным гейтом**: стеной М2 (allow без продвижения указателя — escape это out-of-band действие владельца, не шаг плана), полом М5 (Bash+Write), egress. Каноническая строка egress (`mcp:<tool>:<JSON.stringify(input)>`) детерминирована для идентичного входа (порядок ключей стабилен) — **G-8**.

**Замена «двери».** `floor-decide.approvalOpen` (git-only, только Bash) убираем; на его месте — `escape-grant` во всех ветках. Правка `floor-decide`/`enforce-floor`/`askuser-answer-parser`/`enforce-askuser-answer-parser`/`enforce-mcp-classification`/**`enforce-supreme-gate` (G-1)** → на этапе стройки **обязателен** `audit-context-building` перед каждой. Блок-сообщение пола обязано выводить **точную** каноническую строку (токен `FLOOR-ESCAPE: <canonicalAction>`), чтобы контроллер скопировал её во всплывающий вопрос дословно (**G-5**). `floor-decide`/`enforce-floor` не должны содержать подстроку `override` — инвариант escape≠protection `m5-floor-invariants.test.mjs` (**G-6**).

**Принятый остаточный риск (Р-М6-3 = B).** escape покрывает весь floor-набор, включая отправку секретов/ПДн и запись в runtime. Теоретически владельца можно уговорить одобрить опасное, обрамив «безобидно». Снижение: владелец видит **буквальную каноническую строку** (команда/путь/цель), а не пересказ. Полное устранение соц-инженерии невозможно — часть «~0.5% неустранимого предела» (§8). Боковых дверей мимо binding/парсера не делаем.

---

## 5. Блок 2 — Авто-снимок (точка возврата)

**Когда срабатывает.** Перед действием, которое `classifyDestructive(command).floor` пометил и которое пол **пропустил** (штатно или через escape). `enforce-snapshot` стоит после `enforce-floor`. Edit/Write обычные действия снимок не триггерят (они git-обратимы и не «разрушительны» по классификатору).

**I2 (честный scope-уточнение).** git-снимок реально откатывает **локально-разрушительное** — прежде всего `reset --hard` и `rm -rf` отслеживаемых файлов. Для `force-push` (меняет remote) и `migrate:fresh` (меняет БД) git-снимок имеет **ограниченную** ценность отката, но безвреден (дешёвая ссылка). Не оверселлим: снимок — страховка локального дерева, не универсальная отмена.

**Как делает точку возврата (без помех рабочему дереву):** захватывает текущее git-состояние через `git stash create` (commit-объект с изменениями, **не** меняя файлы/индекс) и закрепляет под `refs/floor-snapshots/<уникальный-id>` (защита от git-gc).

**F-S2 (sharp-edges, средне) — чистое дерево ≠ ошибка.** Если дерево чисто, `git stash create` ничего не выдаёт — это **успех**, точка возврата = текущий `HEAD` (ref на HEAD). **fail-close только на настоящую ошибку git** (не репозиторий / git недоступен / сбой команды). Иначе разрушительное на чистом дереве блокировалось бы спуриозно.

**Что покрывает (Р-М6-4 = A):** git-отслеживаемое — код, доки, нормативка, память. Неотслеживаемые файлы вне git и БД — не входят.

**Журнал восстановления.** Каждая точка → запись в защищённый `~/.claude/runtime/restore-points.jsonl`: `{ts, action, head, snapshot_ref, restore_command}`. Восстановление — **ручное** по готовой команде из журнала (М6 сам не откатывает — владелец решает).

**I3 (ясность, не противоречие).** Снимок и парсер пишут в `~/.claude/runtime` как **процессы-хуки** (легитимно), а не как Write-инструмент контроллера (его пол блокирует) — это разные акторы, как уже у писателя `approve_git_operation`.

**Сбой снимка → fail-CLOSE.** Разрушительное действие без точки возврата (при реальной ошибке git) **не пропускаем** (блок). **escape-пропуск при этом не сгорает** (гасится только на пути, реально дошедшем до выполнения) → повторно одобрять из-за git-заминки не нужно.

**Чистка.** Хук подрезает старые точки (последние ~30 или 14 дней) — дешёвые git-ссылки.

---

## 6. Шов М5↔М6 (что потребляем)

М6 **потребляет** из М5 (spec М5 §5):
- `classifyDestructive(command) → {floor, suspicious}` — floor-предикат для Bash + триггер снимка.
- floor-набор Write-ветки/egress — `floor-decide` (RUNTIME_RE/SECRET_PATH_RE) + `enforce-mcp-classification.scanEgress`.
- `normalizeCommand` / `matchesApproval` / `buildApprovalRecord` / `signApprovalRecord` из `askuser-answer-parser.mjs` (**I1** — переиспользуем, не пишем новую крипту).
- `pathNormalize` (каноническая строка Write-floor).
- Паттерн защищённого side-channel `~/.claude/runtime/**` + «обёртка PostToolUse пишет, контроллер не может» (тот же приём, что у `approve_git_operation`, но **отдельный `kind`**).

**Не нужны:** `floor-signer` (подписант) и API критерия — escape работает через side-channel всплывающего вопроса. (Опц. усиление: подписать `floor_escape`-грант через `signApprovalRecord` — `RECEIPT_DOMAINS.APPROVAL` уже есть; не обязательно, базовая интегрити — protected-path.)

---

## 7. Что НЕ входит (YAGNI → М7)

Портативный normative-канал правок «мозга»; растворение старого зоопарка дисциплинарных хуков (router-gate / branch-switch / verify-before-push / script-watcher / tdd-gate / decomposition / self-debrief / rationalization / coverage); доска видимости «кто на посту».

---

## 8. Остаточные пределы (честно)

~0.5% неустраним: (1) принятый риск соц-инженерии escape (Р-М6-3 = B) — владельца теоретически можно уговорить одобрить опасное, обрамив «безобидно»; снижается показом сырой канонической строки, но не до нуля; (2) абсолют изоляции ключа — только на железе (HSM/YubiKey, v4.2-фаза). М6 закрывает «жёсткая стена без двери и без отмены», но не делает абсолют.

---

## 9. Инвентарь модулей (для writing-plans)

**Новое:**
- `tools/escape-grant.mjs` (чистое ядро: чтение/валидация `floor_escape`-пропуска — точная строка + свежесть + один-раз/консум + сессия; отдельный reader).
- `tools/floor-escape-consume.mjs` (чистое ядро: `consumeDecision` — отметка-погашение по исполненному действию).
- `tools/enforce-floor-escape-consume.mjs` (обёртка-хук PostToolUse: запись отметки-погашения после исполнения; fail-open).
- `tools/snapshot-decide.mjs` (чистое ядро: нужен ли снимок + описание; чистое-дерево vs ошибка).
- `tools/enforce-snapshot.mjs` (обёртка-хук PreToolUse: git-точка возврата + журнал; fail-close на ошибке git).

**Патчи существующих (→ обязателен `audit-context-building` перед каждой правкой):**
- `tools/floor-decide.mjs` — убрать `approvalOpen` → escape-проверка через `escape-grant` в **Bash- и Write-ветках**.
- `tools/enforce-floor.mjs` — подгрузка escape-пропусков и проброс в `floorDecide`.
- `tools/askuser-answer-parser.mjs` — `toFloorEscapeRecord(answer)` (новый, `kind:"floor_escape"`, переиспользует `normalizeCommand`/`buildApprovalRecord`).
- `tools/enforce-askuser-answer-parser.mjs` — писать `floor_escape`-записи в `askuser-decisions-<sess>.jsonl` (fail-open).
- `tools/enforce-mcp-classification.mjs` — escape-проверка для egress-floor (третий локус).
- `tools/enforce-supreme-gate.mjs` (**G-1, М2**) — `decideMode` honors floor_escape: при свежем совпавшем пропуске → allow (`mode:'escape'`) **без** `advanceTo` (указатель не двигается, не журналируется); `runGate`/`main` грузят `loadFloorEscapes`/`loadConsumed`. Без этого стена затеняет escape. (NB: `enforce-supreme-gate` НЕ в override-списке `m5-floor-invariants`, термин escape допустим.)

**Тесты:** `m6-escape.test.mjs`, `m6-snapshot.test.mjs`.

**Активация (шаг владельца, НЕ код):** регистрация `enforce-snapshot` (PreToolUse, ПОСЛЕ `enforce-floor`) + `enforce-floor-escape-consume` (PostToolUse) в `.claude/settings.json` (парсер `enforce-askuser-answer-parser` уже зарегистрирован); сперва тихий режим, затем hard-block. До регистрации новые хуки инертны.

---

## 10. Тестирование

TDD по модулям (RED→GREEN, реальный `expect`):
- **escape (binding/свежесть):** пропуск с точно совпавшей канонической строкой → разрешить+погасить; несовпавшая строка → блок; устаревший / **уже погашенный (one-shot)** / «из будущего» пропуск → блок; `unknown`-сессия → блок; ошибка/нет канала → fail-close.
- **escape (покрытие всех веток, Р-М6-3=B):** floor_escape открывает Bash-floor (`reset --hard`); Write-floor (`.env`/runtime-путь); egress. F4: `migrate:fresh`/`db:wipe` теперь одобряемы (раньше — нет).
- **escape (F-S1 разделение каналов):** `approve_git_operation`-запись НЕ удовлетворяет floor-escape и наоборот (разные `kind`/reader).
- **снимок:** разрушительное (`reset --hard`) → точка возврата создана (ref `refs/floor-snapshots/*`, рабочее дерево не изменено); **F-S2:** чистое дерево → ref=HEAD (успех, не блок); реальная ошибка git → fail-close (блок); неразрушительное → снимка нет; при сбое снимка escape-пропуск не сгорел.
- **интеграция:** порядок `enforce-floor` → `enforce-snapshot`; escaped `reset --hard` создаёт точку возврата до выполнения.

Регрессия tools-only зелёная, планка ≥ **2789 passed + 2 skip** (baseline М5).

---

## 11. Журнал факт-аудита (2026-06-07)

Дизайн прогнан `audit-context-building` (сверка с реальным кодом М5) + `sharp-edges`. `agentic-actions-auditor` рассмотрен и **неприменим** (CI-workflow scope, не локальный hook) — не применялся, чтобы не плодить «линзу вслепую».

| # | Находка | Правка |
|---|---|---|
| F1 | Спек называл `enforce-branch-switch` «§4.5 парсером, пишущим пропуск» — неверно (это branch-detection хук). | Настоящий парсер — `askuser-answer-parser.mjs` + `enforce-askuser-answer-parser.mjs`; исправлены §3/§9. |
| F2 | floor-набор разнесён на 3 локуса; дверь покрывала только Bash → «escape на всё» (B) не обеспечивался. | §4 явно перечисляет 3 локуса; escape в Bash+Write+egress (§3/§9). |
| F3 | «отпечаток над `classifyDestructive`» — тот даёт только булевы. | binding = точное совпадение **канонической строки** (`normalizeCommand` / `tool:pathNormalize` / egress-цель), §4. |
| F4 | `toApprovalRecord` git-only → migrate:fresh/db:wipe/.env не одобряемы (мёртвая дверь). | `floor_escape`-грант покрывает не-git floor; §4 + тест §10. |
| F-S1 | escape как переиспользование `approve_git_operation` (5-мин окно, не one-shot, перекрёстное удовлетворение) — дыра. | Отдельный `kind:"floor_escape"` + отдельный reader + one-shot консум; §3/§4 + тест §10. |
| F-S2 | Снимок fail-close не различал «чистое дерево» и «ошибку git». | Чистое дерево → ref=HEAD (успех); fail-close только на реальную ошибку; §5 + тест §10. |
| I1 | Не указано переиспользование helper'ов. | §6 — `normalizeCommand`/`matchesApproval`/`buildApprovalRecord`/`signApprovalRecord`. |
| I2 | Снимок оверселл («покрывает реальный риск»). | §5 — честно: ценен для `reset --hard`/`rm -rf`; force-push/migrate ограниченно. |
| I3 | Кажущееся противоречие: пол блокирует runtime-запись, а снимок туда пишет. | §5 — снимок/парсер пишут как процессы-хуки, не как Write контроллера. |
| I4 | — | §«Ревизия» + §11 — `agentic-actions-auditor` зафиксирован как неприменимый. |

**Поправка план→спек (2026-06-07):** при написании плана реализации выяснилось, что строгая одноразовость «погашение **после реального исполнения**» (§4 F-S1) требует отдельного PostToolUse-консьюмера. Добавлены модули `floor-escape-consume.mjs` (ядро) + `enforce-floor-escape-consume.mjs` (обёртка) в §3/§9; механизм синхронизирован с планом `docs/superpowers/plans/2026-06-07-router-mentor-machine-6.md` (Пакет 5).

**Аудит плана (2026-06-07, audit-context-building сквозь М1–М5 + sharp-edges):**

| # | Находка | Правка |
|---|---|---|
| G-1 | КРИТ. сквозная: стена М2 (`enforce-supreme-gate` Δ7 + разговорный режим) блокирует разрушительное/мутаторы независимо от пола → floor_escape (только пол) был **no-op сквозь стену**. | **Вариант α (владелец):** escape — сквозной override, чтимый стеной М2 (allow без продвижения указателя), полом, egress. +патч `enforce-supreme-gate` в §3/§9; план Пакет 4b. |
| G-2 | Пакет 4 расплывчато «удалить тесты двери» — смена сигнатуры `floorDecide` краснит блок Δ1 (5 тестов) `floor-decide.test.mjs`. | План Пакет 4: конкретно переписать 5 кейсов двери в escape-форму. |
| G-5 | Привязка требует точного токена в варианте AskUser. | Блок-сообщение пола выводит точный `canonicalAction` (§4). |
| G-6 | `m5-floor-invariants` escape≠protection падает на подстроке `override` в floor-файлах. | Запрет термина `override` в `floor-decide`/`enforce-floor` (§4, план). |
| G-8 | Канон egress на `JSON.stringify`. | Отмечено как детерминированное для идентичного входа (§4). |
| ✅ | Экспорты (`classifyMcpTool`/`pathNormalize`/`bashIsFloor`/`normalizeCommand`) совпадают; М1 (`signApprovalRecord` домен APPROVAL), М3/М4 (`classifyDestructive` только чтение), общий канал `askuser-decisions` фильтр по `type` — без поломок. | — |
