# Observer factor-analysis — phase 1.1 (`user_chose_from_options`) 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-й kind `decision_provenance.kind = user_chose_from_options` к observer schema v2 (детектор, parser-integration, routing-gate, factor-matrix). Устраняет смешение «навязанный метод» vs «collaborative choice из моих предложенных вариантов» в факторном анализе.

**Architecture:** новый чистый модуль `tools/observer-choice-detector.mjs` (extract options + detect user reference). Подключается в `observer-transcript-parser.mjs` ПЕРЕД существующим `detectMethodDirected`. `routingGateDecision` в Stop-хуке расширяется — НЕ блокирует new kind (collaborative choice). `brain-retro-analyzer` factor-matrix получает 3-е значение оси `provenance_kind`.

**Tech Stack:** Node.js ESM, Vitest (через `app/vitest.config.tools.mjs`), pure functions без I/O.

**Spec:** `docs/superpowers/specs/2026-05-19-observer-factor-analysis-design.md` §11 (v1.1, accepted, commit `0c8d0fa`).

---

## File Structure

| Файл | Действие | Ответственность |
|---|---|---|
| `tools/observer-choice-detector.mjs` | Create | Pure: `extractOptions` / `detectReference` / `detectChoiceProvenance` |
| `tools/observer-choice-detector.test.mjs` | Create | Unit-тесты для всех 3-х функций + negative cases |
| `tools/observer-transcript-parser.mjs` | Modify | Вызов `detectChoiceProvenance` ДО `parseRoutingTag` + сборка `decision_provenance` |
| `tools/observer-transcript-parser.test.mjs` | Modify | +тесты для `user_chose_from_options` ветки |
| `tools/observer-stop-hook.mjs` | Modify | `routingGateDecision` — return no-block для `user_chose_from_options` |
| `tools/observer-stop-hook.test.mjs` | Modify | +тест на no-block для new kind |
| `tools/brain-retro-analyzer.mjs` | Modify | Factor matrix — 3-значная ось `provenance_kind` (если whitelisted) |
| `tools/brain-retro-analyzer.test.mjs` | Modify | +тест на 3-значное распределение по оси |
| `docs/Pravila_raboty_Claude_v1_1.md` | Modify | §16.2 — расширить `kind` ∈ 3 значения; bump v1.32 → v1.33 + entry |
| `docs/superpowers/specs/2026-05-19-brain-governance-design.md` | Modify | Cross-ref на spec §11 (если ещё нет) |
| `CLAUDE.md` | Modify (через `/claude-md-management:claude-md-improver`) | §0 Pravila row v1.32 → v1.33; §3.6 +упоминание 3-го kind; §9 +entry |

---

## Task 1: `observer-choice-detector.mjs` — pure module + TDD

**Файлы:**

- Create: `tools/observer-choice-detector.mjs`
- Test: `tools/observer-choice-detector.test.mjs`

**Контекст для subagent'а:** Pure ESM-модуль без побочных эффектов. Подключается в `observer-transcript-parser.mjs` (Task 2). Дизайн — из spec §11.3.

- [ ] **Step 1.1: Failing tests — extractOptions**

Создать `tools/observer-choice-detector.test.mjs`:

