5 tasks TDD plan with explicit code per step. Task 1 creates observer-choice-detector.mjs pure module (23 tests). Task 2 wires into transcript-parser. Task 3 extends routingGateDecision (no-block). Task 4 extends brain-retro factor matrix. Task 5 normative sync (Pravila §16.2 + CLAUDE.md §3.6 + spec cross-ref). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
32 KiB
Observer factor-analysis — phase 1.1 (user_chose_from_options) implementation plan
For agentic workers: REQUIRED SUB-SKILL: Use
superpowers:subagent-driven-development(recommended) orsuperpowers:executing-plansto 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:
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
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:
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
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:
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
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:
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
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:
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
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:
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
cd app
npx vitest run --config vitest.config.tools.mjs observer-choice-detector
Expected: 23/23 PASS.
- Step 1.13: Commit Task 1
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 добавить:
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
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:
- Импорт:
import { detectChoiceProvenance } from './observer-choice-detector.mjs';
- Реализовать helper
extractLastAssistantContent(entries, turnStartIdx)рядом сextractLastUserPromptText:
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;
}
- В функции сборки эпизода — заменить блок сборки
decision_provenance:
// БЫЛО (примерно строка 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
cd app
npx vitest run --config vitest.config.tools.mjs observer-transcript-parser
Expected: все существующие + 3 новых = PASS.
- Step 2.5: Commit Task 2
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 добавить:
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
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:
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
cd app
npx vitest run --config vitest.config.tools.mjs observer-stop-hook
Expected: все существующие + 1 новый = PASS.
- Step 3.5: Commit Task 3
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 добавить:
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
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 (или эквивалент) — расширить:
const PROVENANCE_KINDS = ['autonomous', 'user_directed_method', 'user_chose_from_options'];
- Step 4.4: Run test — verify GREEN
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. Иначе:
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):
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:
- Header
v1.32→v1.33. - §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.
- §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.20entry. -
Step 5.4: Run cross-ref-checker — verify 0 drift
node tools/cross-ref-checker.mjs
Expected: 0 drift.
- Step 5.5: Commit Task 5 — atomic normative sync
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:
cd app
npx vitest run --config vitest.config.tools.mjs
Expected: all GREEN.
- Cross-ref checker:
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. Отдельная задача (memoryproject_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).