Files
brain/docs/superpowers/specs/2026-05-19-observer-factor-analysis-design.md
T

35 KiB
Raw Blame History

Observer factor-analysis extension — design

Версия: 1.2 Дата: 2026-05-20 Статус: accepted — фаза 1 реализована (f7f37fb); фаза 1.1 (см. §11) реализована (commits 7f379bd..dc6d2dd); фаза 1.2 «instrument expansion» (см. §12) — в реализации Связано: ADR-011 (brain governance), spec 2026-05-19-brain-governance-design.md, Pravila §16, PSR_v1 R16, docs/observer/, tools/observer-stop-hook.mjs, tools/observer-transcript-parser.mjs


1. Контекст

Регламент «мозга» (ADR-011) включает наблюдателя — Stop-хук пишет эпизоды в docs/observer/episodes-YYYY-MM.jsonl, /brain-retro раз в спринт строит факторный анализ. После follow-up'а 19.05.2026 (99c7bac) наблюдатель наполняется реальными данными из транскрипта.

Но при разборе с заказчиком вскрылось: данных для полноценного факторного анализа не хватает. Факторный анализ объясняет разброс исходов через факторы. Текущий эпизод:

  • Исход фиктивенoutcome хардкод "success". Объяснять нечего, если зависимая переменная константа. Настоящий исход часто известен только в следующем ходе (заказчик говорит «не то»).
  • Нет провенанса решения — эпизод пишет node_chosen, но не «кто решил». Заказчик ставит задачи и навязывает методы; Claude исполняет. Если заказчик навязал неверный узел → эпизод покажет node_chosen: X, outcome: error, и анализ обвинит роутинг, хотя причина — навязанная инструкция.
  • Нет факторов среды — уровень экономии, модель, пост-компакция, параллельные сессии, длина сессии радикально влияют на исход, но не записываются → анализ искажается смешиванием факторов.
  • Эпизод ≠ задача — одна задача = N ходов = N эпизодов; rework невидим как rework.
  • Нет нормировки размера — «узел X коррелирует с ошибками» может значить «узел X применяют на больших задачах».
  • Процессные события не наполняютсяhook_fired / chain_divergence / confusion_marker / time_burn заявлены в spec v1.1, не генерируются; прерывания и ретраи не фиксируются.
  • Наблюдатель может тихо пропускать — Stop-хук при ошибке делает молчаливый exit 0; пропуск невидим.

Цель: расширить эпизод и наблюдателя так, чтобы факторный анализ стал возможен; сделать дисциплину ведения наблюдателя принудительной (которую Claude не может обойти) и самоконтролируемой (пропуски наблюдателя видимы).

Граница (вне scope): независимый LLM-agent-судья («какой узел был правильным»), confusion_marker как настоящее суждение, real-time in-session флаги friction. Это отдельная фаза 2 — требует agent + $/сессию + дополнительной нормативки.


2. Архитектура — 4 слоя

Слой 1  Схема эпизода v2          — что хранится
Слой 2  Захват (parser + 1 тег)   — детерминированный парсинг + routing-тег
Слой 3  Принуждение (двустороннее)— 3a routing-gate, 3b самодисциплина наблюдателя
Слой 4  Анализ (/brain-retro)     — исход, группировка, цепочки, факторная матрица

3. Слой 1 — схема эпизода v2

Эпизод приобретает schema_version: 2. Новые/изменённые поля поверх v1.1 (5 mandatory + 7-полевой primary_rationale + events):