```javascript
import { describe, it, expect } from 'vitest';
import { extractOptions, detectReference, detectChoiceProvenance } from './observer-choice-detector.mjs';

describe('extractOptions', () => {
  it('returns null when content has fewer than 2 options', () => {
    expect(extractOptions('Just one paragraph of text.')).toBeNull();
    expect(extractOptions('1. only one item')).toBeNull();
  });

  it('extracts numbered list (1. ... 2. ...)', () => {
    const text = '1. First option\n2. Second option\n3. Third option';
    expect(extractOptions(text)).toEqual(['First option', 'Second option', 'Third option']);
  });

  it('extracts numbered list (1) ... 2) ...)', () => {
    const text = '1) Alpha\n2) Beta';
    expect(extractOptions(text)).toEqual(['Alpha', 'Beta']);
  });

  it('extracts lettered list (Latin)', () => {
    const text = 'A. First choice\nB. Second choice\nC. Third choice';
    expect(extractOptions(text)).toEqual(['First choice', 'Second choice', 'Third choice']);
  });

  it('extracts lettered list (Cyrillic)', () => {
    const text = 'А. Первый\nБ. Второй\nВ. Третий';
    expect(extractOptions(text)).toEqual(['Первый', 'Второй', 'Третий']);
  });

  it('extracts bullet list with hyphens', () => {
    const text = '- Apple\n- Banana\n- Cherry';
    expect(extractOptions(text)).toEqual(['Apple', 'Banana', 'Cherry']);
  });

  it('extracts bullet list with asterisks', () => {
    const text = '* Red\n* Green\n* Blue';
    expect(extractOptions(text)).toEqual(['Red', 'Green', 'Blue']);
  });

  it('extracts AskUserQuestion options from tool_use block', () => {
    const askUser = {
      type: 'tool_use',
      name: 'AskUserQuestion',
      input: {
        questions: [{
          options: [
            { label: 'Option A' },
            { label: 'Option B' },
            { label: 'Option C' },
          ],
        }],
      },
    };
    expect(extractOptions(askUser)).toEqual(['Option A', 'Option B', 'Option C']);
  });

  it('returns null when AskUserQuestion has < 2 options', () => {
    const askUser = {
      type: 'tool_use',
      name: 'AskUserQuestion',
      input: { questions: [{ options: [{ label: 'Only one' }] }] },
    };
    expect(extractOptions(askUser)).toBeNull();
  });
});
```

- [ ] **Step 1.2: Run tests — verify FAIL**

```powershell
cd app
npx vitest run --config vitest.config.tools.mjs observer-choice-detector
```

Expected: FAIL with "Cannot find module" (`observer-choice-detector.mjs` ещё не создан).

- [ ] **Step 1.3: Implement `extractOptions`**

Создать `tools/observer-choice-detector.mjs`:

```javascript
const NUMBERED_RE = /^(\d+)[.\)]\s+(.+)$/;
const LETTERED_RE = /^([A-Za-zА-Яа-я])[.\)]\s+(.+)$/;
const BULLET_RE = /^[-*]\s+(.+)$/;

function extractFromText(text) {
  const lines = String(text).split(/\r?\n/);
  const numbered = [];
  const lettered = [];
  const bulleted = [];
  for (const line of lines) {
    const trimmed = line.trim();
    let m;
    if ((m = NUMBERED_RE.exec(trimmed))) numbered.push(m[2].trim());
    else if ((m = LETTERED_RE.exec(trimmed))) lettered.push(m[2].trim());
    else if ((m = BULLET_RE.exec(trimmed))) bulleted.push(m[1].trim());
  }
  if (numbered.length >= 2) return numbered;
  if (lettered.length >= 2) return lettered;
  if (bulleted.length >= 2) return bulleted;
  return null;
}

function extractFromAskUser(toolUse) {
  if (!toolUse || toolUse.type !== 'tool_use' || toolUse.name !== 'AskUserQuestion') return null;
  const questions = toolUse.input?.questions;
  if (!Array.isArray(questions) || questions.length === 0) return null;
  const options = questions[0]?.options;
  if (!Array.isArray(options) || options.length < 2) return null;
  return options.map((o) => o?.label).filter((l) => typeof l === 'string');
}

export function extractOptions(content) {
  if (content == null) return null;
  if (typeof content === 'string') return extractFromText(content);
  if (typeof content === 'object') {
    const fromTool = extractFromAskUser(content);
    if (fromTool && fromTool.length >= 2) return fromTool;
    if (Array.isArray(content)) {
      for (const block of content) {
        const r = extractOptions(block);
        if (r) return r;
      }
    }
    if (typeof content.text === 'string') return extractFromText(content.text);
  }
  return null;
}
```

- [ ] **Step 1.4: Run tests — verify GREEN**

```powershell
cd app
npx vitest run --config vitest.config.tools.mjs observer-choice-detector
```

Expected: 9/9 PASS for `extractOptions`.

- [ ] **Step 1.5: Failing tests — detectReference**

Добавить в `tools/observer-choice-detector.test.mjs`:

```javascript
describe('detectReference', () => {
  const options = ['First option label', 'Second option label', 'Third option label'];

  it('returns null on empty prompt', () => {
    expect(detectReference('', options)).toBeNull();
    expect(detectReference('   ', options)).toBeNull();
  });

  it('matches bare number at start', () => {
    expect(detectReference('1', options)).toEqual({ index: 0, label: 'First option label' });
    expect(detectReference('2 экономия 5%', options)).toEqual({ index: 1, label: 'Second option label' });
    expect(detectReference('3, делаем', options)).toEqual({ index: 2, label: 'Third option label' });
  });

  it('matches verb-prefixed number', () => {
    expect(detectReference('делай 1', options)).toEqual({ index: 0, label: 'First option label' });
    expect(detectReference('выбираю 2', options)).toEqual({ index: 1, label: 'Second option label' });
    expect(detectReference('беру 3', options)).toEqual({ index: 2, label: 'Third option label' });
    expect(detectReference('хочу 1', options)).toEqual({ index: 0, label: 'First option label' });
    expect(detectReference('вариант 2', options)).toEqual({ index: 1, label: 'Second option label' });
  });

  it('matches bare letter at start (Latin)', () => {
    expect(detectReference('A', options)).toEqual({ index: 0, label: 'First option label' });
    expect(detectReference('B делаем', options)).toEqual({ index: 1, label: 'Second option label' });
    expect(detectReference('c, идём', options)).toEqual({ index: 2, label: 'Third option label' });
  });

  it('matches bare letter at start (Cyrillic)', () => {
    expect(detectReference('А', options)).toEqual({ index: 0, label: 'First option label' });
    expect(detectReference('б делаем', options)).toEqual({ index: 1, label: 'Second option label' });
    expect(detectReference('в, идём', options)).toEqual({ index: 2, label: 'Third option label' });
  });

  it('matches verb-prefixed letter', () => {
    expect(detectReference('делай A', options)).toEqual({ index: 0, label: 'First option label' });
    expect(detectReference('выбираю Б', options)).toEqual({ index: 1, label: 'Second option label' });
  });

  it('matches label substring (first 2-4 words)', () => {
    expect(detectReference('First option уточни', options)).toEqual({ index: 0, label: 'First option label' });
    expect(detectReference('делаем Second option', options)).toEqual({ index: 1, label: 'Second option label' });
  });

  it('returns null when no signal matches', () => {
    expect(detectReference('какой-то текст без ссылки', options)).toBeNull();
    expect(detectReference('просто продолжай работу', options)).toBeNull();
  });

  it('returns null on out-of-range position', () => {
    expect(detectReference('99', options)).toBeNull();
    expect(detectReference('Z', options)).toBeNull();
    expect(detectReference('Я', options)).toBeNull();
  });
});
```

- [ ] **Step 1.6: Run tests — verify FAIL**

```powershell
cd app
npx vitest run --config vitest.config.tools.mjs observer-choice-detector
```

Expected: 9 PASS, 9 FAIL.

- [ ] **Step 1.7: Implement `detectReference`**

Добавить в `tools/observer-choice-detector.mjs`:

```javascript
const VERBS = ['делай', 'выбираю', 'выбери', 'беру', 'хочу', 'вариант', 'option', 'pick', 'choose'];
const VERBS_RE = new RegExp('^(?:' + VERBS.join('|') + ')\\s+', 'i');

const LATIN_TO_INDEX = {
  a: 0, b: 1, c: 2, d: 3, e: 4, f: 5, g: 6, h: 7,
};
const CYR_TO_INDEX = {
  'а': 0, 'б': 1, 'в': 2, 'г': 3, 'д': 4, 'е': 5, 'ж': 6, 'з': 7,
};

function tryPosition(prompt, options) {
  const lowered = prompt.toLowerCase();
  const stripped = lowered.replace(VERBS_RE, '');
  const m = stripped.match(/^(\d+|[a-zа-я])(?=[\s,\.\):;-]|$)/);
  if (!m) return null;
  const token = m[1];
  let idx;
  if (/^\d+$/.test(token)) {
    idx = parseInt(token, 10) - 1;
  } else {
    idx = LATIN_TO_INDEX[token] ?? CYR_TO_INDEX[token];
  }
  if (idx == null || idx < 0 || idx >= options.length) return null;
  return { index: idx, label: options[idx] };
}

function trySubstring(prompt, options) {
  const lowered = prompt.toLowerCase();
  for (let i = 0; i < options.length; i++) {
    const opt = String(options[i]).toLowerCase();
    const words = opt.split(/\s+/).filter(Boolean);
    if (words.length < 2) continue;
    for (let n = Math.min(4, words.length); n >= 2; n--) {
      const prefix = words.slice(0, n).join(' ');
      if (lowered.includes(prefix)) return { index: i, label: options[i] };
    }
  }
  return null;
}

export function detectReference(prompt, options) {
  const text = String(prompt || '').trim();
  if (!text || !Array.isArray(options) || options.length < 2) return null;
  return tryPosition(text, options) ?? trySubstring(text, options);
}
```

