docs(m7): fold plan-critique findings P-1..P-8 into plan + spec

Independent critical review of the implementation plan (audit-context ->
sharp-edges -> variant-analysis -> systematic-debugging, grounded against
real M1-M6 code). 8 findings, all code-grounded, no ghosts:

- P-1 (CRIT): content-floor "full port" was a SUBSET of BASH_HARD_BLACKLIST
  (lost #21 env-prefix / #22 --watch / #34 echo-inj / C16 2>file /
  cp-mv-chmod-chown-chgrp; cp X ~/.claude/runtime = escape-grant forge)
  -> single source matchBashHardBlacklist relocated to shell-content-rules
  (Task 1.0.5) + §12 content-floor invariant proves completeness.
- P-2 (CRIT): canonicalAction has no PowerShell branch -> every PS command
  collapses to 'write:' -> one escape unlocks ALL PS commands; Task 1.5 test
  was spuriously green (both sides equally broken) -> Task 1.2b + specificity.
- P-3 (HIGH): PS floor branch returned block:false skipping runtime/secret
  guard (command field not parsed) -> Set-Content ~/.claude/runtime forge
  -> psProtectedWrite guard (Task 1.4).
- P-4 (MED): content-block whole-string only -> bashIsContentBlock whole+per
  -segment parity with bashIsFloor (Task 1.3).
- P-5 (MED): suspicious blind to content-danger -> suspicious |= contentBlock.
- P-6 (MED): §12 CI-invariants ownerless -> assigned per phase (phase rule).
- P-7 (LOW): Phase 0 fail-CLOSE "subset" unlisted -> explicit hook list.
- P-8 (LOW): plan = detailed Phase 1 + scoped skeleton -> honest framing.