Поле Тип Источник
schema_version 2 константа
decision_provenance { kind, claude_would_have_chosen } routing-тег (Слой 2)
environment { economy_level, model, post_compaction, session_turn, parallel_session } детерминированный парсинг
task_size { tool_calls, files_touched } детерминированный парсинг
task_ref string sessionId + сегмент задачи
outcome enum провизорно при записи, уточняется Слоем 4
events[] расширенные виды детерминированный парсинг + тег
  • decision_provenance.kindautonomous | user_directed_method. autonomous — дефолт (тега нет). user_directed_method — тег есть. claude_would_have_chosen — узел-контрфактуал, не-null только при user_directed_method. (Случай «заказчик задал задачу» — это норма, конфлируется с autonomous: для факторного анализа важно только «навязан ли метод».)
  • environment.economy_level0 | 5 | 100 | null — из маркера === ECONOMY MODE: N% === в транскрипте.
  • environment.model — из метаданных assistant-сообщений; null если недоступно.
  • environment.post_compactiontrue если перед текущим ходом в транскрипте был маркер компакции.
  • environment.session_turn — порядковый номер реального user-prompt в сессии.
  • environment.parallel_session — best-effort: true при маркерах коллизии (foreign git index, упоминание параллельной сессии); иначе false. Честно: детерминированно ненадёжно.
  • task_size.tool_calls — число tool_use в ходе; files_touched — число уникальных путей в Read/Edit/Write.
  • outcomesuccess | partial | failure | rework | blocked | unknown. При записи — unknown (честный дефолт вместо фиктивного success). Уточняется /brain-retro.
  • events[].kindskill_invoked | tool_summary | error | hook_fired | interrupt | retry | time_burn | parse_gap. (confusion_marker, chain_divergence — зарезервированы под фазу 2 / agent.)

Маркер-эпизод observer_error — особая минимальная строка при внутреннем отказе хука: { schema_version: 2, observer_error: true, error_message, timestamps, task_id }. Пишется вместо тихого пропуска.

7-полевой primary_rationale из v1.1 сохраняется без изменений (triggers_matched / candidates_considered / boundaries_applied остаются [] — детерминированно невосстановимы).


4. Слой 2 — захват

4.1. Расширение tools/observer-transcript-parser.mjs

Все факторы среды, размер, процессные события — детерминированно из транскрипта:

  • economy_level — regex === ECONOMY MODE: (\d+)% === по транскрипту хода.
  • model — из message.model assistant-сообщений (если поле есть); иначе null.
  • post_compaction — поиск compaction-маркера (isCompactSummary / запись типа компакции) до начала хода.
  • session_turn — счётчик isRealUserPrompt до текущего.
  • task_size — подсчёт tool_use + уникальных файловых путей.
  • events: hook_fired — из записей attachment.type === "hook_success"/hook_error; interrupt — из маркера [Request interrupted by user]; retry — эвристика «тот же инструмент по той же цели после error»; time_burn — производное от длительности хода > порога; parse_gap — если доля непарсибельных строк выше порога.

4.2. Routing-тег (подход C)

Когда заказчик навязал метод, Claude печатает в ответе одну строку:

<!-- routing: provenance=user_directed_method node=<выбранный> counterfactual=<что выбрал бы сам> -->

Парсер извлекает тег regex'ом, заполняет decision_provenance. Тег эмитится только при method-direction; на автономных ходах тега нет, провенанс по умолчанию autonomous. Тег — HTML-комментарий: виден заказчику (прозрачность — это сама по себе обратная связь), не шумит в рендере.


5. Слой 3 — принуждение (двустороннее)

5.1. 3a — routing-gate (сторона Claude)

Логика в tools/observer-stop-hook.mjs:

  1. Всегда сначала — записать эпизод (как Слой 1/2), exit-0-safe.
  2. Затем — детектор «навязан ли метод» (детерминированный, консервативный): regex по последнему user-prompt — содержит ли имя узла/скила из реестра в директивном контексте (запусти X, используй X, через X, /команда, имя скила). Список известных узлов — из Tooling Прил. Н + список скилов.
  3. Если метод навязан и в ответе нет <!-- routing: --> тега → хук выводит { "decision": "block", "reason": "<инструкция: добавь routing-тег>" }.
  4. decision: block заставляет Claude продолжить ход; Claude добавляет тег; следующий Stop — хук перепроверяет, проходит.

Необойдёмость: Stop-хук исполняется Claude Code детерминированно в конце каждого хода. Claude не может его пропустить, рационализировать или обойти через Bash. Завершить ход без тега (когда метод навязан и детектор сработал) — нельзя.

Честный предел: не «обход Claude», а покрытие детектора. False negative (метод навязан тонко, regex не поймал) → тега нет → провенанс ложно autonomous. False positive (имя узла упомянуто вскользь) → лишний блок. Детектор делается консервативно-широким — false negative минимизируется ценой редких false positive.