- [ ] **Step 1.8: Run tests — verify GREEN**

```powershell
cd app
npx vitest run --config vitest.config.tools.mjs observer-choice-detector
```

Expected: 18/18 PASS.

- [ ] **Step 1.9: Failing tests — detectChoiceProvenance**

Добавить в `tools/observer-choice-detector.test.mjs`:

```javascript
describe('detectChoiceProvenance', () => {
  it('returns null when no options', () => {
    expect(detectChoiceProvenance('1 делаем', 'just text without list')).toBeNull();
  });

  it('returns null when options exist but no reference', () => {
    const lastAsst = '1. First\n2. Second\n3. Third';
    expect(detectChoiceProvenance('расскажи как дела', lastAsst)).toBeNull();
  });

  it('returns full result on successful match (numbered)', () => {
    const lastAsst = '1. Phase A\n2. Phase B\n3. Phase C';
    expect(detectChoiceProvenance('2 делаем', lastAsst)).toEqual({
      kind: 'user_chose_from_options',
      node: 'Phase B',
      options_offered: ['Phase A', 'Phase B', 'Phase C'],
      claude_would_have_chosen: 'Phase A',
    });
  });

  it('returns full result on successful match (Cyrillic letter, keyboard layout)', () => {
    const lastAsst = 'А. Первый\nБ. Второй\nВ. Третий';
    expect(detectChoiceProvenance('в делаем', lastAsst)).toEqual({
      kind: 'user_chose_from_options',
      node: 'Третий',
      options_offered: ['Первый', 'Второй', 'Третий'],
      claude_would_have_chosen: 'Первый',
    });
  });

  it('returns full result with AskUserQuestion source', () => {
    const askUser = {
      type: 'tool_use',
      name: 'AskUserQuestion',
      input: { questions: [{ options: [{ label: 'Recommended' }, { label: 'Alternative' }] }] },
    };
    expect(detectChoiceProvenance('делай 2', askUser)).toEqual({
      kind: 'user_chose_from_options',
      node: 'Alternative',
      options_offered: ['Recommended', 'Alternative'],
      claude_would_have_chosen: 'Recommended',
    });
  });
});
```

- [ ] **Step 1.10: Run tests — verify FAIL**

```powershell
cd app
npx vitest run --config vitest.config.tools.mjs observer-choice-detector
```

Expected: 18 PASS, 5 FAIL.

- [ ] **Step 1.11: Implement `detectChoiceProvenance`**

Добавить в `tools/observer-choice-detector.mjs`:

```javascript
export function detectChoiceProvenance(promptText, lastAssistantContent) {
  const options = extractOptions(lastAssistantContent);
  if (!options) return null;
  const ref = detectReference(promptText, options);
  if (!ref) return null;
  return {
    kind: 'user_chose_from_options',
    node: ref.label,
    options_offered: options.slice(),
    claude_would_have_chosen: options[0],
  };
}
```

- [ ] **Step 1.12: Run tests — verify GREEN**

```powershell
cd app
npx vitest run --config vitest.config.tools.mjs observer-choice-detector
```

Expected: 23/23 PASS.

- [ ] **Step 1.13: Commit Task 1**

```bash
git commit -m "feat(observer): choice detector — user_chose_from_options kind" -- tools/observer-choice-detector.mjs tools/observer-choice-detector.test.mjs
```

