# supreme-gate PostToolUse-advance — Implementation Plan

⛔ **ОТВЕРГНУТО (2026-06-14).** Подход Post-advance реализован и откатан: live-смоук
показал, что PostToolUse-хуки не срабатывают на упавшем (exit≠0) инструменте → RED-шаги
TDD не продвигаются. Корректный фикс — Pre-advance + перестановка `enforce-domain-skill-discipline`
ПЕРЕД `enforce-supreme-gate`. Детали — в спеке `2026-06-14-supreme-gate-post-advance-design.md`
(статус ОТВЕРГНУТ). План сохранён как запись урока.

> **For agentic workers:** REQUIRED SUB-SKILL: исполнять inline через superpowers:executing-plans
> (деликатное ядро стены — НЕ субагенты, per Pravila §15 / брифа эпика). Steps — чекбоксы.

**Goal:** Перенести сдвиг указателя шага в `enforce-supreme-gate` с PreToolUse-allow на
подтверждённое исполнение (PostToolUse), устранив рассинхрон при блоке поздним хуком / user-deny.

**Architecture:** `decide`/`decideMode` не меняются (оракул решения). `runGate` расщепляется на
`runGatePre` (ворота + журнал-намерение, без сдвига) и `runGatePost` (сдвиг по подтверждённому
исполнению, никогда не блокирует). `runGate` остаётся compat-обёрткой (Pre→Post) для прямых
вызовов/тестов. `main()` ветвится по `event.hook_event_name`. Владелец доп-регистрирует хук на
PostToolUse `'*'`.

**Tech Stack:** Node ESM, vitest (tools-конфиг). Файл: `tools/enforce-supreme-gate.mjs` +
тесты `tools/enforce-supreme-gate.test.mjs`.

**Спека:** `docs/superpowers/specs/2026-06-14-supreme-gate-post-advance-design.md`.

**Регрессия (одиночная команда):** `npx vitest run --root app --config vitest.config.tools.mjs`

---

## Файловая структура

- Modify: `tools/enforce-supreme-gate.mjs` — расщепить `runGate`; добавить `runGatePre`,
  `runGatePost`, `isPostEvent`; переписать `main()`.
- Modify (tests): `tools/enforce-supreme-gate.test.mjs` — новые describe-блоки
  `runGatePre` / `runGatePost` / `isPostEvent`; существующий блок `runGate (pure orchestration)`
  и escape-тесты остаются и должны пройти без правок (обёртка сохраняет поведение).

---

### Task 1: `runGatePost` — сдвиг только на подтверждённом исполнении

**Files:**

- Modify: `tools/enforce-supreme-gate.mjs`
- Test: `tools/enforce-supreme-gate.test.mjs`

- [ ] **Step 1: Написать падающие тесты**

В конце `tools/enforce-supreme-gate.test.mjs` добавить (использует существующие `PLAN`, `KEY`,
`verifyStub` из файла; `verifyStub` уже определён в области тестов runGate):

```js
import { runGatePost, runGatePre, isPostEvent } from './enforce-supreme-gate.mjs';

describe('runGatePost (сдвиг по подтверждённому исполнению)', () => {
  const baseArgs = (over) => ({
    event: { tool_name: 'Write', tool_input: { file_path: 'tools/foo.mjs' } },
    frozenPlan: { ...PLAN, judge_mode: 'live-block' },
    frozenArtifact: { sig: 'ok', judge_mode: 'live-block' },
    stepPtr: 0, key: KEY, verifyImpl: verifyStub, verifyArtifactImpl: verifyStub,
    normalize: (p) => p.toLowerCase(), saveStep: () => {}, ...over,
  });

  it('allow+advance → saveStep(advanceTo) вызван, advanced:true, не блокирует', () => {
    let saved = null;
    const r = runGatePost(baseArgs({ saveStep: (n) => { saved = n; } }));
    expect(saved).toBe(1);
    expect(r.advanced).toBe(true);
    expect(r.block).toBeUndefined();
  });

  it('несовпадение шага → saveStep НЕ вызван, advanced:false', () => {
    let saved = 'untouched';
    const r = runGatePost(baseArgs({
      event: { tool_name: 'Write', tool_input: { file_path: 'tools/evil.mjs' } },
      saveStep: (n) => { saved = n; },
    }));
    expect(saved).toBe('untouched');
    expect(r.advanced).toBe(false);
  });

  it('seed (Skill brainstorming) → не двигает (advance не true)', () => {
    let saved = 'untouched';
    const r = runGatePost(baseArgs({
      event: { tool_name: 'Skill', tool_input: { skill: 'superpowers:brainstorming' } },
      saveStep: (n) => { saved = n; },
    }));
    expect(saved).toBe('untouched');
    expect(r.advanced).toBe(false);
  });
});
```