5.2. 3b — самодисциплина наблюдателя

  • Видимость отказа. observer-stop-hook.mjs оборачивается так: при любой внутренней ошибке — записать маркер observer_error и exit 0. Принцип «не блокировать Stop» сохранён, пропуск становится видимым в данных.
  • Видимость потери данных. Парсер при доле непарсибельных строк выше порога пишет событие parse_gap.
  • Контроль покрытия. Расширение C3 observer-of-observer (или новый контролёр): сверяет «есть git-активность за период, но 0 эпизодов» → флаг в STATUS.md. Тихая смерть наблюдателя становится заметной.
  • Целостность регистрации. Проверка (стиль C1 L1-watcher): Stop-хук прописан в .claude/settings.json; .git/hooks/post-commit установлен. Удалили/сломали → флаг.

5.3. HK1 — анализ коллизии хуков (ADR-010)

Stop-событие сейчас несёт: user-level economy-verifier (agent) + project-level observer-stop-hook (command). routing-gate встраивается внутрь существующего observer-stop-hook — нового Stop-entry не добавляется (остаётся 2). Оба способны вернуть decision: block — конфликта нет: Claude Code прогоняет все Stop-хуки, любой block ведёт к продолжению хода. observer-gate детерминированный и дешёвый. UserPromptSubmit-напоминание (осознанность) — отдельный лёгкий компонент, не Stop. HK1: коллизии нет.


6. Слой 4 — анализ (/brain-retro)

/brain-retro остаётся read-only агрегатором (не мутирует JSONL). Расширения:

  • Вывод настоящего исхода. Для каждого эпизода: interrupt-событие → partial; больше error-событий, чем retry (невосстановленный сбой) → blocked; иначе — по первому user-prompt следующего эпизода той же сессии: коррекция (не то / не так / переделай / откати / сломал / не работает / revert / … — расширенный набор) → исход прошлого = rework; одобрение/новая задача (ок / спасибо / дальше / готово) → success; нет следующего → unknown. failure детерминированно невосстановим (суждение «работа неверна И не исправлена») — отложен в фазу 2 (agent-судья). Уточнённый исход — в retro-ноте (JSONL не трогается, append-only).
  • Группировка «эпизоды → задача». task_ref по sessionId; сегментация — новая задача начинается с top-level user-prompt после success или после паузы.
  • Каузальные цепочки. Детерминированная корреляция: эпизоды, делящие files_touched; error в N → исправление того же файла в N+1. Surface как «кандидаты цепочек».
  • Факторная матрица. Строки — факторы (decision_provenance.kind, economy_level, model, post_compaction, бакет session_turn, parallel_session, бакет task_size, node_chosen, task_classification — 9 осей); столбцы — распределение outcome. Пример вывода: «user_directed_method: 40% rework против autonomous 12%» — прямой ответ на вопрос заказчика «моя ли вина».

7. Нормативные изменения

  • ADR-011 — поправка: добавить решение «observer factor-analysis extension v2» (схема v2, двустороннее принуждение).
  • Pravila §16 — §16.2 расширить: схема эпизода v2; новое правило routing-тег-дисциплины (заказчик навязал метод → Claude обязан эмитить routing-тег; Stop-gate enforced); правило самодисциплины наблюдателя (каждый Stop фиксируется; внутренний отказ → observer_error, не замалчивается; покрытие и регистрация проверяются контролёром).
  • PSR_v1 R16 — синхронизировать (brain evidence loop — расширенные события).
  • spec brain-governance — cross-ref на этот документ.

Все правки нормативки — через pre-flight sync (Pravila §15.2, 8-файловый список); CLAUDE.md — через claude-md-management.