Full message body:

```
Pure module — extracts options (numbered/lettered/bullets/AskUserQuestion)
from last assistant message, detects user reference (position-based +
substring), returns decision_provenance for the 3rd kind.

23/23 tests GREEN.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
```

---

## Task 2: Integrate into `observer-transcript-parser.mjs`

**Файлы:**

- Modify: `tools/observer-transcript-parser.mjs`
- Test: `tools/observer-transcript-parser.test.mjs`

**Контекст:** parser строит эпизод из транскрипта. `decision_provenance` собирается из routing-тега (`parseRoutingTag`). Задача — ДО проверки тега вызвать `detectChoiceProvenance` с (a) текстом текущего user-prompt и (b) последним assistant-content перед ним.

- [ ] **Step 2.1: Failing test — parser produces user_chose_from_options episode**

В `tools/observer-transcript-parser.test.mjs` добавить:

```javascript
describe('parseTranscript — user_chose_from_options', () => {
  it('classifies as user_chose_from_options when last assistant offered options and user picked one', () => {
    const lines = [
      JSON.stringify({
        type: 'user',
        timestamp: '2026-05-19T10:00:00.000Z',
        sessionId: 's1',
        message: { role: 'user', content: 'continue task' },
      }),
      JSON.stringify({
        type: 'assistant',
        timestamp: '2026-05-19T10:00:01.000Z',
        sessionId: 's1',
        message: {
          role: 'assistant',
          content: [{ type: 'text', text: 'Choose:\n1. First option\n2. Second option\n3. Third option' }],
        },
      }),
      JSON.stringify({
        type: 'user',
        timestamp: '2026-05-19T10:00:10.000Z',
        sessionId: 's1',
        message: { role: 'user', content: '2 делаем' },
      }),
      JSON.stringify({
        type: 'assistant',
        timestamp: '2026-05-19T10:00:11.000Z',
        sessionId: 's1',
        message: { role: 'assistant', content: [{ type: 'text', text: 'ok' }] },
      }),
    ].join('\n');
    const ep = parseTranscript(lines, 'fallback');
    expect(ep.decision_provenance).toEqual({
      kind: 'user_chose_from_options',
      node: 'Second option',
      options_offered: ['First option', 'Second option', 'Third option'],
      claude_would_have_chosen: 'First option',
    });
  });

  it('falls back to autonomous when last assistant had no options', () => {
    const lines = [
      JSON.stringify({
        type: 'assistant',
        timestamp: '2026-05-19T10:00:00.000Z',
        sessionId: 's1',
        message: { role: 'assistant', content: [{ type: 'text', text: 'just a paragraph' }] },
      }),
      JSON.stringify({
        type: 'user',
        timestamp: '2026-05-19T10:00:10.000Z',
        sessionId: 's1',
        message: { role: 'user', content: '2 делаем' },
      }),
      JSON.stringify({
        type: 'assistant',
        timestamp: '2026-05-19T10:00:11.000Z',
        sessionId: 's1',
        message: { role: 'assistant', content: [{ type: 'text', text: 'ok' }] },
      }),
    ].join('\n');
    const ep = parseTranscript(lines, 'fallback');
    expect(ep.decision_provenance).toEqual({ kind: 'autonomous', claude_would_have_chosen: null });
  });

  it('routing-tag user_directed_method preserved when no choice detected', () => {
    const lines = [
      JSON.stringify({
        type: 'user',
        timestamp: '2026-05-19T10:00:00.000Z',
        sessionId: 's1',
        message: { role: 'user', content: 'запусти brainstorming' },
      }),
      JSON.stringify({
        type: 'assistant',
        timestamp: '2026-05-19T10:00:01.000Z',
        sessionId: 's1',
        message: {
          role: 'assistant',
          content: [{ type: 'text', text: 'ok\n<!-- routing: provenance=user_directed_method node=brainstorming counterfactual=writing-plans -->' }],
        },
      }),
    ].join('\n');
    const ep = parseTranscript(lines, 'fallback');
    expect(ep.decision_provenance.kind).toBe('user_directed_method');
  });
});
```

- [ ] **Step 2.2: Run tests — verify FAIL**