- [ ] **Step 2: Запустить — убедиться, что падает**

Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/enforce-supreme-gate.test.mjs`
Expected: FAIL — `runGatePost is not a function` / `not exported`.

- [ ] **Step 3: Реализовать `runGatePost`**

В `tools/enforce-supreme-gate.mjs` сразу после функции `runGate` (≈строка 387) добавить:

```js
/**
 * PostToolUse-такт (фикс рассинхрона): сдвиг указателя ТОЛЬКО после подтверждённого
 * исполнения инструмента. Пере-выводит decideMode по ТЕКУЩЕМУ (не сдвинутому) stepPtr;
 * на allow+advance — saveStep(advanceTo) (+ removeFrozenPlan при planComplete). НИКОГДА
 * не блокирует (инструмент уже исполнился). seed/observe/escape/finishPlan/несовпадение
 * → не двигает.
 */
export function runGatePost({ event, frozenPlan, frozenArtifact, stepPtr, key, verifyImpl, verifyArtifactImpl, normalize, saveStep, removeFrozenPlan, escapeGrants = [], escapeConsumed = [], now = Date.now() }) {
  const toolUse = { name: event.tool_name, input: event.tool_input };
  const r = decideMode({ toolUse, frozenPlan, frozenArtifact, stepPtr, key, verifyImpl, verifyArtifactImpl, normalize, escapeGrants, escapeConsumed, now });
  if (r.decision === 'allow' && r.advance === true) {
    saveStep(r.advanceTo);
    if (r.planComplete && typeof removeFrozenPlan === 'function') {
      try { removeFrozenPlan(); } catch { /* best-effort */ }
    }
    return { advanced: true, planComplete: !!r.planComplete };
  }
  return { advanced: false, planComplete: false };
}
```

- [ ] **Step 4: Запустить — зелёный**

Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/enforce-supreme-gate.test.mjs`
Expected: PASS (новые `runGatePost`-тесты зелёные; `runGatePre`/`isPostEvent`-импорт ещё может падать — это Tasks 2/4; если падает только на отсутствии этих экспортов, продолжаем).

---

### Task 2: `runGatePre` — ворота + журнал, без сдвига

**Files:**

- Modify: `tools/enforce-supreme-gate.mjs`
- Test: `tools/enforce-supreme-gate.test.mjs`

- [ ] **Step 1: Написать падающие тесты**

Добавить describe-блок:

```js
describe('runGatePre (ворота + журнал-намерение, БЕЗ сдвига)', () => {
  const baseArgs = (over) => ({
    event: { tool_name: 'Write', tool_input: { file_path: 'tools/foo.mjs' } },
    frozenPlan: { ...PLAN, judge_mode: 'live-block' },
    frozenArtifact: { sig: 'ok', judge_mode: 'live-block' },
    stepPtr: 0, key: KEY, verifyImpl: verifyStub, verifyArtifactImpl: verifyStub,
    normalize: (p) => p.toLowerCase(), journal: () => true, ...over,
  });

  it('allow+advance → журналирует намерение, block:false (указатель НЕ двигает — нет saveStep)', () => {
    const journaled = [];
    const r = runGatePre(baseArgs({ journal: (e) => { journaled.push(e); return true; } }));
    expect(r.block).toBe(false);
    expect(journaled).toHaveLength(1);
    expect(journaled[0].step).toBe(1);
  });

  it('несовпадение шага → block, не журналирует', () => {
    const journaled = [];
    const r = runGatePre(baseArgs({
      event: { tool_name: 'Write', tool_input: { file_path: 'tools/evil.mjs' } },
      journal: (e) => { journaled.push(e); return true; },
    }));
    expect(r.block).toBe(true);
    expect(journaled).toHaveLength(0);
  });

  it('Δ3: journal вернул false → block', () => {
    const r = runGatePre(baseArgs({ journal: () => false }));
    expect(r.block).toBe(true);
    expect(r.message).toMatch(/пред-запис|нет записи/i);
  });

  it('Δ3: journal бросил → block', () => {
    const r = runGatePre(baseArgs({ journal: () => { throw new Error('io'); } }));
    expect(r.block).toBe(true);
  });
});
```