8. Тест-план

  • Unit (Vitest, tools/*.test.mjs): расширения парсера — извлечение economy_level / model / post_compaction / session_turn / task_size; события hook_fired / interrupt / retry / time_burn / parse_gap; парсинг routing-тега; путь observer_error.
  • Unit: детектор routing-gate (regex по известным узлам), логика decision: block.
  • Unit: /brain-retro — вывод исхода из следующего эпизода, группировка, факторная матрица.
  • Integration: CLI smoke — хук против реального транскрипта → v2-эпизод наполнен; симуляция method-directed хода без тега → хук выводит decision: block.
  • Полный tools Vitest GREEN; lefthook pre-commit зелёный.

9. Открытые риски

  • R1 — покрытие детектора routing-gate. Regex по именам узлов: false negative (тонкая директива) / false positive (упоминание вскользь). Митигируется консервативной широтой; остаточный риск принят.
  • R2 — model / parallel_session детерминированно ненадёжны. Записываются best-effort, null/false при недоступности. Не блокирует факторный анализ по остальным факторам.
  • R3 — вывод исхода эвристичен. Next-prompt sentiment — приближение; систематический сдвиг (заказчик не всегда реагирует словом коррекции). Принято для фазы 1; точный исход — фаза 2 (agent).
  • R4 — два Stop-блокатора. economy-agent + observer-gate. Сосуществуют (HK1 §5.3), но при одновременном блоке Claude получает два reason'а — нормально, оба отрабатываются.
  • R5 — transcript_path в Stop-payload. Унаследовано из Item 1 (99c7bac): если Claude Code не передаёт путь — хук деградирует, эпизод беднеет. Проверяется post-deploy.
  • R6 — reconstructive bias контрфактуала. claude_would_have_chosen пишет тот же Claude, что исполнял задачу. Сдвиг признан заказчиком осознанно; независимая проверка — фаза 2 (agent).

10. Что НЕ делаем (фаза 2, отдельный spec)

Независимый agent-судья «какой узел был правильным»; confusion_marker / chain_divergence как настоящие суждения; real-time in-session флаги friction; автоматические правки нормативки по результатам факторного анализа (остаётся: /brain-retro только предлагает кандидатов).


11. Phase 1.1 amendment — user_chose_from_options (2026-05-19)

11.1. Контекст

Live-триггер 19.05.2026 при выполнении фазы 1: при предложении пользователю A/B/C-выбора (numbered/lettered options в моём ответе) — пользователь короткими промптами выбирает один (1 экономия 0%, в делаем, делай 2). Routing-gate классифицирует такой ход как user_directed_method, но это не навязанный извне метод — выбор сделан из choice-space, который я сам сформулировал. Контрфактуал тоже отличается: в user_directed_method контрфактуал — «что бы я выбрал автономно без директивы», а здесь — «какую из моих опций я бы рекомендовал». Для факторного анализа эти случаи смешиваются → искажение матрицы.

Третий kind decision_provenance устраняет смешение.

11.2. Схема (расширение §3)

decision_provenance.kind расширяется до 3 значений: autonomous | user_directed_method | user_chose_from_options.

Для user_chose_from_options decision_provenance принимает форму:

{
  "kind": "user_chose_from_options",
  "node": "<выбранный пользователем>",
  "options_offered": ["<node-1>", "<node-2>", "<node-3>"],
  "claude_would_have_chosen": "<моя рекомендация — первый из options_offered по конвенции>"
}

Поле claude_would_have_chosen — то же поле, что и в user_directed_method (semantic: «что выбрал бы Claude автономно / самостоятельно»). Для user_chose_from_options его значение — «моя рекомендация из предложенных опций». Tag-literal counterfactual= в routing-теге сохраняется (mapping counterfactual=Xclaude_would_have_chosen: X).

Поле node — для user_directed_method не записывалось (только claude_would_have_chosen контрфактуал); для user_chose_from_options фиксирует фактический выбор пользователя. options_offered — массив, появляется только при kind: user_chose_from_options.

11.3. Детектор (новый tools/observer-choice-detector.mjs)

Pure-функция detectChoiceProvenance(promptText, lastAssistantContent){ kind, options, chosen, counterfactual } | null.

Шаг 1 — извлечь опции из последнего assistant-сообщения (broad detection per user-approved scope):

  • AskUserQuestion tool_use → опции из questions[].options[].label.
  • Numbered list: ^\d+[.\)]\s+... (минимум 2 подряд).
  • Lettered list: ^[A-Za-zА-Яа-я][.\)]\s+... (минимум 2 подряд, латиница ИЛИ кириллица).
  • Bullet list: ^[-*]\s+... (минимум 2 подряд).

Если найдено <2 опций — return null, fallback на текущую логику.

Шаг 2 — найти ссылку в user-prompt:

  • Position-based: prompt начинается с числа/буквы (опционально после verb-префикса: делай|выбираю|беру|хочу|вариант|option), сопровождаемой пробелом/запятой/точкой/EOL. Поддержка кириллицы + латиницы (keyboard-layout collision: BВ).
  • Substring match: prompt содержит первые 2-4 слова из label одной из опций.

Если ни один сигнал не найден — return null.

Шаг 3 — собрать результат:

  • chosen — label/node соответствующей выбранной опции.
  • claude_would_have_chosen — первая опция из списка (моя рекомендация по конвенции).
  • options_offered — все извлечённые опции (как массив строк).

11.4. Routing-gate (расширение §5.1)

Существующая логика (routingGateDecision в tools/observer-stop-hook.mjs) расширяется:

  1. СначалаdetectChoiceProvenance (новый). Если вернул не-null → decision_provenance.kind = user_chose_from_options, gate НЕ блокирует (collaborative-choice; tag желателен, но не обязательный — данные восстановимы детерминированно из транскрипта).
  2. Иначе — текущий detectMethodDirected (как сейчас). Если directed без тега → decision: block.
  3. Routing-тег <!-- routing: provenance=user_chose_from_options node=X counterfactual=Y --> Claude может эмитить добровольно (для прозрачности); парсер его примет, но детектор работает и без него.

11.5. Парсер (расширение §4)

tools/observer-transcript-parser.mjs — добавляется вызов detectChoiceProvenance ДО detectMethodDirected. Извлечение lastAssistantContent — последняя assistant-message строка перед текущим user-turn в JSONL транскрипта.

Если detectChoiceProvenance срабатывает — decision_provenance собирается из его результата; routing-тег (если есть) проверяется на consistency (warn, не fail). Если нет — старая логика (тег или autonomous).

11.6. /brain-retro (расширение §6)

Факторная матрица: ось decision_provenance.kind теперь имеет 3 значения. Counterfactual-анализ: для user_chose_from_options отдельная строка «пользователь выбрал не первую (мою рекомендованную) опцию — N% случаев, rework-доля Y%».

11.7. Тест-план

  • Unit (tools/observer-choice-detector.test.mjs): извлечение опций из AskUserQuestion / numbered / lettered / bullets; position-based references (число/буква, кириллица/латиница, verb-префиксы); substring label match; negative cases (нет опций / нет ссылки → null).
  • Unit (расширение observer-stop-hook.test.mjs): routingGateDecision с user_chose_from_options — НЕ блокирует.
  • Unit (расширение observer-transcript-parser.test.mjs): сборка эпизода с kind: user_chose_from_options, миграция чтения claude_would_have_chosencounterfactual.
  • Integration: CLI smoke — реальный транскрипт с AskUserQuestion-ходом → эпизод корректно классифицирован.

11.8. Out of scope (phase 1.1)

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

12. Phase 1.2 amendment — instrument expansion (2026-05-20)

Реализованы 18 рекомендаций из brain-retro 2026-05-20 (план docs/superpowers/plans/2026-05-20-observer-instrument-expansion.md v1.1 REVISION). Worktree: .claude/worktrees/observer-v2-expansion, ветка feat/observer-v2-expansion.

12.1. Схема v2 (расширения)

  • Опциональное поле task_cost в episode (#2): захват message.usage agregat по Σ всех assistant-сообщений turn'а. Поля: input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens, web_search_requests, web_fetch_requests, iterations (B1-verified shape, бонус server_tool_use + iterations поверх 4 базовых). Backward-compat: V2_FIELDS validator не расширен — старые v2-эпизоды без task_cost остаются валидны.
  • outcome enum дополнен soft_success (#16): next-prompt neutral интерпретируется как silent success ('no objection'). Слабее explicit approval, отдельно labelled.
  • Новые events[] kinds:
    • ask_user_question (#4) — per AskUserQuestion question с answer_kindoption|custom|no_answer. Сигнал quality предлагаемых options.
    • subagent_invoked (#12) — per Agent tool_use с subagent_type / model / description (80 chars).
    • error enriched (#7) — теперь несёт tool (имя инструмента, атрибуция через id-map) + summary (first 80 chars причины); раньше bare message: 'tool_result reported is_error'.

12.2. Парсер — heuristic capture (Слой 2)

  • primary_rationale arrays больше не пусты (#6): три pure-функции extractTriggers / extractCandidates / extractBoundaries сканируют assistant.text на маркеры (Pravila §N, ADR-N, PSR_v1 RX, routing-off-phase LN, hard-floor/rule + numbered/bulleted lists ≥2). Conservative-broad — false positives accepted; agent-judge остаётся out-of-scope (фаза 2 §10).
  • Opt-in reasoning-tag (#11): <!-- reasoning: triggers="..." candidates="..." boundaries="..." --> HTML-комментарий в assistant.text; semicolon-separated значения merged в heuristic arrays через Set-dedupe.
  • <system-reminder> strip в promptText (#8): UserPromptSubmit hook injections больше не загрязняют classifyTask / classifyPromptSignal / routing detection.
  • classifyTask vocabulary +7 classes (#1): analysis / memory-sync / regulatory-bump / release / cleanup / monitoring / planning. Closes «59% other» observation.
  • classifyPromptSignal vocabulary + (#9): correction +'не совсем', 'другое', 'wrong direction'; approval +'класс', 'well done', 'nice'; new_task prefix +'теперь', 'далее', 'next', 'now'. Bug fix: JS \b не работает с Cyrillic (Cyrillic ≠ word char) — substring match для русских correction markers, lookahead для prefix-based new_task.
  • parallel_session +OR pre-flight git fetch (#13 PIVOT): additive signal — Bash command git fetch && git log HEAD..origin/... (Pravila §15.2 pre-flight) = strong signal для parallel sessions. Не overwrite F1 narrow-to-tool_result детектор; OR-clause.

12.3. Анализатор (Слой 4)

  • session_segment_turn axis rename (#14): factor matrix ось session_turnsession_segment_turn (turns-since-last-compaction — что фактически и было per parser). Семантика не меняется, только имя для ясности.
  • inferOutcome neutral → soft_success (#16) — см. §12.1.

12.4. STATUS.md generator

  • Real PII counter (#3 SIMPLIFIED): sanitizeWithCount в pii-filter
    • persistent docs/observer/.pii-counters.json (per-month aggregation, bumped on each Stop-hook write) + countPiiMatches() reads counter. STATUS перестаёт врать 0 PII matches. PII patterns themselves NOT changed (F7 of parallel session already extended).
  • Last /brain-retro: N day(s) ago (#10): метрика читается из docs/observer/.read-counter.json last_read_at.
  • Legacy v1 episodes (not in factor analysis) (#18): count of pre-2026-05-19T08:06 episodes без schema_version=2 — visible.

12.5. /brain-retro skill

  • Step 4 explicit recordRead (#15): replaced abstract 'bump' instruction with node tools/observer-of-observer.mjs record. Atomic read-modify-write через fs.
  • Step 8a STATUS auto-refresh (#19): после save retro-note запускается status-md-generator — STATUS.md становится immediately current (Last /brain-retro: 0 day(s) ago, fresh episode count).

12.6. Ad-hoc tooling

  • tools/glob-latency-investigator.mjs (#17): one-off script для расследования Glob p50=12.7s аномалии из исходного ретро. Smoke-test на session 553717ec: top-5 slowest все docs/adr/** @ 20265ms — Glob recursive по ADR-каталогу = apparent culprit. Не production code path.

12.7. Infrastructure

  • npm run test:tools (B3-1, applied вне scope плана): canonical entry point для tools/observer-*.test.mjs runner. Корневой package.json script.

12.8. Not done

  • Phase 2 (out-of-scope, см. §10 + spec brain-governance §10): agent-judge для true vs nominal use, confusion_marker / chain_divergence как real judgments, real-time in-session friction flags, automatic Pravila/Tooling edits from factor matrix.
  • Task 5 (hot-file two-tier) — SKIPPED per REVISION v1.1: F4 of parallel session already added full exclude of memory/*.md; warm-tier для adjacent имел diminishing return.

12.9. Регрессия

После всех 18 task'ов: NNN/NNN GREEN в npm run test:tools (baseline 232 → final NNN — заполнить в финальном commit Task 21).

Amendments

2026-05-23 — schema v3 (parser skill/hook expand)

Spec extension: forward-only bump schema_version 2 → 3. Two new fields:

  • events[].hook_fired.scripts: { script_name: count, ... } — reverse-lookup .claude/settings.json → имена хук-скриптов. Old counts (matcher level) preserved для backward-compat.
  • primary_rationale.recommended_node: "#NN" | null — для direct-эпизодов derived из classification-map + dormancy. null при использованном skill / отсутствии рекомендации / всех dormant.

Analyzer фильтр schema_version === 2>= 2; missed-activations фильтр !== 2< 2. FACTOR_FNS +recommended_node_for_direct.

Полный spec: docs/superpowers/specs/2026-05-23-observer-parser-skill-hook-expand-design.md.