```powershell
cd app
npx vitest run --config vitest.config.tools.mjs observer-transcript-parser
```

Expected: existing tests PASS, 3 new tests FAIL.

- [ ] **Step 2.3: Implement parser integration**

В `tools/observer-transcript-parser.mjs`:

1. Импорт:

```javascript
import { detectChoiceProvenance } from './observer-choice-detector.mjs';
```

1. Реализовать helper `extractLastAssistantContent(entries, turnStartIdx)` рядом с `extractLastUserPromptText`:

```javascript
function extractLastAssistantContent(entries, turnStartIdx) {
  for (let i = turnStartIdx - 1; i >= 0; i--) {
    const e = entries[i];
    if (e?.message?.role === 'assistant') {
      const content = e.message.content;
      if (Array.isArray(content)) return content;
      if (typeof content === 'string') return content;
    }
  }
  return null;
}
```

1. В функции сборки эпизода — заменить блок сборки `decision_provenance`:

```javascript
// БЫЛО (примерно строка 313):
const decision_provenance = tag
  ? { kind: 'user_directed_method', claude_would_have_chosen: tag.claude_would_have_chosen }
  : { kind: 'autonomous', claude_would_have_chosen: null };

// СТАЛО:
const userPromptText = extractLastUserPromptText(turnEntries) || '';
const lastAsstContent = extractLastAssistantContent(allEntries, turnStartIdx);
const choice = detectChoiceProvenance(userPromptText, lastAsstContent);
let decision_provenance;
if (choice) {
  decision_provenance = choice;
} else {
  decision_provenance = tag
    ? { kind: 'user_directed_method', claude_would_have_chosen: tag.claude_would_have_chosen }
    : { kind: 'autonomous', claude_would_have_chosen: null };
}
```

NB: имена `turnEntries` / `allEntries` / `turnStartIdx` могут отличаться — субагент адаптирует под реальную структуру `parseTranscript`. Ключевой инвариант: `lastAsstContent` — последняя assistant-message ДО текущего user-turn.

- [ ] **Step 2.4: Run tests — verify GREEN**

```powershell
cd app
npx vitest run --config vitest.config.tools.mjs observer-transcript-parser
```

Expected: все существующие + 3 новых = PASS.

- [ ] **Step 2.5: Commit Task 2**

```bash
git commit -m "feat(observer): parser integration — user_chose_from_options before routing-tag" -- tools/observer-transcript-parser.mjs tools/observer-transcript-parser.test.mjs
```

Full body:

```
detectChoiceProvenance runs BEFORE parseRoutingTag; if last assistant
turn offered options and user prompt references one, decision_provenance
becomes user_chose_from_options. Otherwise falls back to existing
routing-tag / autonomous logic.

3 new parser tests GREEN; all existing tests still GREEN.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
```

---

## Task 3: Extend `routingGateDecision` — no-block для new kind

**Файлы:**

- Modify: `tools/observer-stop-hook.mjs`
- Test: `tools/observer-stop-hook.test.mjs`

**Контекст:** `routingGateDecision` блокирует при `detectMethodDirected` + отсутствии `user_directed_method` тега. Для `user_chose_from_options` блокировать НЕ нужно — collaborative-choice не требует тега.

- [ ] **Step 3.1: Failing test**

В `tools/observer-stop-hook.test.mjs` добавить:

```javascript
describe('routingGateDecision — user_chose_from_options', () => {
  it('does NOT block when episode is user_chose_from_options even if prompt mentions a node', () => {
    const ep = v2Episode({
      decision_provenance: {
        kind: 'user_chose_from_options',
        node: 'brainstorming',
        options_offered: ['brainstorming', 'writing-plans'],
        claude_would_have_chosen: 'brainstorming',
      },
    });
    const decision = routingGateDecision(ep, 'запусти brainstorming', ['brainstorming', 'writing-plans'], false);
    expect(decision.block).toBe(false);
  });
});
```

NB: вспомогательная фабрика `v2Episode` уже есть в файле (используется в существующих тестах) — её используем без изменений.

- [ ] **Step 3.2: Run test — verify FAIL**