- [ ] **Step 2: Запустить — убедиться, что падает**

Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/enforce-supreme-gate.test.mjs`
Expected: FAIL — `runGatePre is not a function`.

- [ ] **Step 3: Реализовать `runGatePre`**

В `tools/enforce-supreme-gate.mjs` перед функцией `runGate` добавить:

```js
/**
 * PreToolUse-такт (фикс рассинхрона): ТОЛЬКО ворота. decideMode → block при несовпадении;
 * escape/finishPlan — здесь (они от исполнения не зависят); на allow+advance —
 * журнал-намерение (Δ3, «нет записи → нет действия»), но указатель НЕ двигается
 * (сдвиг — в runGatePost после подтверждённого исполнения).
 */
export function runGatePre({ event, frozenPlan, frozenArtifact, stepPtr, key, verifyImpl, verifyArtifactImpl, normalize, journal, removeFrozenPlan, escapeGrants = [], escapeConsumed = [], now = Date.now() }) {
  const toolUse = { name: event.tool_name, input: event.tool_input };
  const r = decideMode({ toolUse, frozenPlan, frozenArtifact, stepPtr, key, verifyImpl, verifyArtifactImpl, normalize, escapeGrants, escapeConsumed, now });
  if (r.mode === 'escape') {
    if (typeof journal === 'function') {
      try { journal({ op: toolUse.name, object: actionOf(toolUse).object, step: stepPtr, at: event.nowMs ?? null, escape: true }); } catch { /* best-effort */ }
    }
    return { block: false, message: r.reason };
  }
  if (r.finishPlan) {
    if (typeof removeFrozenPlan === 'function') { try { removeFrozenPlan(); } catch { /* best-effort */ } }
    return { block: false, message: r.reason };
  }
  const withWarn = (msg) => (r.warn ? `${msg} ⚠ ${r.warnReason}` : msg);
  if (r.decision === 'allow' && r.advance === true) {
    let recorded;
    try {
      recorded = journal({ op: toolUse.name, object: actionOf(toolUse).object, step: stepPtr + 1, at: event.nowMs ?? null }) !== false;
    } catch { recorded = false; }
    if (!recorded) {
      return { block: true, message: 'Δ3: не удалось пред-записать намерение в журнал — действие не разрешено (нет записи → нет действия)' };
    }
    // Сдвиг указателя НЕ здесь — runGatePost после подтверждённого исполнения (фикс рассинхрона).
  }
  return { block: r.decision === 'block', message: withWarn(r.reason) };
}
```

- [ ] **Step 4: Запустить — зелёный**

Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/enforce-supreme-gate.test.mjs`
Expected: PASS для `runGatePre`-блока (isPostEvent-импорт ещё падает — Task 4).

---

### Task 3: `runGate` → compat-обёртка (Pre→Post); существующие тесты зелёные

**Files:**

- Modify: `tools/enforce-supreme-gate.mjs`
- Test: `tools/enforce-supreme-gate.test.mjs` (существующий блок `runGate (pure orchestration)` + escape — БЕЗ правок)

- [ ] **Step 1: Переписать тело `runGate` как обёртку**

Заменить всё тело текущей функции `runGate` (строки ≈342–387) на:

```js
/**
 * Совместимость/прямой вызов: Pre-такт, затем (если не блок) Post-такт — эквивалент
 * прежнего одно-проходного поведения для ПРЯМОГО вызова (тесты/инструменты). В реальной
 * хук-цепочке main() зовёт runGatePre (PreToolUse) и runGatePost (PostToolUse) РАЗДЕЛЬНО,
 * иначе поздний блок / user-deny рассинхронят указатель (спека 2026-06-14).
 */
export function runGate(args) {
  const pre = runGatePre(args);
  if (pre.block) return pre;
  runGatePost(args);
  return pre;
}
```

(`runGatePre`/`runGatePost` определены выше по файлу — порядок: сначала `runGatePre`,
затем `runGatePost`, затем `runGate`.)

- [ ] **Step 2: Запустить весь файл тестов — существующие runGate/escape зелёные**

Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/enforce-supreme-gate.test.mjs`
Expected: PASS — блок `runGate (pure orchestration)` (allow advances+journals / block / Δ3×2) и
escape-тесты («escape-allow … НЕ зовёт saveStep», journal-throw) проходят без изменений; новые
блоки тоже зелёные (кроме `isPostEvent` — Task 4).

---

### Task 4: `main()` ветвится по `hook_event_name` + `isPostEvent`

**Files:**

- Modify: `tools/enforce-supreme-gate.mjs`
- Test: `tools/enforce-supreme-gate.test.mjs`

- [ ] **Step 1: Тест на `isPostEvent`**

```js
describe('isPostEvent', () => {
  it('PostToolUse → true', () => { expect(isPostEvent({ hook_event_name: 'PostToolUse' })).toBe(true); });
  it('PreToolUse → false', () => { expect(isPostEvent({ hook_event_name: 'PreToolUse' })).toBe(false); });
  it('нет поля / null → false', () => { expect(isPostEvent({})).toBe(false); expect(isPostEvent(null)).toBe(false); });
});
```

- [ ] **Step 2: Запустить — падает (нет экспорта)**

Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/enforce-supreme-gate.test.mjs`
Expected: FAIL — `isPostEvent is not a function`.

- [ ] **Step 3: Добавить `isPostEvent` и переписать `main()`**

Добавить рядом с `runGate` (экспортируемая чистая функция):

```js
/** Различение такта по событию harness: PostToolUse → сдвиг; иначе → ворота. */
export function isPostEvent(event) {
  return !!event && event.hook_event_name === 'PostToolUse';
}
```

Заменить тело `main()` на (структура — как было, добавлена развилка Pre/Post):

```js
async function main() {
  let event = {}; let escapeGrants = []; let escapeConsumed = [];
  try {
    event = parseEventJson(await readStdin());
    const { resolveReceiptKey } = await import('./receipt-key-config.mjs');
    const { loadFrozenPlan, loadFrozenArtifact, removeFrozenPlan } = await import('./plan-lock.mjs');
    const { journalAppend } = await import('./action-journal.mjs');
    const os = await import('node:os'); const fs = await import('node:fs');
    const runtimeDir = `${os.homedir()}/.claude/runtime`;
    const sess = resolveSessionId(event); // R-28: из stdin-события
    escapeGrants = loadFloorEscapes(sess);   // G-1 α: read-only floor_escape-пропуски
    escapeConsumed = loadConsumed(sess);
    const key = resolveReceiptKey();
    const frozenPlan = loadFrozenPlan({ sessionId: sess, runtimeDir });
    const frozenArtifact = loadFrozenArtifact({ sessionId: sess, runtimeDir });
    const stepPath = stepStatePath(runtimeDir, sess); // N3-shared guard формы sessionId
    let stored = null; try { stored = JSON.parse(fs.readFileSync(stepPath, 'utf8')); } catch {}
    const stepPtr = resolveStepPtr(stored, frozenPlan?.plan_id, key ? (s) => verifyStepState(s, key) : null);
    const common = { event, frozenPlan, frozenArtifact, stepPtr, key, escapeGrants, escapeConsumed };
    if (isPostEvent(event)) {
      // PostToolUse-такт: сдвиг указателя по подтверждённому исполнению. Никогда не блокирует.
      runGatePost({ ...common,
        saveStep: (n) => fs.writeFileSync(stepPath, JSON.stringify(signStepState(frozenPlan?.plan_id ?? null, n, key))),
        removeFrozenPlan: () => removeFrozenPlan({ sessionId: sess, runtimeDir }),
      });
      exitDecision({ block: false });
      return;
    }
    // PreToolUse-такт: ворота + журнал-намерение (без сдвига).
    const r = runGatePre({ ...common,
      journal: (entry) => journalAppend({ payload: entry, key, sessionId: sess, runtimeDir }),
      removeFrozenPlan: () => removeFrozenPlan({ sessionId: sess, runtimeDir }),
    });
    if (r.block) logGuardBlock(event, 'М2 Стена', r.message);
    exitDecision({ block: r.block, message: r.block ? `[supreme-gate] ${r.message}` : undefined });
  } catch {
    // Post-такт: fail-safe (никогда не блокирует уже исполнённое), но fail-loud WARN.
    if (isPostEvent(event)) {
      try { process.stderr.write('[supreme-gate] PostToolUse: внутренняя ошибка — сдвиг указателя пропущен (fail-safe)\n'); } catch { /* ignore */ }
      exitDecision({ block: false });
      return;
    }
    // Pre-такт panic (правило 7б): escape владельца оценён, иначе fail-CLOSED.
    const p = panicEscapeDecision(event, escapeGrants, escapeConsumed);
    exitDecision({ block: p.block, message: p.block ? '[supreme-gate] внутренняя ошибка — fail-CLOSED' : undefined });
  }
}
```

