diff --git a/docs/observer/STATUS.md b/docs/observer/STATUS.md index 845aab6b..46c5c65e 100644 --- a/docs/observer/STATUS.md +++ b/docs/observer/STATUS.md @@ -1,6 +1,6 @@ # Brain Status (auto-generated) -Last updated: 2026-05-19T07:55:48.974Z +Last updated: 2026-05-19T08:47:41.763Z | Контролёр | Состояние | Детали | |---|---|---| @@ -8,11 +8,11 @@ Last updated: 2026-05-19T07:55:48.974Z | C2 Cross-ref consistency | ✅ | [cross-ref-checker] OK — 0 drift in 4 files | | C3 Observer-of-observer | ✅ | [observer-of-observer] OK — last read 0 week(s) ago | | C4 Сигнальный статус | ✅ | This file (self-reference) | -| C5 Observer-coverage | ✅ | 10 episode(s), 952 recent commit(s) · Stop-hook + post-commit OK | +| C5 Observer-coverage | ✅ | 18 episode(s), 954 recent commit(s) · Stop-hook + post-commit OK | ## Метрики (информационные, не алерты) -- Observer evidence: 10 episodes this month, 0 observer_error markers, 0 PII matches before filter +- Observer evidence: 18 episodes this month, 0 observer_error markers, 0 PII matches before filter - Использование узлов: см. `/brain-retro` (раз в спринт). **Неиспользованные узлы — не проблема** (capability-readiness; см. memory `feedback_brain_unused_tools_not_problem` — outside-repo memory store). ## Алерт-индикаторы diff --git a/docs/superpowers/plans/2026-05-19-observer-factor-analysis-phase-1-1.md b/docs/superpowers/plans/2026-05-19-observer-factor-analysis-phase-1-1.md new file mode 100644 index 00000000..d1694378 --- /dev/null +++ b/docs/superpowers/plans/2026-05-19-observer-factor-analysis-phase-1-1.md @@ -0,0 +1,869 @@ +# 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) +``` + +--- + +## 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' }], + }, + }), + ].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) +``` + +--- + +## 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) +``` + +--- + +## 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 — исключение `` 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).