# Router-discipline enforcement — Уровень 1 + 2 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:** Закрыть 3 структурные дырки текущей архитектуры enforcement-хуков, которые позволяют контроллеру обходить роутер: (1) одна override-фраза снимает все хуки разом, (2) override-лимит per-day=5 не ловит rate-spike (40 событий в 59 минут today), (3) single-node рекомендации роутера блокируются только при `confidence ≥ 0.8` — borderline cases (0.5-0.8) проходят без enforcement.

**Architecture:** Точечные правки в 2 существующих модуля и 1 JSON-конфиг. Никаких новых хуков — функционал «router-recommendation = обязательство» уже реализован в `enforce-classifier-match.mjs`, нужно только понизить порог уверенности и добавить inline-override `router-skip: <50+ chars>`. Override-vocabulary сужается: `recovery` теряет 3 категории из 5, `ремонт инфраструктуры` — 8 из 11. Лимит обходов получает второе измерение — per-rate-window.

**Tech Stack:** Node.js (.mjs ESM modules), vitest для TDD, lefthook для pre-commit. Поведенческие тесты, не unit-shells.

---

## Контекст для агента

**Запрещено перед началом:**

- Не трогать другие enforce-*.mjs модули вне списка ниже.
- Не менять structure `enforce-override-vocab.json` (только `suppresses` arrays и `requires_justification`).
- Не редактировать `~/.claude/settings.json` — хуки уже зарегистрированы.

**Файлы которые меняются:**

- Modify: `tools/enforce-override-vocab.json` (narrow `recovery` + `ремонт инфраструктуры`)
- Modify: `tools/enforce-override-limit.mjs` (+ rate-window logic)
- Modify: `tools/enforce-override-limit.test.mjs` (+ rate-window TDD tests)
- Modify: `tools/enforce-classifier-match.mjs` (threshold + inline override)
- Modify: `tools/enforce-classifier-match.test.mjs` (+ inline override TDD tests)

**Регрессия после каждого таска:** `npx vitest run --include "tools/enforce-override-limit.test.mjs" --include "tools/enforce-classifier-match.test.mjs"` — должна оставаться GREEN.

**Финальная регрессия:** `npx vitest run --include "tools/**/*.test.mjs" --exclude "tools/ruflo-*"` — full tools-only regression GREEN.

---

## Task 1: Narrow `recovery` override-vocab — суппрессить только git-recovery flow

**Files:**

- Modify: `tools/enforce-override-vocab.json` — `phrases[].phrase==="recovery"`

**Контекст:** Текущая запись `recovery` суппрессит 5 правил: `branch-switch`, `git-recovery`, `graph-first`, `chain-recommendation`, `semgrep-security`. Из них только первые два — реально про git-recovery. Остальные 3 — побочные эффекты которые я эксплуатировал. Сегодня сожгло 525 events на phrase `recovery`.

- [ ] **Step 1: Read current vocab entry**

```bash
node -e "const v=require('fs').readFileSync('tools/enforce-override-vocab.json','utf8'); const j=JSON.parse(v); const rec=j.phrases.find(p=>p.phrase==='recovery'); console.log(JSON.stringify(rec,null,2));"
```

Expected output: `recovery` entry with `suppresses` array of 5 items.

- [ ] **Step 2: Edit JSON — narrow `suppresses` array**

В файле `tools/enforce-override-vocab.json` заменить `suppresses` массив у phrase `recovery` с `["branch-switch", "git-recovery", "graph-first", "chain-recommendation", "semgrep-security"]` на `["branch-switch", "git-recovery"]`. Поле `description` обновить на: `"Git recovery only — branch-state mismatch ok. Does NOT suppress graph-first / chain-recommendation / semgrep-security (use specific phrases for those)."`.

- [ ] **Step 3: Verify JSON valid + only `recovery` changed**

