# Sub-plan E — footgun-фиксы существующих машин Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Закрыть 4 точечных footgun'а из спеки (✅O13 escape `skill:<name>`, O11 сузить `detectMoney`, ✅O18 judge_mode WARN, §4/SE-R7-6 loop-termination) **поверх существующих машин** — минимальными правками по TDD, не ломая существующие тесты.

**Architecture:** 4 фикса в существующих файлах + 1 новый чистый хелпер. Каждый фикс — АДДИТИВНЫЙ или сужающий (не расширяет дыру). Все правки сверены по реальному коду построчно. Wiring новых сигналов (warn в owner-резюме, loop-termination в цикл) — задача sub-plan C; E лишь даёт корректные деки́деры/каноны.

**Tech Stack:** Node ESM (`.mjs`), vitest (tools-config). Источник истины спеки: `docs/superpowers/specs/2026-06-10-router-mentor-live-rebuild-design.md` §6 NB (✅O13) / §7.1 + O11 / §7.3 (✅O18) / §4 (SE-R7-6).

**Сверенные факты кода (построчно):**
- `canonicalAction` `[escape-grant.mjs:30-49]` — **нет ветки `Skill`**: Skill падает в `extractWritePath` (поле `skill` не в `PATH_FIELDS` `[:18]`) → `write:''`/cwd → грант агностичен (любой скил совпадает).
- `toFloorEscapeRecord` `[askuser-answer-parser.mjs:195-203]` — `action = m[1].trim()` AS-IS (без нормализации регистра).
- `MONEY_RE` `[judge-seal-channel.mjs:24]` — подстрочный, без границ слова (комментарий :23 это признаёт) → `несбалансированный`→`баланс`, `recharge`/`surcharge`→`charge`.
- judge_mode silent-cliff `[enforce-supreme-gate.mjs:211-216]` — `judge_mode !== 'live-block'` на любой из двух печатей → ТИХО разговорный (fail-safe, но без сигнала).
- gate-3 `goal_achieved` — `judge-engine.mjs:26` (VOTE_LAYOUTS gate3) — критерий завершения цикла.

**⚠️ Координационная пометка (НЕ переоткрывать — ✅O13 решён владельцем):** после реализации Task 1-2 владелец печатает escape для **формальных скилов** как `FLOOR-ESCAPE: skill:<полное имя скила в нижнем регистре>` (было `write:<cwd>`). Семена (writing-plans/brainstorming/systematic-debugging/test-driven-development/requesting-code-review/verification-before-completion/discovery-interview) escape НЕ требуют — их не касается. Handoff/инструкции владельца обновить при активации (вне scope E — это docs владельца, не код).

**Scope (граница):**
- **В E:** правки `tools/escape-grant.mjs`, `tools/askuser-answer-parser.mjs`, `tools/judge-seal-channel.mjs`, `tools/enforce-supreme-gate.mjs` + новый `tools/loop-termination.mjs` (+ кейсы в существующих `*.test.mjs` + новый `loop-termination.test.mjs`).
- **НЕ в E (→ sub-plan C):** wiring warn-сигнала O18 в owner-резюме гейта-1; wiring `loopTerminationDecision` в петлю §4; передача gate-3 GO. E даёт чистые деки́деры/каноны готовыми.
- **НЕ в E:** обновление handoff/инструкций владельца под новый escape-формат (docs владельца).

**Квирк среды (ОБЯЗАТЕЛЬНО для шагов «Run»):** vitest запускается ТОЛЬКО из `app/`:
```
cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs <фильтр> --reporter dot
```
cwd между Bash-вызовами ненадёжен — каждый Run-шаг префиксить `cd app && ...` в одной команде.

**NB git:** git зажат стеной у Claude — `git commit`-шаги выполняет ВЛАДЕЛЕЦ/исполнитель вне Claude-стены (или субагент per Pravila §15.1).

**NB регрессия:** каждая правка существующей машины ОБЯЗАНА оставить существующие тесты файла зелёными. Каждый Task включает прогон тестов затронутого файла + (Task 6) полную регрессию.