Plan: Tasks 1.0.5/1.1/1.2b/1.3/1.4/1.5/1.6 + phase-transition rule + self-review.
Spec: §5 PowerShell row, §12 M5 line, §13 addendum.
No code built. commit-not-push.
This commit is contained in:
Дмитрий
2026-06-08 08:20:40 +03:00
parent eac1c45bbb
commit 475d381e0c
2 changed files with 153 additions and 49 deletions
@@ -47,6 +47,14 @@
**Правило перехода между фазами:** фаза закрыта ТОЛЬКО при зелёной регрессии 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-карты остальных.
---
@@ -55,25 +63,44 @@
**Зачем:** стена М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).
**Дизайн-решения (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 снимает блок в обеих ветках.
- Порт из `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 кейсы)
- 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` (content-block ветка Bash + PowerShell, escapable)
- Test: `tools/floor-decide.test.mjs` (+content-block + escape-bypass кейсы)
- 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), `tools/bash-tokenizer.mjs`, `tools/escape-grant.mjs`. Зафиксировать: текущую сигнатуру `classifyDestructive(command) → {floor, suspicious, reason}`; `floorDecide({toolUse, escapeGrants, escapeConsumed, now, normalizeImpl})`; что `bashIsFloor` уже посегментно ходит через tokenizeBash. Это контекст — без правок.
- [ ] **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)`.
---
@@ -96,6 +123,12 @@ describe('contentBlock (правило 8 §4.1, V1)', () => {
'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);
@@ -123,37 +156,18 @@ 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` дополнить выдачу:
В `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));
const suspicious = floor || SUSPICIOUS_RE.some((re) => re.test(cmd));
const contentBlock = CONTENT_BLOCK_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)'
@@ -162,6 +176,8 @@ export function classifyDestructive(command) {
}
```
> **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 кейсы зелёные).
@@ -249,6 +265,34 @@ feat(m7-floor): powershell-destructive psContentBlock (V1-PS)
---
### 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`
@@ -286,14 +330,27 @@ Expected: FAIL — `node -e` сейчас не floor → block:false (дыра V
В `tools/floor-decide.mjs` импорт + в ветке `if (name === 'Bash')` добавить content-block ДО проверки `bashIsFloor`:
```js
// импорт сверху:
// импорты сверху:
import { psContentBlock } from './powershell-destructive.mjs';
// classifyDestructive уже импортирован (bashIsFloor его зовёт).
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 (classifyDestructive(input.command || '').contentBlock) {
if (bashIsContentBlock(input.command || '')) {
if (escaped()) return { block: false, reason: 'floor: content-block снят аварийным выходом (floor_escape)' };
return { block: true, reason: `floor: опасная по содержанию команда без аварийного выхода — блок (правило 8); FLOOR-ESCAPE: ${action}` };
}
@@ -305,6 +362,8 @@ import { psContentBlock } from './powershell-destructive.mjs';
}
```
> **NB (P-4 тест):** добавить кейс «`echo "node -e foo" && cat x` НЕ over-блокируется (кавычки нейтрализованы), а `cat x && node -e "y"` блокируется (content-сегмент за читающим)» — доказывает per-segment паритет.
- [ ] **Step 4: Запустить — убедиться, что проходит**
Bash: та же команда. Expected: PASS.
@@ -346,11 +405,23 @@ Expected: FAIL — PowerShell сейчас уходит в write-path-ветку
- [ ] **Step 3: Реализовать минимально**
В `floor-decide.mjs` добавить ветку PowerShell ПОСЛЕ Bash-ветки, ДО OBSERVE_TOOLS:
В `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') {
if (psContentBlock(input.command || '')) {
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}` };
}
@@ -358,6 +429,8 @@ Expected: FAIL — PowerShell сейчас уходит в write-path-ветку
}
```
> **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.
@@ -393,12 +466,21 @@ describe('escape снимает content-block (owner-санкция)', () => {
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() уже в обеих ветках). Если FAIL — canonicalAction для PowerShell даёт иную строку, чем floorDecide вычисляет → проверить, что обе зовут `canonicalAction(name, input)` идентично (floorDecide: `action = canonicalAction(name, input, {normalizeImpl})`). Выровнять.
Expected: первые два кейса PASS (escaped() в обеих ветках); кейс специфичности PASS **только после Task 1.2b** (до неё `grantA === canonicalAction(B) === 'write:'` → ложно block:false). Если специфичность падает — Task 1.2b не выполнен/не выровнен: floorDecide и тест обязаны звать один `canonicalAction(name, input)`.
- [ ] **Step 3: Коммит**
@@ -415,17 +497,17 @@ test(m7-floor): инвариант escape снимает content-block (Bash+PS)
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 закрыта**
- [ ] **Step 2: Smoke — дыра V1/V1-PS закрыта + §12 content-floor инвариант**
Подтвердить в выводе: `floor-decide` тесты «blocks node -e / curl / Remove-Item even with no escape» зелёные; «safe reading НЕ блокируется» зелёные. Это и есть доказательство закрытия V1/V1-PS.
Подтвердить в выводе: `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, регрессия GREEN
chore(m7-floor): Фаза 1 закрыта — content-floor V1/V1-PS (P-1..P-5), регрессия GREEN
```
**Acceptance Фазы 1:** опасное-по-содержанию (Bash + PowerShell) рубится полом независимо от плана; safe-reading не over-блокируется; escape снимает; регрессия ≥ baseline. → разблокирует увольнение router-gate/powershell-gate в Фазе 8.
**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.
---
@@ -433,6 +515,8 @@ chore(m7-floor): Фаза 1 закрыта — content-floor V1/V1-PS, регр
### Фаза 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 действий.
@@ -463,6 +547,16 @@ chore(m7-floor): Фаза 1 закрыта — content-floor V1/V1-PS, регр
**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). ✓
**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. ✓
**Известное допущение для исполнителя:** регексы content-block — порт из `BASH_HARD_BLACKLIST`; при увольнении router-gate (Фаза 8) `classify-destructive::CONTENT_BLOCK_RE` становится единственным домом этих правил.
**Известное допущение для исполнителя (обновлено разбором плана 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).
@@ -131,7 +131,7 @@
| Увольняемый | Карта покрытия (что доказать) |
|---|---|
| `router-gate` (Bash content default-deny) | **Перенос ПОЛНОГО `BASH_HARD_BLACKLIST` (`enforce-router-gate.mjs:45-73`) в content-floor М5 (правило 8), рубящий ВНЕ зависимости от плана.** Покрыть поимённо: (1) **arbitrary-exec** `node -e/-p/--eval/-r/--import`, `python -c`, `bash/sh -c`, `eval`, node-inline-`fs.*` — критично: это отмычки, способные подделать escape-грант в `~/.claude/runtime` и подорвать машины (floor-write-guard только Write-TOOL, не node-fs); (2) **install** `npm/yarn/pnpm/composer install/require/add`, `npx claude-*`; (3) **egress** `curl -X`, `wget`, `nc/ncat/netcat`, `socat`; (4) **redirect** `>`/`>>`/`2>file` (C16); (5) **env-prefix-интерпретатор**, `--watch` persistent; (6) sub-shell `$(...)`, prompt-injection `echo`. Необратимый субсет (`rm -rf`/`migrate:fresh`/`reset --hard`/force-push/`db:wipe`) уже в `FLOOR_RE`. Whitelist-подход router-gate (default-deny) снимается — content-безопасность Bash держат content-floor (всегда) + членство-в-плане (стена). |
| `powershell-gate` (PowerShell content) `[V1-PS]` | **Реоткрытие v3.8 F1 при наивном увольнении.** `floor-decide` инспектирует команду только при `name==='Bash'` (стр. 85) → PowerShell-tool пол не смотрит вообще. Перенос: content-floor правила 8 расширить на PowerShell-tool отдельным набором (`Remove-Item -Recurse -Force`, `Invoke-WebRequest`/`iwr`, `iex`, redirect `>`/`Out-File`, `Start-Process` интерпретатора) — PS-нативные глаголы НЕ матчат unix-regex, нужен свой набор. |
| `powershell-gate` (PowerShell content) `[V1-PS]` | **Реоткрытие v3.8 F1 при наивном увольнении.** `floor-decide` инспектирует команду только при `name==='Bash'` (стр. 85) → PowerShell-tool пол не смотрит вообще. Перенос: content-floor правила 8 расширить на PowerShell-tool отдельным набором (`Remove-Item -Recurse -Force`, `Invoke-WebRequest`/`iwr`, `iex`, redirect `>`/`Out-File`, `Start-Process` интерпретатора) — PS-нативные глаголы НЕ матчат unix-regex, нужен свой набор. **+ Поправки разбора плана:** (P-2) `canonicalAction` ОБЯЗАН получить ветку PowerShell (`powershell:…`) — без неё все PS-команды схлопываются в `write:`, escape неспецифичен (один грант открывает все PS); (P-3) PS-ветка пола рубит запись write-глаголом (`Set-Content`/`Add-Content`/`Copy-Item`/…) в защищённый путь — иначе PS подделывает escape-грант. См. план Task 1.2b/1.4. |
| `L4` (`llm-judge-per-tool`/`response-scan`) | Тупой per-tool consistency-вопрос → покрыт надзором А2 судьи М4 (расхождение → голос «Верность намерению»). М4 шире (приёмка ТЗ/плана/результата). |
| `runtime-write-deny` | Запись Write-инструментом в `~/.claude/runtime` → ветка пола М5 (RUNTIME_RE). **NB (правило 7):** после переезда обязан чтить escape (сейчас не чтит — DOC-1). |
| `verify-before-push` / `verify-record` | «Свежая проверка перед push» → М5 по-критерийный подписанный GREEN + М4 Гейт-3. |
@@ -239,7 +239,7 @@ L4 (`llm-judge-per-tool`/`response-scan`) ретайрится после тог
**Правки существующих машин (→ обязателен `audit-context-building` перед каждой):**
- М4: журнал-K2 для coverage/todowrite/decomposition (через skill-журналер); Гейт-2 план-требование; Гейт-3 verify; пост-оценщик D31 + halt-counter для rationalization; вливание self-debrief; ИИ-проводка (обёртка `enforce-judge-gate` + ключ/флаг).
- М5: мутация P18 как условие GREEN; подписанный по-критерийный GREEN; **content-floor класс (правило 8): port полного `BASH_HARD_BLACKLIST` + PowerShell-набор, блок независимо от плана** (V1/V1-PS — НЕ просто «перенос необратимого»); floor-write-guard расширить с Write-TOOL на содержимое команд node/PowerShell.
- М5: мутация P18 как условие GREEN; подписанный по-критерийный GREEN; **content-floor класс (правило 8): port полного `BASH_HARD_BLACKLIST` + PowerShell-набор, блок независимо от плана** (V1/V1-PS — НЕ просто «перенос необратимого»); floor-write-guard расширить с Write-TOOL на содержимое команд node/PowerShell. **Реализация (разбор плана 2026-06-08):** единый источник `matchBashHardBlacklist` (перенесён в `shell-content-rules.mjs`, НЕ ручной список — P-1); content-block whole+per-segment паритет с `bashIsFloor` (P-4); `suspicious ||= contentBlock` (P-5); `canonicalAction` +ветка PowerShell (P-2); PS forge-страж записи в защищённый путь (P-3).
- М2: ветка-как-шаг-плана; действие-в-плане для вытесненной дисциплины; seed-allow реактивных дисциплинарных навыков (SE-K).
- М3: роутинг по журналу (замена `router-tool-gate`); enforce by construction (без warn-only).
- М6: escape на normative-ЗАКОН (Кусок 2) — расширение floor_escape на класс «правка закона»; **escape-survivability (правило 7): canonicalAction тотальна, panic-ветка, чтение escape остальными стражами**.
@@ -281,6 +281,16 @@ L4 (`llm-judge-per-tool`/`response-scan`) ретайрится после тог
**Открытый verify-item прошлого handoff закрыт фактом:** `enforce-parallel-session-lock.mjs:12` — no-op → §10 увольнение.
### Поправки разбора ПЛАНА (2026-06-08, второй проход)
После написания плана реализации владелец заказал отдельный критический разбор **самого плана** (объектив «как будто план писал не я»; та же цепочка `audit-context-building``sharp-edges``variant-analysis``superpowers:systematic-debugging`, заземление против реального кода М1–М6). Найдено 8 находок (2 КРИТ / 1 ВЫС / 3 СРЕД / 2 НИЗ), все заземлены в коде, призраки отсеяны. Внесены в план (детали — план §«Поправки разбора плана» + Tasks 1.0.5/1.1/1.2b/1.3/1.4/1.5/1.6). Затрагивают и спеку:
- **P-1 (КРИТ):** заявленный «полный port `BASH_HARD_BLACKLIST`» в плане был **подмножеством** (терял #21 env-prefix / #22 --watch / #34 echo-inj / C16 `2>file` / cp-mv-chmod-chown-chgrp; `cp X ~/.claude/runtime/…` = forge escape-гранта). Закрытие: **единый источник** `matchBashHardBlacklist` (перенесён в `shell-content-rules.mjs`), не ручной список; §12 content-floor инвариант доказывает полноту по конструкции. Поэтому §5 «покрыть поимённо» теперь читается как «покрыть единым матчером + инвариант», не «перепечатать».
- **P-2 (КРИТ):** `canonicalAction` (escape-grant.mjs) не имеет ветки PowerShell → все PS-команды = `write:` → escape неспецифичен (одного гранта хватает на любую PS-команду в окне), причём план-тест escape был зелёным ложно (обе стороны равно сломаны). Закрытие — ветка PowerShell + тест специфичности.
- **P-3 (ВЫС):** PS-ветка пола возвращала `block:false` минуя runtime/secret-страж (поле `command` не парсится общим write-стражем) → PS forge escape-гранта. Закрытие — PS forge-страж.
- **P-4/P-5/P-6/P-7/P-8** — per-segment паритет content-block; `suspicious ||= contentBlock` (судья видит); §12 CI-инварианты закреплены за фазами; явный список fail-CLOSE-подмножества Фазы 0; честная пометка «план = детальная Фаза 1 + scoped-скелет».
Промис §1 и предпосылки П1–П3 не меняются — разбор плана подтвердил их и **уточнил, что П1 (content-floor) держится только при едином источнике + PowerShell-привязке escape**, иначе остаётся дыра.
## Само-аудит спеки (2026-06-08)
Дизайн собран через цепочку `audit-context-building` (карта обходов §2, построчно с цитатами строк) + `sharp-edges` (6 находок SE1–SE6 → 6 правил ужесточения §4.1) + `variant-analysis` (sweep всех близнецов §2.1). Ядро — **принцип атрибуции §1**: непробиваемость (Кусок 1) + неприкосновенный закон (Кусок 2) + видимость (Кусок 3) ⇒ глюк машины ≠ хитрость контроллера. Технический content проверен против реального кода М1–М6; границы с машинами — в §3–§5. Остаточные пределы названы честно §11.