# 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.kind` ∈ `autonomous` | `user_directed_method`. `autonomous` — дефолт (тега нет). `user_directed_method` — тег есть. `claude_would_have_chosen` — узел-контрфактуал, не-null только при `user_directed_method`. (Случай «заказчик задал задачу» — это норма, конфлируется с `autonomous`: для факторного анализа важно только «навязан ли *метод*».)
- `environment.economy_level` ∈ `0` | `5` | `100` | `null` — из маркера `=== ECONOMY MODE: N% ===` в транскрипте.
- `environment.model` — из метаданных assistant-сообщений; `null` если недоступно.
- `environment.post_compaction` — `true` если перед текущим ходом в транскрипте был маркер компакции.
- `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.
- `outcome` ∈ `success` | `partial` | `failure` | `rework` | `blocked` | `unknown`. При записи — `unknown` (честный дефолт вместо фиктивного `success`). Уточняется `/brain-retro`.
- `events[].kind` ∈ `skill_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` принимает форму:

```json
{
  "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=X` → `claude_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_chosen` → `counterfactual`.
- 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_kind`
    ∈ `option|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_turn` → `session_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`.