---

### Task 1: ✅O13 — ветка `Skill` в `canonicalAction` (escape привязан к скилу)

**Files:**
- Modify: `tools/escape-grant.mjs` (вставка ветки в `canonicalAction`, после mcp-ветки `[:42]`, перед `const p = extractWritePath(input)` `[:43]`)
- Test: `tools/escape-grant.test.mjs` (добавить кейсы)

- [ ] **Step 1: Write the failing test**

```js
// добавить в tools/escape-grant.test.mjs (рядом с существующими canonicalAction-тестами)
import { canonicalAction } from './escape-grant.mjs';

describe('canonicalAction — Skill (✅O13)', () => {
  it('Skill → skill:<имя в нижнем регистре>, не write:cwd', () => {
    expect(canonicalAction('Skill', { skill: 'Audit-Context-Building:Audit-Context-Building' }))
      .toBe('skill:audit-context-building:audit-context-building');
  });
  it('разные скилы → разные каноны (не агностичны)', () => {
    expect(canonicalAction('Skill', { skill: 'sharp-edges:sharp-edges' }))
      .not.toBe(canonicalAction('Skill', { skill: 'variant-analysis:variants' }));
  });
  it('пустой skill → skill: (детерминирован, не cwd)', () => {
    expect(canonicalAction('Skill', {})).toBe('skill:');
  });
  it('не-Skill инструменты не затронуты (Bash остаётся bash:)', () => {
    expect(canonicalAction('Bash', { command: 'ls' })).toMatch(/^bash:/);
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/escape-grant.test.mjs --reporter dot`
Expected: FAIL — Skill даёт `write:...` (агностичен), не `skill:...`.

- [ ] **Step 3: Write minimal implementation**

В `tools/escape-grant.mjs`, в `canonicalAction`, СРАЗУ после mcp-ветки `[:42]` и ПЕРЕД `const p = extractWritePath(input);` `[:43]` вставить:

```js
    // ✅O13 (router-mentor): escape привязан к КОНКРЕТНОМУ скилу (не write:cwd-агностик).
    // Канон нормализован (lowercase/trim) — совпадает с тем, что пишет toFloorEscapeRecord.
    if (name === 'Skill') return `skill:${String(input.skill || '').toLowerCase().trim()}`;
```

- [ ] **Step 4: Run test to verify it passes + существующие тесты файла**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/escape-grant.test.mjs --reporter dot`
Expected: PASS — новые 4 + все существующие escape-grant тесты зелёные.

- [ ] **Step 5: Commit**

```bash
git add tools/escape-grant.mjs tools/escape-grant.test.mjs
git commit -m "feat(escape-grant): canonicalAction ветка Skill skill:<name> (O13, sub-plan E Task 1)"
```

---

### Task 2: ✅O13 — нормализация `skill:`-действия в `toFloorEscapeRecord` (обе стороны совпадают)

**Files:**
- Modify: `tools/askuser-answer-parser.mjs` (`toFloorEscapeRecord` `[:195-203]`)
- Test: `tools/askuser-answer-parser.test.mjs`

**Зачем:** грант пишет парсер из FLOOR-ESCAPE-строки владельца; стена матчит его против `canonicalAction(...)`. Task 1 канонизирует Skill в lowercase → парсер обязан lowercase'ить `skill:`-действие, иначе при ином регистре грант не совпадёт. `write:`-действия НЕ трогаем (их регистр — ответственность владельца, как сейчас).

- [ ] **Step 1: Write the failing test**

```js
// добавить в tools/askuser-answer-parser.test.mjs
import { toFloorEscapeRecord } from './askuser-answer-parser.mjs';