- [ ] **Step 4: Запустить весь файл — зелёный**

Run: `npx vitest run --root app --config vitest.config.tools.mjs tools/enforce-supreme-gate.test.mjs`
Expected: PASS — все блоки (`runGatePre`, `runGatePost`, `runGate`, `isPostEvent`, существующие).

---

### Task 5: Полная регрессия tools-only + коммит

**Files:** (без правок кода)

- [ ] **Step 1: Полная регрессия**

Run: `npx vitest run --root app --config vitest.config.tools.mjs`
Expected: PASS, 0 регрессий (база ~3928–3936 passed + 2 skip; новые тесты добавляются сверху).
Точное число зафиксировать из вывода как новый baseline.

- [ ] **Step 2: Коммит (по разрешению владельца — «коммиты владелец»)**

```bash
git add tools/enforce-supreme-gate.mjs tools/enforce-supreme-gate.test.mjs
git commit -m "fix(wall): supreme-gate сдвигает указатель на PostToolUse (фикс рассинхрона)"
```

(Перед коммитом — спросить владельца, как в этой сессии; lefthook post-commit перегенерит
STATUS.md — это ожидаемо.)

---

## Acceptance (за пределами unit-тестов)

- **Регистрация владельцем:** добавить `enforce-supreme-gate` на **PostToolUse** matcher `'*'`
  в `settings.json` (PreToolUse-регистрация сохраняется) + вернуть стену из maintenance + перезапуск.
- **Сквозной live-смоук** (после регистрации): повторить церемонию тупого судьи навыков —
  объявить навык, не вызвать → первый мутирующий шаг блокируется skill-discipline, указатель
  НЕ уходит вперёд (Pre больше не двигает); вызвать навык → шаг проходит → PostToolUse двигает
  указатель. (Runtime закрыт — вердикты видит владелец.)

## Self-review (выполнено при написании плана)

- **Покрытие спеки:** D1 (расщепление) → Tasks 1-3; D2 (main) → Task 4; D3 (крайние случаи:
  RED/поздний-блок/user-deny — через runGatePost-семантику; идемпотентность — один Post;
  сбой Post — fail-safe в catch) → Tasks 1/4; D4 (конвенция, чистые экспорты) → весь код;
  D5 (TDD + регрессия) → Tasks 1-5; D6 (owner settings) → Acceptance.
- **Плейсхолдеров нет** — весь код приведён дословно.
- **Согласованность имён:** `runGatePre`/`runGatePost`/`isPostEvent`/`runGate` едины во всех задачах;
  поля `advanced`/`planComplete`/`block`/`advance`/`advanceTo` соответствуют `decide`.