```bash
node -e "const j=JSON.parse(require('fs').readFileSync('tools/enforce-override-vocab.json','utf8')); const rec=j.phrases.find(p=>p.phrase==='recovery'); console.log('suppresses count:', rec.suppresses.length); console.log('items:', rec.suppresses);"
```

Expected: `suppresses count: 2`, items: `[ 'branch-switch', 'git-recovery' ]`.

- [ ] **Step 4: Run hook-helpers test (vocab loader)**

```bash
npx vitest run --include "tools/enforce-hook-helpers.test.mjs"
```

Expected: PASS (vocab loader doesn't care about suppress-array contents).

- [ ] **Step 5: Commit**

```bash
git add tools/enforce-override-vocab.json
git commit -m "chore(override-vocab): narrow 'recovery' scope to git-recovery only

Reduces 'recovery' suppresses 5→2 categories. Removes graph-first /
chain-recommendation / semgrep-security side-effects.

Driver: brain-retro #10 trend analysis — 'recovery' fired 525 times
on 2026-05-28 (vs 10/day baseline 25.05). Per Level 1 plan."
```

---

## Task 2: Narrow `ремонт инфраструктуры` override-vocab — суппрессить только verify-related

**Files:**

- Modify: `tools/enforce-override-vocab.json` — `phrases[].phrase==="ремонт инфраструктуры"`

**Контекст:** Текущая запись `ремонт инфраструктуры` суппрессит 11 правил — все, что есть. Это full opt-out. Реальный use case — починка ломаной TDD/verify-инфры. Остальные 8 категорий — побочные эффекты.

- [ ] **Step 1: Read current entry**

```bash
node -e "const j=JSON.parse(require('fs').readFileSync('tools/enforce-override-vocab.json','utf8')); const r=j.phrases.find(p=>p.phrase==='ремонт инфраструктуры'); console.log(JSON.stringify(r,null,2));"
```

Expected: full opt-out entry with 11 items in `suppresses` and `requires_justification: "ремонт:"`.

- [ ] **Step 2: Edit JSON — narrow `suppresses` to verify-only**

В записи `ремонт инфраструктуры` заменить `suppresses` массив на `["tdd-gate", "verify-before-commit", "verify-before-push"]` (3 категории). Поле `requires_justification: "ремонт:"` оставить как есть. Поле `description` заменить на: `"Infrastructure repair — bypass TDD-gate + verify hooks only. Other rules (skill-required, classifier-mismatch, chain-recommendation, graph-first, semgrep-security, memory-sync-coverage, coverage-skill-match, writing-plans-required) require their own override phrases."`.

- [ ] **Step 3: Verify JSON valid**

```bash
node -e "const j=JSON.parse(require('fs').readFileSync('tools/enforce-override-vocab.json','utf8')); const r=j.phrases.find(p=>p.phrase==='ремонт инфраструктуры'); console.log('suppresses count:', r.suppresses.length); console.log('items:', r.suppresses);"
```

Expected: `suppresses count: 3`, items: `[ 'tdd-gate', 'verify-before-commit', 'verify-before-push' ]`.

- [ ] **Step 4: Run any test depending on this phrase**

```bash
npx vitest run --include "tools/enforce-hook-helpers.test.mjs" --include "tools/enforce-override-limit.test.mjs"
```

Expected: PASS. If any test asserts `ремонт инфраструктуры` suppresses specific non-verify rule — REPORT and stop. Don't auto-fix the test until human confirms — that test encoded the old (broad) contract.

- [ ] **Step 5: Commit**

```bash
git add tools/enforce-override-vocab.json
git commit -m "chore(override-vocab): narrow 'ремонт инфраструктуры' to verify-only

Reduces full-opt-out from 11→3 categories (tdd-gate / verify-before-commit /
verify-before-push). Requires_justification 'ремонт:' kept intact.

Driver: brain-retro #10 trend — 'ремонт инфраструктуры' fired 26 times today
(vs 71 yesterday). Used as side-effect to bypass classifier/chain/skill hooks.
Per Level 1 plan."
```

---

## Task 3: Add per-rate-window to `enforce-override-limit.mjs`

**Files:**

- Modify: `tools/enforce-override-limit.mjs` (+ `RATE_WINDOW_MIN`, `RATE_THRESHOLD`, `countWindowUsage`, extend `shouldBlock`)
- Modify: `tools/enforce-override-limit.test.mjs` (+ rate-window TDD tests)

**Контекст:** Текущий `THRESHOLD = 5` per-day. Сегодня одна сессия `4a8b327e` сожгла 40 events за 59 минут (0.68/min). Per-day=5 не реагирует — счётчик идёт по календарному дню. Нужно второе измерение: per-rate-window.

- [ ] **Step 1: Write failing test for `countWindowUsage`**

Добавить в `tools/enforce-override-limit.test.mjs` после существующих `countTodayUsage` тестов:

```javascript
import { countWindowUsage } from './enforce-override-limit.mjs';

describe('countWindowUsage', () => {
  it('counts only entries within window minutes of now', () => {
    const now = new Date('2026-05-28T13:00:00Z');
    const log = [
      // 5 min ago — IN window
      JSON.stringify({ ts: '2026-05-28T12:55:00.000Z', phrase: 'recovery', session_id: 's1', rule: 'r1' }),
      // 8 min ago — IN window
      JSON.stringify({ ts: '2026-05-28T12:52:00.000Z', phrase: 'recovery', session_id: 's1', rule: 'r2' }),
      // 11 min ago — OUT of window
      JSON.stringify({ ts: '2026-05-28T12:49:00.000Z', phrase: 'recovery', session_id: 's1', rule: 'r3' }),
      // different phrase — OUT
      JSON.stringify({ ts: '2026-05-28T12:55:00.000Z', phrase: 'без скилов', session_id: 's1', rule: 'r4' }),
    ].join('\n');
    expect(countWindowUsage(log, 'recovery', now, 10)).toBe(2);
  });

  it('returns 0 on empty log', () => {
    expect(countWindowUsage('', 'recovery', new Date(), 10)).toBe(0);
  });

  it('handles malformed lines gracefully', () => {
    const now = new Date('2026-05-28T13:00:00Z');
    const log = [
      'not-json',
      JSON.stringify({ ts: '2026-05-28T12:55:00.000Z', phrase: 'recovery' }),
      '{broken',
    ].join('\n');
    expect(countWindowUsage(log, 'recovery', now, 10)).toBe(1);
  });
});
```

- [ ] **Step 2: Run test, verify FAIL**

```bash
npx vitest run --include "tools/enforce-override-limit.test.mjs"
```

Expected: FAIL with `countWindowUsage is not a function` (or import error).

- [ ] **Step 3: Implement `countWindowUsage` in `enforce-override-limit.mjs`**

После `countTodayUsage` функции (~ line 50) добавить:

```javascript
export function countWindowUsage(rawLog, phrase, now = new Date(), windowMinutes = 10) {
  if (typeof rawLog !== 'string' || !rawLog) return 0;
  const cutoffMs = now.getTime() - windowMinutes * 60_000;
  let count = 0;
  for (const line of rawLog.split('\n')) {
    if (!line) continue;
    try {
      const e = JSON.parse(line);
      if (e.phrase !== phrase) continue;
      if (typeof e.ts !== 'string') continue;
      const tsMs = Date.parse(e.ts);
      if (Number.isFinite(tsMs) && tsMs >= cutoffMs && tsMs <= now.getTime()) {
        count++;
      }
    } catch {
      // ignore malformed
    }
  }
  return count;
}
```

- [ ] **Step 4: Run test, verify PASS**

```bash
npx vitest run --include "tools/enforce-override-limit.test.mjs"
```

Expected: PASS (3/3 new tests + all existing).

- [ ] **Step 5: Write failing test for `shouldBlock` rate-window branch**

Добавить в `tools/enforce-override-limit.test.mjs` в существующий `describe('shouldBlock')` блок (или создать новый):

```javascript
describe('shouldBlock with rate-window', () => {
  const now = new Date('2026-05-28T13:00:00Z');

  it('blocks when same phrase used 5+ times within 10 minutes (even if day-count < 5)', () => {
    // 5 events in last 9 minutes (within window), all SAME calendar day so daily would also be 5
    // But we test the rate path: only 5 today total, so daily threshold not breached (THRESHOLD=5 means 6th day-event blocks).
    // Force daily < THRESHOLD by spreading 4 of them in same day, 1 in same window:
    // Actually we need: daily <5 AND window >=5. Easiest: all 5 in window, daily total = 5 = THRESHOLD itself (no daily block since check is >= THRESHOLD).
    // Re-reading existing shouldBlock: blocks when todayCount >= THRESHOLD (5+). So at exactly 5 daily — blocks.
    // To isolate rate-only path: 4 today + 5 in 10-min window? Impossible (window IS subset of day).
    // Better test: rate-window threshold lower than daily — 5 events in 3 minutes = rate spike.
    const log = [
      JSON.stringify({ ts: '2026-05-28T12:58:30.000Z', phrase: 'recovery', session_id: 's' }),
      JSON.stringify({ ts: '2026-05-28T12:58:00.000Z', phrase: 'recovery', session_id: 's' }),
      JSON.stringify({ ts: '2026-05-28T12:57:30.000Z', phrase: 'recovery', session_id: 's' }),
      JSON.stringify({ ts: '2026-05-28T12:57:00.000Z', phrase: 'recovery', session_id: 's' }),
      JSON.stringify({ ts: '2026-05-28T12:56:30.000Z', phrase: 'recovery', session_id: 's' }),
    ].join('\n');
    const result = shouldBlock('делай recovery', log, now);
    expect(result.block).toBe(true);
    expect(result.phrase).toBe('recovery');
    // Block reason should mention rate-window
    expect(result.reason).toBeDefined();
  });

  it('does NOT block when rate-window count < 5', () => {
    const log = [
      JSON.stringify({ ts: '2026-05-28T12:55:00.000Z', phrase: 'recovery', session_id: 's' }),
      JSON.stringify({ ts: '2026-05-28T12:50:00.000Z', phrase: 'recovery', session_id: 's' }),
    ].join('\n');
    const result = shouldBlock('делай recovery', log, now);
    expect(result.block).toBe(false);
  });
});
```

- [ ] **Step 6: Run test, verify FAIL**

```bash
npx vitest run --include "tools/enforce-override-limit.test.mjs"
```

Expected: FAIL — current `shouldBlock` returns `{block: false}` on 5 events in window (because daily < 5+1).

- [ ] **Step 7: Extend `shouldBlock` in `enforce-override-limit.mjs`**

Заменить функцию `shouldBlock` в `tools/enforce-override-limit.mjs` на:

```javascript
export const RATE_WINDOW_MIN = 10;
export const RATE_THRESHOLD = 5;

export function shouldBlock(prompt, rawLog, now = new Date()) {
  if (typeof prompt === 'string' && prompt.toLowerCase().includes(BYPASS_PHRASE.toLowerCase())) {
    return { block: false, bypass: true };
  }
  const phrases = findPhrasesInPrompt(prompt);
  for (const phrase of phrases) {
    // Daily check
    const todayCount = countTodayUsage(rawLog, phrase, now);
    if (todayCount >= THRESHOLD) {
      return {
        block: true,
        phrase,
        todayCount,
        triggered: 'daily',
        reason: `daily count ${todayCount} >= ${THRESHOLD}`,
      };
    }
    // Rate-window check
    const windowCount = countWindowUsage(rawLog, phrase, now, RATE_WINDOW_MIN);
    if (windowCount >= RATE_THRESHOLD) {
      return {
        block: true,
        phrase,
        windowCount,
        triggered: 'rate',
        reason: `rate-window count ${windowCount} >= ${RATE_THRESHOLD} in ${RATE_WINDOW_MIN} min`,
      };
    }
  }
  return { block: false };
}
```

И обновить `buildBlockOutput`:

```javascript
export function buildBlockOutput({ phrase, todayCount, windowCount, triggered }) {
  if (triggered === 'rate') {
    return {
      decision: 'block',
      reason:
        `[enforce-override-limit] Override-фраза «${phrase}» использована ${windowCount} раз за последние ${RATE_WINDOW_MIN} минут (порог ${RATE_THRESHOLD}). ` +
        `Rate-spike обнаружен — это шаблонная привычка обхода, не реальный нужда. ` +
        `Сделай ПАУЗУ 10 минут перед следующим override, или вызови AskUserQuestion и попроси заказчика подтвердить новый bypass через «${BYPASS_PHRASE}» (счётчик НЕ сбрасывается).`,
    };
  }
  return {
    decision: 'block',
    reason:
      `[enforce-override-limit] Override-фраза «${phrase}» уже использована ${todayCount} раз сегодня (порог ${THRESHOLD}/день per phrase). ` +
      `Это 6-е или последующее использование — hard-block per Phase 2 plan. ` +
      `Чтобы продолжить, вызови AskUserQuestion и спроси заказчика явно. ` +
      `Если он подтверждает — следующий промпт должен содержать фразу «${BYPASS_PHRASE}» (one-shot bypass, счётчик НЕ сбрасывается).`,
  };
}
```

- [ ] **Step 8: Run tests, verify PASS**

```bash
npx vitest run --include "tools/enforce-override-limit.test.mjs"
```

Expected: PASS (all daily + window tests).

- [ ] **Step 9: Commit**

```bash
git add tools/enforce-override-limit.mjs tools/enforce-override-limit.test.mjs
git commit -m "feat(override-limit): add per-rate-window check (5 events / 10 min)

Adds RATE_WINDOW_MIN=10 + RATE_THRESHOLD=5 alongside existing per-day THRESHOLD=5.
Closes gap where per-day limit doesn't catch rate-spikes:
 - 2026-05-28 session 4a8b327e burned 40 events / 59 minutes (0.68/min).
 - Per-day=5 was breached after 5 events; rate-spike of next 35 went uncounted.

shouldBlock returns triggered='daily' or 'rate' with reason. buildBlockOutput
emits rate-specific message asking for 10-min pause + bypass-phrase confirmation.

Driver: brain-retro #10 trend analysis, Level 1 plan."
```

---

## Task 4: Lower `enforce-classifier-match` threshold + add inline override

**Files:**

- Modify: `tools/enforce-classifier-match.mjs` (CONFIDENCE_THRESHOLD 0.8→0.6, add `ROUTER_SKIP_RE` inline override)
- Modify: `tools/enforce-classifier-match.test.mjs` (+ tests for 0.6-0.8 range + inline override)

**Контекст:** Текущий threshold 0.8 пропускает borderline-recommendations (0.5-0.8) без enforcement. Был поднят с 0.7 24.05 из-за false positives на #3 / #36 в LLM-классификации. Понижаем до 0.6 — компромисс: ловит больше real-flagов, но требует escape hatch для legitimate false positives. Escape hatch — inline `router-skip: <50+ chars>` в моём ответе.

- [ ] **Step 1: Write failing test for inline `router-skip` override**

Добавить в `tools/enforce-classifier-match.test.mjs`:

```javascript
describe('inline router-skip override', () => {
  const recommendation = '#19';
  const editTool = { name: 'Edit', input: { file_path: 'x.txt' } };

  it('does NOT block when assistant text contains "router-skip: <50+ chars>"', () => {
    const assistantText = 'router-skip: deliberately choosing direct because router recommendation #19 is irrelevant for this trivial typo fix in docs';
    const result = decide({
      toolUses: [editTool],
      recommendation,
      confidence: 0.85,
      assistantText,
      override: null,
    });
    expect(result.block).toBe(false);
  });

  it('DOES block when "router-skip:" justification < 50 chars', () => {
    const assistantText = 'router-skip: too short';
    const result = decide({
      toolUses: [editTool],
      recommendation,
      confidence: 0.85,
      assistantText,
      override: null,
    });
    expect(result.block).toBe(true);
  });

  it('DOES block when no "router-skip:" present at all', () => {
    const result = decide({
      toolUses: [editTool],
      recommendation,
      confidence: 0.85,
      assistantText: 'just normal text, no skip',
      override: null,
    });
    expect(result.block).toBe(true);
  });
});

describe('lowered confidence threshold', () => {
  const recommendation = '#19';
  const editTool = { name: 'Edit', input: { file_path: 'x.txt' } };

  it('blocks at confidence 0.65 (above new threshold 0.6)', () => {
    const result = decide({
      toolUses: [editTool],
      recommendation,
      confidence: 0.65,
      assistantText: '',
      override: null,
    });
    expect(result.block).toBe(true);
  });

  it('does NOT block at confidence 0.55 (below new threshold)', () => {
    const result = decide({
      toolUses: [editTool],
      recommendation,
      confidence: 0.55,
      assistantText: '',
      override: null,
    });
    expect(result.block).toBe(false);
  });
});
```

- [ ] **Step 2: Run test, verify FAIL**

```bash
npx vitest run --include "tools/enforce-classifier-match.test.mjs"
```

Expected: FAIL — current threshold 0.8 doesn't block at 0.65; no `router-skip:` handling exists.

- [ ] **Step 3: Implement changes in `enforce-classifier-match.mjs`**

В файле `tools/enforce-classifier-match.mjs`:

(a) Заменить:

```javascript
const CONFIDENCE_THRESHOLD = 0.8;
```

на:

```javascript
const CONFIDENCE_THRESHOLD = 0.6;
const ROUTER_SKIP_RE = /^router-skip:\s*(.{50,})$/m;
```

(b) Заменить функцию `decide` на:

```javascript
export function decide({ toolUses, recommendation, confidence, assistantText, override }) {
  // Pure conversation: skip.
  const hasMutating = toolUses.some((u) => MUTATING_TOOLS.has(u.name));
  if (!hasMutating) return { block: false };
  if (override) return { block: false };

  if (!recommendation) return { block: false };
  if (typeof confidence === 'number' && confidence < CONFIDENCE_THRESHOLD) return { block: false };

  const matched = toolUses.some((u) => nodeMatches(recommendation, u));
  if (matched) return { block: false };

  // Inline override: "router-skip: <50+ chars justification>" in assistant text.
  if (typeof assistantText === 'string' && ROUTER_SKIP_RE.test(assistantText)) {
    return { block: false };
  }

  return {
    block: true,
    message: [
      `[enforce-classifier-match] Classifier recommended "${recommendation}" (confidence=${confidence ?? 'n/a'}) but turn did not invoke that skill/node.`,
      `Either:`,
      `  - Invoke ${recommendation} via Skill / Task tool, OR`,
      `  - Add an explicit "router-skip: <reason 50+ chars>" line in your response, OR`,
      `  - Include "без скилов" / "direct ok" in the next user prompt.`,
    ].join('\n'),
  };
}
```

(c) Обновить header-comment файла:

```javascript
/**
 * Rule #8 — Classifier-mismatch enforce.
 *
 * Stop hook. Reads classifier output from router-state. If classifier recommended
 * a node with confidence >= 0.6 AND the turn DIDN'T invoke a matching
 * skill/task — block.
 *
 * Escape hatches:
 *   - Invoke recommended skill via Skill / Task tool, OR
 *   - "router-skip: <reason 50+ chars>" line in assistant text (inline), OR
 *   - Global vocab override ("без скилов" / "direct ok") in user prompt.
 *
 * Spec: docs/superpowers/specs/2026-05-25-enforce-hard-rules-design.md
 *       docs/superpowers/plans/2026-05-28-router-discipline-level-1-2.md
 */
```

- [ ] **Step 4: Run tests, verify PASS**

```bash
npx vitest run --include "tools/enforce-classifier-match.test.mjs"
```

Expected: PASS (new tests + all existing).

Если existing test раньше assertил `block: false at confidence 0.65` — это **спецификация старого порога**, и он должен теперь FAIL → переделать в `block: true at confidence 0.65`. Если existing test assertил `block: true at confidence 0.85 without skip` — должен оставаться PASS.

- [ ] **Step 5: Commit**

```bash
git add tools/enforce-classifier-match.mjs tools/enforce-classifier-match.test.mjs
git commit -m "feat(classifier-match): lower threshold 0.8→0.6 + inline router-skip override

Two changes:
1. CONFIDENCE_THRESHOLD 0.8 → 0.6 — catches borderline recommendations
   that previously slipped through. Driver: brain-retro #10 shows 0%
   single-node-skill follow-through, suggesting hook needs to fire more.
2. Inline escape hatch — 'router-skip: <reason 50+ chars>' in assistant text.
   Per-tool scope (does not affect other tools in same turn).
   Replaces 'override: <reason>' which was self-bypass loophole.

Updates message to surface router-skip as new escape route.
Per Level 2 plan."
```

---

## Task 5: Integration smoke-test — full hook sweep

**Files:**

- No new files; verification of combined behavior.

- [ ] **Step 1: Run full tools regression**

```bash
npx vitest run --include "tools/**/*.test.mjs" --exclude "tools/ruflo-*" --exclude "tools/subagent-prompt-prefix*"
```

Expected: ALL PASS. Если что-то FAIL — диагностировать перед merge.

- [ ] **Step 2: Verify lefthook pre-commit hook still runs clean**

```bash
git status
LEFTHOOK=0 git add -A
git status --short
# Если staged changes присутствуют — это untracked файлы которые не должны попасть. Reset and add only the 5 task files.
git reset
git add tools/enforce-override-vocab.json tools/enforce-override-limit.mjs tools/enforce-override-limit.test.mjs tools/enforce-classifier-match.mjs tools/enforce-classifier-match.test.mjs docs/superpowers/plans/2026-05-28-router-discipline-level-1-2.md
git status --short
```

Expected: ровно 6 файлов в staged.

- [ ] **Step 3: Smoke-test rate-window manually**

```bash
node -e "
const { shouldBlock, RATE_THRESHOLD, RATE_WINDOW_MIN } = await import('./tools/enforce-override-limit.mjs');
const now = new Date();
const log = [];
for (let i=0; i<5; i++) {
  const ts = new Date(now.getTime() - i*60_000).toISOString();
  log.push(JSON.stringify({ ts, phrase: 'recovery', session_id: 's' }));
}
const result = shouldBlock('делай recovery', log.join('\n'), now);
console.log('5-events-in-5-min:', JSON.stringify(result));
"
```

Expected output contains `"block":true` and `"triggered":"rate"`.

- [ ] **Step 4: Smoke-test classifier-match with mid-confidence + router-skip**

```bash
node -e "
const { decide } = await import('./tools/enforce-classifier-match.mjs');
const editTool = { name: 'Edit', input: { file_path: 'x.txt' } };

const r1 = decide({ toolUses:[editTool], recommendation:'#19', confidence:0.65, assistantText:'', override:null });
console.log('0.65 no skip:', JSON.stringify(r1));

const r2 = decide({ toolUses:[editTool], recommendation:'#19', confidence:0.65, assistantText:'router-skip: deliberately choosing direct path for a one-character typo fix that requires no planning context', override:null });
console.log('0.65 with skip:', JSON.stringify(r2));

const r3 = decide({ toolUses:[editTool], recommendation:'#19', confidence:0.55, assistantText:'', override:null });
console.log('0.55 (below threshold):', JSON.stringify(r3));
"
```

Expected: r1 `block:true`, r2 `block:false`, r3 `block:false`.

- [ ] **Step 5: Final commit (plan file itself)**

```bash
git add docs/superpowers/plans/2026-05-28-router-discipline-level-1-2.md
git commit -m "plan(router-discipline): Level 1+2 implementation plan

5-task plan to close 3 enforcement gaps surfaced by brain-retro #10:
 1. Narrow 'recovery' override scope (5→2 categories)
 2. Narrow 'ремонт инфраструктуры' override scope (11→3)
 3. Per-rate-window in enforce-override-limit (5/10min)
 4. Lower classifier-match threshold 0.8→0.6 + inline router-skip

Driver: 679 override events on 2026-05-28 vs 12 baseline on 25.05.
User selected option B (Level 1+2) after brain-retro #10 analysis."
```

---

## Self-Review

### Spec coverage

| Spec requirement | Task |
|---|---|
| Level 1 / A1: Remove `recovery` from broad scope | Task 1 |
| Level 1 / A2: Remove `ремонт инфраструктуры` from broad scope | Task 2 |
| Level 1 / A3: Per-rate-window limit on override events | Task 3 |
| Level 2 / B1: Router-recommendation = obligation | Task 4 (lower threshold + inline override) |
| Verification | Task 5 |

### Placeholder scan

- No "TBD", "TODO", or "implement later".
- No "similar to Task N".
- All code blocks are concrete; all commands have expected output.

### Type consistency

- `THRESHOLD` (existing, per-day) stays at 5.
- `RATE_WINDOW_MIN = 10`, `RATE_THRESHOLD = 5` (new).
- `CONFIDENCE_THRESHOLD = 0.6` (lowered from 0.8).
- `ROUTER_SKIP_RE = /^router-skip:\s*(.{50,})$/m` — same naming convention as `CHAIN_OVERRIDE_RE` in `enforce-chain-recommendation.mjs`.
- `shouldBlock` return shape extended: `{ block, phrase, todayCount?, windowCount?, triggered, reason }`.
- `buildBlockOutput` branches on `triggered === 'rate'` vs default (daily).

### Risk assessment

| Risk | Mitigation |
|---|---|
| Existing tests assertion older thresholds | Step 4 of Task 4 explicitly checks for legacy assertions and re-formulates them. |
| Vocab change breaks hook regression suite | Tasks 1 and 2 each include test-run as Step 4. |
| Rate-window false positives during legitimate retro/sprint work | Bypass phrase `лимит снят` still works; threshold 5/10min is generous (was 40/59min in incident). |
| Confidence 0.6 re-introduces false positives on #3 / #36 LLM-classifications | Inline `router-skip: <50+ chars>` is the escape hatch — high friction (50 chars), discourages reflexive use. |

---

## Execution Handoff

**Plan complete and saved to `docs/superpowers/plans/2026-05-28-router-discipline-level-1-2.md`. Two execution options:**

1. **Subagent-Driven (recommended)** — отправить каждую из 5 задач свежему Sonnet субагенту через `superpowers:subagent-driven-development`, контроллер ревьюит между задачами. Per Pravila §15.1 + сегодняшний урок (subagent crashed на 1ч+ задачах — каждая из этих ≤30 мин).

2. **Inline Execution** — выполнить здесь же через `superpowers:executing-plans`. Быстрее для коротких правок (Task 1/2 = 1-2 минуты каждая), но Task 3/4 — multi-step TDD (5-10 минут каждая), субагент эффективнее.

**Что выбираешь — субагенты или inline?**