describe('toFloorEscapeRecord — skill-канон (✅O13)', () => {
  it('skill:-действие приводится к нижнему регистру (совпадает с canonicalAction)', () => {
    const r = toFloorEscapeRecord('FLOOR-ESCAPE: skill:Audit-Context-Building:Audit-Context-Building', { nowMs: 1 });
    expect(r.action).toBe('skill:audit-context-building:audit-context-building');
  });
  it('write:-действие НЕ меняет регистр (поведение как раньше)', () => {
    const r = toFloorEscapeRecord('FLOOR-ESCAPE: write:c:/Моя/Path.md', { nowMs: 1 });
    expect(r.action).toBe('write:c:/Моя/Path.md');
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/askuser-answer-parser.test.mjs --reporter dot`
Expected: FAIL — skill-действие остаётся в исходном регистре.

- [ ] **Step 3: Write minimal implementation**

В `tools/askuser-answer-parser.mjs`, в `toFloorEscapeRecord`, заменить блок `[:200-202]`:

```js
  const action = m[1].trim();
  if (!action) return null;
  return { type: 'floor_escape', action, ts: nowMs };
```

на:

```js
  let action = m[1].trim();
  if (!action) return null;
  // ✅O13: skill-escape канон — нижний регистр (зеркало canonicalAction Skill-ветки),
  // иначе грант от иного регистра не совпадёт. write:-каноны не трогаем (как сейчас).
  if (/^skill:/i.test(action)) action = action.toLowerCase();
  return { type: 'floor_escape', action, ts: nowMs };
```

- [ ] **Step 4: Run test to verify it passes + существующие тесты файла**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/askuser-answer-parser.test.mjs --reporter dot`
Expected: PASS — новые 2 + все существующие парсер-тесты зелёные.

- [ ] **Step 5: Commit**

```bash
git add tools/askuser-answer-parser.mjs tools/askuser-answer-parser.test.mjs
git commit -m "feat(askuser-parser): skill-escape канон lowercase (O13 обе стороны, sub-plan E Task 2)"
```

---

### Task 3: O11 — сузить `detectMoney` (левая граница слова)

**Files:**
- Modify: `tools/judge-seal-channel.mjs` (`MONEY_RE` `[:24]`)
- Test: `tools/judge-seal-channel.test.mjs`

**Зачем:** подстрочный `MONEY_RE` ложно срабатывает (`несбалансированный`→`баланс`, `recharge`→`charge`) → лишние owner-seal'ы → fatigue. Сузить **левой** Unicode-границей слова `(?<![\p{L}])` (флаг `u`). Русские суффиксы-стемы справа СОХРАНЯЕМ (`списан`→`списание` обязан остаться) — поэтому правую границу НЕ ставим. Направление осторожности неизменно (истинные деньги по-прежнему → владелец).

- [ ] **Step 1: Write the failing test**

```js
// добавить в tools/judge-seal-channel.test.mjs
import { detectMoney } from './judge-seal-channel.mjs';

describe('detectMoney — сужение (O11)', () => {
  it('истинные деньги — по-прежнему true', () => {
    for (const t of ['баланс', 'списание средств', 'оплата', 'тариф', 'charge', 'invoice', 'руб'])
      expect(detectMoney(t)).toBe(true);
  });
  it('русский стем-суффикс сохранён (списан→списание)', () => {
    expect(detectMoney('произведено списание')).toBe(true);
    expect(detectMoney('начисление бонуса')).toBe(true);
  });
  it('ложные подстроки теперь false', () => {
    expect(detectMoney('несбалансированный массив')).toBe(false); // баланс с буквой слева
    expect(detectMoney('recharge the page')).toBe(false);          // charge с буквой слева
    expect(detectMoney('surcharge-free')).toBe(false);
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/judge-seal-channel.test.mjs --reporter dot`
Expected: FAIL — `несбалансированный`/`recharge`/`surcharge` сейчас дают true.

- [ ] **Step 3: Write minimal implementation**

В `tools/judge-seal-channel.mjs` заменить `[:22-24]`:

```js
// Осторожно-широкий список денежных маркеров: при сомнении → деньги → владелец.
// NB: без \b у кириллицы (ASCII-границы слова кириллицу не видят) — только подстроки.
const MONEY_RE = /(руб|₽|копе|тариф|списан|биллинг|баланс|оплат|плат[её]ж|выручк|начислен|деньг|payment|charge|refund|invoice|money)/i;
```

на:

```js
// Осторожно-широкий список денежных маркеров: при сомнении → деньги → владелец.
// O11: левая Unicode-граница слова (?<![\p{L}]) режет ложь несбалансированный→баланс,
// recharge/surcharge→charge. ПРАВУЮ границу НЕ ставим — русские стем-суффиксы остаются
// (списан→списание, начислен→начисление). Флаг u нужен для \p{L}.
const MONEY_RE = /(?<![\p{L}])(руб|₽|копе|тариф|списан|биллинг|баланс|оплат|плат[её]ж|выручк|начислен|деньг|payment|charge|refund|invoice|money)/iu;
```

- [ ] **Step 4: Run test to verify it passes + существующие тесты файла**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/judge-seal-channel.test.mjs --reporter dot`
Expected: PASS — новые 3 + все существующие judge-seal-channel тесты зелёные. **Если существующий тест ожидал true на ложной подстроке — это был баг-фиксируемый кейс: проверить, что тест проверял реальные деньги, а не артефакт подстроки; при необходимости — systematic-debugging.**

- [ ] **Step 5: Commit**

```bash
git add tools/judge-seal-channel.mjs tools/judge-seal-channel.test.mjs
git commit -m "fix(judge-seal-channel): сузить detectMoney левой границей (O11, sub-plan E Task 3)"
```

---

### Task 4: ✅O18 — `judgeModeMismatch` + warn-сигнал при рассинхроне judge_mode

**Files:**
- Modify: `tools/enforce-supreme-gate.mjs` (+ экспорт `judgeModeMismatch`; warn в ветке `[:211-216]`)
- Test: `tools/enforce-supreme-gate.test.mjs`

**Зачем:** рассинхрон judge_mode (одна печать `live-block`, другая нет) тихо отключает энфорсмент. ✅O18: направление fail-safe (→ разговорный) СОХРАНИТЬ, но добавить `warn`-сигнал в возврат — owner-резюме гейта-1 (wiring в C) его покажет.

- [ ] **Step 1: Write the failing test**

```js
// добавить в tools/enforce-supreme-gate.test.mjs
import { judgeModeMismatch, decideMode } from './enforce-supreme-gate.mjs';

describe('judgeModeMismatch (✅O18)', () => {
  it('одна live-block, другая нет → рассинхрон true', () => {
    expect(judgeModeMismatch('live-block', 'shadow')).toBe(true);
    expect(judgeModeMismatch('shadow', 'live-block')).toBe(true);
  });
  it('обе одинаковы → не рассинхрон', () => {
    expect(judgeModeMismatch('live-block', 'live-block')).toBe(false);
    expect(judgeModeMismatch('shadow', 'shadow')).toBe(false);
    expect(judgeModeMismatch(undefined, null)).toBe(false);
  });
});

describe('decideMode — warn при рассинхроне (✅O18)', () => {
  it('plan=live-block, artifact=shadow, мутатор → conversational + warn:true', () => {
    const r = decideMode({
      toolUse: { name: 'Write', input: { file_path: 'x' } },
      frozenPlan: { judge_mode: 'live-block' },
      frozenArtifact: { judge_mode: 'shadow' },
      stepPtr: 0, key: null,
      verifyImpl: () => true, verifyArtifactImpl: () => true, normalize: (s) => s,
      escapeGrants: [], escapeConsumed: [], now: 1000,
    });
    expect(r.mode).toBe('conversational');
    expect(r.warn).toBe(true);
    expect(r.warnReason).toMatch(/рассинхрон/);
  });
  it('обе shadow → conversational БЕЗ warn (намеренно off, не footgun)', () => {
    const r = decideMode({
      toolUse: { name: 'Write', input: { file_path: 'x' } },
      frozenPlan: { judge_mode: 'shadow' },
      frozenArtifact: { judge_mode: 'shadow' },
      stepPtr: 0, key: null,
      verifyImpl: () => true, verifyArtifactImpl: () => true, normalize: (s) => s,
      escapeGrants: [], escapeConsumed: [], now: 1000,
    });
    expect(r.warn).toBeUndefined();
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/enforce-supreme-gate.test.mjs --reporter dot`
Expected: FAIL — `judgeModeMismatch is not a function` + нет `warn`-поля.
**NB:** если decideMode требует доп. стабов (verifyImpl-проверка плана до `[:200]` — сверить фактическую сигнатуру `[:188-199]`), добавить недостающие поля в setup теста по реальной сигнатуре, не угадывая.

- [ ] **Step 3: Write minimal implementation**

(а) В `tools/enforce-supreme-gate.mjs` добавить экспорт в module-scope (рядом с `isObserveOnly`, ~`[:64]`):

```js
/** ✅O18: рассинхрон печатей judge_mode — РОВНО одна 'live-block' (XOR). Обе одинаковы → false. */
export function judgeModeMismatch(planMode, artifactMode) {
  return (planMode === 'live-block') !== (artifactMode === 'live-block');
}
```

(б) В ветке `[:211-216]` заменить:

```js
  if (frozenPlan.judge_mode !== 'live-block' || frozenArtifact.judge_mode !== 'live-block') {
    if (isSeed(toolUse) || isObserveOnly(toolUse)) {
      return { decision: 'allow', mode: 'conversational', reason: 'seed/observe (не live-block-печать, SE-2)' };
    }
    return { decision: 'block', mode: 'conversational', reason: 'не live-block-печать (shadow/наблюдение) — не одобрение к энфорсменту (SE-2)' };
  }
```

на:

```js
  if (frozenPlan.judge_mode !== 'live-block' || frozenArtifact.judge_mode !== 'live-block') {
    // ✅O18: рассинхрон (одна печать live-block, другая нет) — fail-safe направление СОХРАНЕНО
    // (→ разговорный), но ГРОМКО: warn-поле в возврат (owner-резюме гейта-1 покажет, wiring в C).
    const warnFields = judgeModeMismatch(frozenPlan.judge_mode, frozenArtifact.judge_mode)
      ? { warn: true, warnReason: 'judge_mode рассинхрон план≠артефакт — энфорсмент off (O18)' } : {};
    if (isSeed(toolUse) || isObserveOnly(toolUse)) {
      return { decision: 'allow', mode: 'conversational', reason: 'seed/observe (не live-block-печать, SE-2)', ...warnFields };
    }
    return { decision: 'block', mode: 'conversational', reason: 'не live-block-печать (shadow/наблюдение) — не одобрение к энфорсменту (SE-2)', ...warnFields };
  }
```

- [ ] **Step 4: Run test to verify it passes + существующие тесты файла**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/enforce-supreme-gate.test.mjs --reporter dot`
Expected: PASS — новые + все существующие enforce-supreme-gate тесты зелёные. `warnFields` спред пуст при обеих одинаковых печатях → существующие conversational-тесты не затронуты (поле отсутствует).

- [ ] **Step 5: Commit**

```bash
git add tools/enforce-supreme-gate.mjs tools/enforce-supreme-gate.test.mjs
git commit -m "feat(supreme-gate): judgeModeMismatch + warn при рассинхроне (O18, sub-plan E Task 4)"
```

---

### Task 5: §4 SE-R7-6 — `loopTerminationDecision` (новый чистый хелпер)

**Files:**
- Create: `tools/loop-termination.mjs`
- Test: `tools/loop-termination.test.mjs`

**§4 SE-R7-6.** Внешняя петля закрывается ТОЛЬКО когда владелец явно объявил «цель достигнута» ЛИБО судья gate-3 `goal_achieved` `[judge-engine.mjs:26]` дал GO. Контроллер сам петлю НЕ закрывает. Чистый детерминированный хелпер (wiring в петлю — C).

- [ ] **Step 1: Write the failing test**

```js
// tools/loop-termination.test.mjs
import { describe, it, expect } from 'vitest';
import { loopTerminationDecision } from './loop-termination.mjs';

describe('loopTerminationDecision (SE-R7-6)', () => {
  it('владелец объявил цель достигнутой → terminate', () => {
    expect(loopTerminationDecision({ ownerDeclaredDone: true }).terminate).toBe(true);
  });
  it('судья gate-3 GO → terminate', () => {
    expect(loopTerminationDecision({ judgeGate3Go: true }).terminate).toBe(true);
  });
  it('ни того ни другого → продолжать (контроллер сам не закрывает)', () => {
    const r = loopTerminationDecision({});
    expect(r.terminate).toBe(false);
    expect(r.reason).toMatch(/контроллер сам не закрывает/);
  });
  it('оба → terminate', () => {
    expect(loopTerminationDecision({ ownerDeclaredDone: true, judgeGate3Go: true }).terminate).toBe(true);
  });
  it('truthy-не-true НЕ закрывает (строгий ===true, fail-safe)', () => {
    expect(loopTerminationDecision({ ownerDeclaredDone: 'yes' }).terminate).toBe(false);
    expect(loopTerminationDecision({ judgeGate3Go: 1 }).terminate).toBe(false);
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/loop-termination.test.mjs --reporter dot`
Expected: FAIL — `Cannot find module './loop-termination.mjs'`.

- [ ] **Step 3: Write minimal implementation**

```js
// tools/loop-termination.mjs
#!/usr/bin/env node
/**
 * loop-termination (§4 SE-R7-6) — внешняя петля наставника закрывается ТОЛЬКО по
 * явному «цель достигнута» владельца ЛИБО GO судьи gate-3 goal_achieved [judge-engine.mjs:26].
 * Контроллер сам петлю НЕ закрывает. Чистый детерминированный хелпер; wiring в петлю — sub-plan C.
 * Строгий ===true (fail-safe): любой иной truthy НЕ закрывает (не дать «случайно» завершить).
 */
export function loopTerminationDecision({ ownerDeclaredDone = false, judgeGate3Go = false } = {}) {
  if (ownerDeclaredDone === true) return { terminate: true, reason: 'владелец объявил цель достигнутой' };
  if (judgeGate3Go === true) return { terminate: true, reason: 'судья gate-3 goal_achieved → GO на завершение' };
  return { terminate: false, reason: 'продолжать — контроллер сам не закрывает петлю (SE-R7-6)' };
}
```

- [ ] **Step 4: Run test to verify it passes**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs ../tools/loop-termination.test.mjs --reporter dot`
Expected: PASS — 5 passed.

- [ ] **Step 5: Commit**

```bash
git add tools/loop-termination.mjs tools/loop-termination.test.mjs
git commit -m "feat(loop-termination): выход только владелец/gate-3 (SE-R7-6, sub-plan E Task 5)"
```

---

### Task 6: Регрессия tools-vitest GREEN

**Files:** (нет правок — проверка)

- [ ] **Step 1: Прогнать ВЕСЬ tools-набор**

Run: `cd app && node node_modules/vitest/vitest.mjs run --config vitest.config.tools.mjs --reporter dot`
Expected: PASS — весь tools-набор зелёный. **0 регрессий в машинах М1-М7** — это критично: E правит 4 существующих файла (escape-grant/askuser-parser/judge-seal-channel/enforce-supreme-gate), любой упавший сосед = регрессия. Зафиксировать число passed.

- [ ] **Step 2: Если RED — systematic-debugging**

При падении соседних тестов — НЕ править наугад: `superpowers:systematic-debugging`, root cause (особое внимание: (а) тесты, ожидавшие старый `write:`-канон Skill-escape — обновить под `skill:<name>`; (б) тесты detectMoney на ложных подстроках — пере-оценить; (в) тесты decideMode с exact-equal, увидевшие новое warn-поле), точечный фикс, повтор Step 1.

- [ ] **Step 3: Commit (если были фиксы Step 2; иначе пропустить)**

```bash
git add tools/
git commit -m "fix(footgun-fixes): регрессия tools-vitest GREEN (sub-plan E Task 6)"
```

---

## R6.3 amendments (ревью 5-скил цепочки 2026-06-10)

Поправки к задачам выше (исполнять ВМЕСТЕ):

**SE1 (coordination — объявлять точный skill-id).** Усилить: «O13 требует ТОЧНЫЙ `input.skill`-id (есть близнецы `audit-context-building:audit-context` vs `:audit-context-building`). Перед запросом escape формального скила Claude ОБЯЗАН ОБЪЯВИТЬ точную строку `FLOOR-ESCAPE: skill:<id>` для копирования владельцем — НЕ заставлять угадывать». Cross-ref C2 (mentor-seam объявляет id).

**C-4 (activation-coordination — синк owner-runbook).** Новая нота/задача: «При активации E (canonicalAction Skill-ветка) — СИНХРОННО обновить owner-runbook/handoff: формальный escape `write:cwd` → `skill:<id>`. До синка владелец, печатая старый `write:cwd`, получит БЛОК (грант не совпадёт `[escape-grant.mjs:43-44]`). Это plan↔runbook контракт-рассинхрон — ОБЯЗАТЕЛЕН в момент активации».

**V-6 / SE13 (cross-cutting).** Регрессия (Task 6) — «[НЕ-TDD: регрессия]». «N passed» — иллюстративно; сверяй ЗЕЛЁНОСТЬ, не точное число.

**Self-review E — обновить:** +SE1 (объявлять skill-id) / +C-4 (синк runbook при активации).

---

## Self-Review (writing-plans)

**1. Spec coverage:**
- ✅O13 escape `skill:<name>` (обе стороны: canonicalAction + toFloorEscapeRecord) → Task 1/2. ✅
- O11 сузить detectMoney (левая граница, русские стемы сохранены) → Task 3. ✅
- ✅O18 judge_mode WARN (fail-safe направление сохранено + warn-поле) → Task 4. ✅
- §4 SE-R7-6 loop-termination (только владелец/gate-3) → Task 5. ✅
- Регрессия (правки 4 машин не ломают соседей) → Task 6. ✅
- **Вне E (→ C):** wiring warn в owner-резюме, wiring loopTerminationDecision в петлю, передача gate-3 GO. НЕ покрыты намеренно. **Вне E:** обновление handoff/инструкций владельца под `skill:`-escape (docs владельца).

**2. Placeholder scan:** реальный код в каждом шаге; точные OLD→NEW блоки сверены по прочитанным строкам (`escape-grant.mjs:42-43`, `askuser-answer-parser.mjs:200-202`, `judge-seal-channel.mjs:22-24`, `enforce-supreme-gate.mjs:211-216`); команды Run с ожидаемым выводом; нет «TBD». ✅

**3. Type consistency:** `canonicalAction`→string (`skill:...`); `toFloorEscapeRecord`→`{type,action,ts}` (action lowercase для skill:); `detectMoney`→boolean; `judgeModeMismatch`→boolean; `decideMode`→`{decision,mode,reason,warn?,warnReason?}`; `loopTerminationDecision`→`{terminate,reason}`. Имена согласованы. ✅

**Известное ограничение (честно):** ✅O13 — поведенческая смена для владельца (escape формальных скилов `write:<cwd>` → `skill:<name>`); семена не затронуты. Реализация Task 1-2 меняет ЖИВОЙ escape-механизм активной обороны M1-M6 → координировать с владельцем при активации (обновить handoff). detectMoney O11 — левая граница не ловит редкий случай «деньги-маркер в середине склейки» (напр. слитное `балансовыйотчёт` без пробела) — приемлемо: осторожность сохранена на нормальных текстах, истинные деньги по-прежнему → владелец. decideMode warn-тест может потребовать доп. стабов под фактическую сигнатуру `[:188-199]` — отмечено в Task 4 Step 2.