```powershell
cd app
npx vitest run --config vitest.config.tools.mjs observer-stop-hook
```

Expected: новый тест FAIL.

- [ ] **Step 3.3: Implement no-block branch**

В `tools/observer-stop-hook.mjs` функция `routingGateDecision` — добавить **одну** строку после `stopHookActive` check:

```javascript
export function routingGateDecision(episode, promptText, knownNodes, stopHookActive) {
  if (stopHookActive) return { block: false, reason: null };
  if (episode?.decision_provenance?.kind === 'user_chose_from_options') return { block: false, reason: null };
  // ... rest unchanged
}
```

- [ ] **Step 3.4: Run test — verify GREEN**

```powershell
cd app
npx vitest run --config vitest.config.tools.mjs observer-stop-hook
```

Expected: все существующие + 1 новый = PASS.

- [ ] **Step 3.5: Commit Task 3**

```bash
git commit -m "feat(observer): routing-gate no-block for user_chose_from_options" -- tools/observer-stop-hook.mjs tools/observer-stop-hook.test.mjs
```

---

## Task 4: Extend `brain-retro-analyzer.mjs` factor matrix

**Файлы:**

- Modify: `tools/brain-retro-analyzer.mjs`
- Test: `tools/brain-retro-analyzer.test.mjs`

**Контекст:** `buildFactorMatrix` строит распределение `outcome` по факторам. Ось `decision_provenance.kind` должна поддержать 3-е значение.

- [ ] **Step 4.1: Failing test**

В `tools/brain-retro-analyzer.test.mjs` добавить:

```javascript
describe('buildFactorMatrix — provenance kind 3 values', () => {
  it('counts all 3 kinds in provenance axis', () => {
    const episodes = [
      { decision_provenance: { kind: 'autonomous' }, outcome: 'success' },
      { decision_provenance: { kind: 'user_directed_method' }, outcome: 'rework' },
      { decision_provenance: { kind: 'user_chose_from_options' }, outcome: 'success' },
      { decision_provenance: { kind: 'user_chose_from_options' }, outcome: 'rework' },
    ];
    const matrix = buildFactorMatrix(episodes);
    const provenanceRow = matrix.find((row) => row.factor === 'decision_provenance.kind');
    expect(provenanceRow).toBeTruthy();
    expect(provenanceRow.buckets).toHaveProperty('autonomous');
    expect(provenanceRow.buckets).toHaveProperty('user_directed_method');
    expect(provenanceRow.buckets).toHaveProperty('user_chose_from_options');
    expect(provenanceRow.buckets.user_chose_from_options.success).toBe(1);
    expect(provenanceRow.buckets.user_chose_from_options.rework).toBe(1);
  });
});
```

NB: имена полей в matrix-структуре (`factor` / `buckets`) — субагент проверяет в реальном коде; если schema другая (например `{ axis, distribution }`), test адаптирует под существующую форму.

- [ ] **Step 4.2: Run test — verify FAIL or already-PASS**

```powershell
cd app
npx vitest run --config vitest.config.tools.mjs brain-retro-analyzer
```

Если test PASS без изменений (dynamic bucketing) — Step 4.3 skip, переход к 4.5 (без commit). Если FAIL (whitelisted enum) — fix per 4.3.

- [ ] **Step 4.3: If FAIL — extend whitelist**

Найти в `tools/brain-retro-analyzer.mjs` определение `PROVENANCE_KINDS` (или эквивалент) — расширить:

```javascript
const PROVENANCE_KINDS = ['autonomous', 'user_directed_method', 'user_chose_from_options'];
```

- [ ] **Step 4.4: Run test — verify GREEN**

```powershell
cd app
npx vitest run --config vitest.config.tools.mjs brain-retro-analyzer
```

Expected: все PASS.

- [ ] **Step 4.5: Commit Task 4 (если был fix)**

Если 4.3 был no-op — skip commit. Иначе:

```bash
git commit -m "feat(brain-retro): factor matrix — 3rd provenance kind in axis" -- tools/brain-retro-analyzer.mjs tools/brain-retro-analyzer.test.mjs
```

---

## Task 5: Normative sync — Pravila §16.2 + spec cross-ref + CLAUDE.md

**Файлы:**

- Modify: `docs/Pravila_raboty_Claude_v1_1.md`
- Modify: `docs/superpowers/specs/2026-05-19-brain-governance-design.md`
- Modify: `CLAUDE.md` (через `/claude-md-management:claude-md-improver`)

**Pre-flight sync** (Pravila §15.2):

```bash
git fetch origin
git log HEAD..origin/main --oneline
```

Если есть un-merged коммиты на origin/main, затрагивающие 8-файловый список — pull/rebase ДО правок.

- [ ] **Step 5.1: Update Pravila §16.2**

В `docs/Pravila_raboty_Claude_v1_1.md`:

1. Header `v1.32` → `v1.33`.
2. §10 история версий — добавить entry v1.33:

```
- **v1.33 (2026-05-19):** §16.2 — расширен `decision_provenance.kind` до 3 значений
  (`autonomous` | `user_directed_method` | `user_chose_from_options`). Третий kind —
  collaborative-choice case (пользователь выбирает один из вариантов, предложенных Claude
  в предыдущем ходе). Routing-gate его НЕ блокирует. Spec §11:
  `docs/superpowers/specs/2026-05-19-observer-factor-analysis-design.md` v1.1.
```

1. §16.2 — в абзаце «Схема эпизода v2» заменить «`decision_provenance.kind` ∈ `autonomous` | `user_directed_method`» на «`decision_provenance.kind` ∈ `autonomous` | `user_directed_method` | `user_chose_from_options`» + добавить одно предложение про 3-й kind.

- [ ] **Step 5.2: Update brain-governance-design.md cross-ref**

В `docs/superpowers/specs/2026-05-19-brain-governance-design.md` — убедиться, что cross-ref на factor-analysis spec упоминает «§11 phase 1.1 (user_chose_from_options)». Если cross-ref общий — пропустить.

- [ ] **Step 5.3: Update CLAUDE.md via plugin**

Через `/claude-md-management:claude-md-improver`:

- §0 Pravila row: `v1.32` → `v1.33` + prepend короткая note.
- §3.6 — в абзаце «Observer schema v2» добавить предложение про 3-й kind.
- §9 история — `v2.20` entry.

- [ ] **Step 5.4: Run cross-ref-checker — verify 0 drift**

```bash
node tools/cross-ref-checker.mjs
```

Expected: 0 drift.

- [ ] **Step 5.5: Commit Task 5 — atomic normative sync**

```bash
git commit -m "docs(brain): phase 1.1 normative sync — user_chose_from_options 3rd kind" -- docs/Pravila_raboty_Claude_v1_1.md docs/superpowers/specs/2026-05-19-brain-governance-design.md CLAUDE.md
```

Full body:

```
Pravila v1.32 → v1.33 (§16.2 — kind enum расширен до 3 значений,
+§10 entry). brain-governance-design.md cross-ref на factor-analysis
spec §11. CLAUDE.md §0 Pravila row + §3.6 + §9.

cross-ref-checker: 0 drift.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
```

---

## Verification — финальный прогон перед push

- [ ] Full tools Vitest suite:

```powershell
cd app
npx vitest run --config vitest.config.tools.mjs
```

Expected: all GREEN.

- [ ] Cross-ref checker:

```bash
node tools/cross-ref-checker.mjs
```

Expected: 0 drift.

- [ ] Smoke (опционально) — записать тестовый эпизод с `user_chose_from_options` через реальный транскрипт.

- [ ] Pre-push: gitleaks-full-history + lychee (через lefthook pre-push) — отрабатывают автоматически.

---

## Out of scope (документировать как backlog)

- Refine routing-gate detector — исключение `<system-reminder>` blocks из scope. Отдельная задача (memory `project_brain_governance_design.md`).
- Inline-проза «вариант A — короткий» без структурированного списка — низкая надёжность эвристики.
- Распознавание «пользователь дополнил мою опцию» (выбрал B, но изменил параметр) — требует семантического понимания.

---

## Execution

После approval плана — `superpowers:subagent-driven-development` (model: Sonnet per Pravila §15.1, fresh subagent per task + two-stage review). 5 tasks, ~5 commits (Task 4 может быть no-op).
