# Brain Governance 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:** Build governance infrastructure for the Лидерра «brain» (~60 формализованных позиций + 20 ruflo plugins): explicit router procedure, passive observer evidence-loop, and 4 mechanical controllers — without speculative regulation that drove churn in 2026-05.

**Architecture:** Router-only (no cached chains) — реестр узлов в Tooling Прил. Н + процедура роутера в новом `docs/router-procedure.md`. Observer scope B — Stop-hook → JSONL append + optional MD notes + ПДн-фильтр + `/brain-retro` aggregator. 4 controllers — все механические (lefthook-jobs + один self-pruning counter), 0 LLM-вызовов в hot path.

**Tech Stack:**

- **Hooks**: Claude Code 2.x `Stop` event + lefthook v2.x pre/post-commit
- **Scripts**: Node 22.x (consistency со `tools/subagent-prompt-prefix.mjs`, `tools/ruflo-*-hook.mjs`); `execFileSync` only (no `exec` / `execSync` per Security Guidance #40)
- **Linters**: lefthook v2.x pre-commit gate (jobs + 4 new); markdownlint, cspell, adr-judge (already wired)
- **Storage**: plain JSONL (`docs/observer/episodes-YYYY-MM.jsonl`) + plain MD (`docs/observer/notes/*.md`, `STATUS.md`) + plain JSON (`docs/observer/.read-counter.json`)
- **Tests**: Vitest (Node-side hooks) + manual smoke runs (Stop-hook integration)

**Spec reference:** [`docs/superpowers/specs/2026-05-19-brain-governance-design.md`](../specs/2026-05-19-brain-governance-design.md) (commit `dd5bded`)

---

## File Structure

### Created

- `docs/adr/ADR-011-brain-governance.md` — Architectural decision record (anchor)
- `docs/router-procedure.md` — Single source of truth for routing procedure (§4.2 spec)
- `docs/observer/README.md` — Observer infrastructure overview + lifecycle
- `docs/observer/.read-counter.json` — C3 counter seed
- `docs/observer/STATUS.md` — C4 dashboard (auto-regenerated)
- `docs/observer/episodes-2026-05.jsonl` — first month JSONL (seeded empty)
- `docs/observer/notes/.gitkeep` — keep dir under git
- `tools/observer-stop-hook.mjs` — Stop-event hook: append JSONL + PII filter
- `tools/observer-stop-hook.test.mjs` — Vitest unit-tests for hook
- `tools/l1-watcher.mjs` — C1 settings.json ↔ Tooling drift detector
- `tools/l1-watcher.test.mjs` — Vitest unit-tests
- `tools/cross-ref-checker.mjs` — C2 normative version-drift detector
- `tools/cross-ref-checker.test.mjs` — Vitest unit-tests
- `tools/observer-of-observer.mjs` — C3 self-prune counter + warn
- `tools/observer-of-observer.test.mjs` — Vitest unit-tests
- `tools/status-md-generator.mjs` — C4 STATUS.md regenerator
- `tools/status-md-generator.test.mjs` — Vitest unit-tests
- `tools/observer-pii-filter.mjs` — shared PII filter (used by Stop-hook)
- `tools/observer-pii-filter.test.mjs` — Vitest unit-tests
- `.claude/skills/brain-retro/SKILL.md` — `/brain-retro` skill descriptor
- `.claude/skills/brain-retro/references/aggregation-template.md` — aggregator output template
- `.github/workflows/brain-l1-watcher-weekly.yml` — weekly cron for C1

### Modified

- `lefthook.yml` — add 4 new jobs (`l1-watcher`, `cross-ref-checker`, `observer-of-observer`) + post-commit STATUS.md regeneration
- `.claude/settings.json` — register `tools/observer-stop-hook.mjs` on Stop-event (chained with existing hooks per HK1 pre-check)
- `docs/Tooling_v8_3.md` — Прил. Н: add 9 obligatory attributes to row template + apply to ~60 rows + bump version
- `docs/Pravila_raboty_Claude_v1_1.md` — new §16 «Brain governance» + bump version
- `docs/Plugin_stack_rules_v1.md` — R16 «Brain evidence loop» (or R15 extension) + bump version
- `CLAUDE.md` — §3.X +cross-ref to `docs/router-procedure.md` + §0 cross-refs bump + §9 changelog entry

### Out of scope (deferred or excluded per spec §2.2)

- ruflo daemon reactivation (independent decision, §14.9 Pravila dormant remains)
- Stainless-controller (#5 removed in turn 8 brainstorming)
- Decision-content controller / per-step nanny / smart preflight / red-team / runtime-cost monitor / decision-size classifier — all explicit non-goals in spec §2.2
- Сторонние alerting-каналы (Slack/email) — STATUS.md MD-файл достаточен

---

## Pre-Flight Mandatory

**Before ANY edit to normative files (8-file list per Pravila §15.2)**, run:

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

If output is non-empty — **STOP** and rebase/sync before continuing. This applies to: `docs/Pravila_raboty_Claude_v1_1.md`, `CLAUDE.md`, `docs/Tooling_v8_3.md`, `docs/Plugin_stack_rules_v1.md`, `MEMORY.md`, `docs/Открытые_вопросы_v8_3.md`, `docs/adr/*`, `db/schema.sql`.

**HK1 pre-check (Pravila ADR-010)** is **required** before adding any new hook. See Task B4.

**Worktree-эксцепшн §5 п.10**: if implementation runs in an isolated worktree, `CLAUDE.md` is edited via direct `Edit` (precedent A11/C10/discovery). Otherwise use `/claude-md-management:claude-md-improver`.

**Atomic commits**: one logical change → one commit. Sequence each Task's commit message clearly.

**Security Guidance #40 compliance**: All new scripts use `execFileSync` (NOT `execSync`, NOT `exec`); no `shell: true`; no string interpolation into subprocess args.

---

## Phase A — Normative foundation

### Task A1: ADR-011 anchor

**Files:**

- Create: `docs/adr/ADR-011-brain-governance.md`
- Read for style: `docs/adr/ADR-010-anthropic-dev-tooling.md`

- [ ] **Step 1: Run Pravila §15.2 pre-flight sync**

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

Expected: empty output (in sync). If non-empty — stop and resolve.

- [ ] **Step 2: Read ADR-010 to capture canonical structure**

Read `docs/adr/ADR-010-anthropic-dev-tooling.md`. Note: frontmatter (id/title/status/date/related), §Context, §Decision, §Consequences, §References, §Enforcement.

- [ ] **Step 3: Create ADR-011 file**

Create `docs/adr/ADR-011-brain-governance.md`:

```markdown
---
id: ADR-011
title: Brain governance — router-only + observer + 4 mechanical controllers
status: Accepted
date: 2026-05-19
related:
  - docs/superpowers/specs/2026-05-19-brain-governance-design.md
  - docs/discovery/2026-05-18-system-audit-brain.md
  - ADR-010 (HK1 hard-rule, hook collision pre-check)
---

# ADR-011: Brain governance — router-only + observer + 4 mechanical controllers

## Status

Accepted (2026-05-19).

## Context

The Лидерра «brain» (60 formal positions + 20 ruflo plugins per Tooling Прил. Н §0) accreted faster than it was regulated. SYSTEM-аудит 18.05.2026 (`docs/discovery/2026-05-18-system-audit-brain.md`) closed Rec1–Rec5; intervention session 19.05.2026 went deeper to design ongoing governance.

Three recurring problems were identified:

1. **L1-pattern**: plugin enabled in `~/.claude/settings.json` user-level without formalization in Tooling Прил. Н. Occurred 3× in 8 days (UPM/21st 10.05; Sentry/Redis 13.05; Anthropic dev-tooling 18.05).
2. **Version drift** between 8 normative files. Tooling v2.11 collision 17.05.2026 — two parallel sessions consumed the same version number.
3. **Speculative regulation ahead of usage**. Initial recommendation «prune unused» rejected by owner — capability-readiness is an explicit strategy.

## Decision

### 1. Router-only

The brain has a single routing source of truth: the existing registry in [Tooling Прил. Н](../Tooling_v8_3.md) §4.X (extended with 9 obligatory attributes per spec §4.1) + the procedure in [`docs/router-procedure.md`](../router-procedure.md).

There is **no cache of «verified chains»**. There is **no 3-layer update mechanism**. There is **no forced-choice gate**. Every task is a fresh router-derived path.

Canonical chains L1–L12 in [`docs/routing-off-phase.md`](../routing-off-phase.md) remain as general-shape recommendations, not history-based records.

### 2. Observer (scope B, full package from day 1)

A passive Stop-event hook appends one JSONL line per session to `docs/observer/episodes-YYYY-MM.jsonl` and optionally a MD note in `docs/observer/notes/`. **Observer only writes; never intervenes.** PII-filter (gitleaks-like regex) is mandatory pre-write.

**Each episode has 5 mandatory fields** including a structured `primary_rationale` (7 sub-fields per spec §5.2.1: `step` / `node_chosen` / `triggers_matched` / `candidates_considered` / `boundaries_applied` / `hard_floor` / `task_classification`). Each individual router decision is also recorded as a `routing_decision` event in `events[]` (one per node-choice for chains). This enables **factor analysis** through `/brain-retro` — answers «which factors most often resolve conflicts between nodes X and Y» rather than just «node X used N times».

A `/brain-retro` skill aggregates evidence once per sprint and proposes regulatory candidates; the owner accepts or rejects manually.

### 3. 4 mechanical controllers (first wave)

All 4 are mechanical (regex/diff/JSON math). 0 LLM calls in hot path.

- **C1 L1-watcher** — lefthook job + weekly cron. Detects plugins in `settings.json` not formalized in Tooling Прил. Н.
- **C2 Cross-ref consistency** — lefthook job, regex-style (adr-judge analog). Detects version drift between normative files.
- **C3 Observer-of-observer** — counter + lefthook warn. Self-prune through **54 weeks** without reads.
- **C4 STATUS dashboard** — `docs/observer/STATUS.md`, regenerated per-commit.

### 4. Behavioral rule «unused ≠ problem»

The capability-readiness strategy is explicit. A node never used on a real task is **not** a problem and **not** an auto-removal candidate. Used-count is informational, never an alert. This rule overrides the analytical instinct to «prune unused».

Exception: deprecated upstream packages or physically broken tools (separate category — `npm audit` / `composer outdated`).

## Consequences

### Positive

- Speculative regulation eliminated structurally — no chain catalog can drift.
- Evidence-loop active from day 1 — owner has data for monthly/quarterly review.
- 3 recurring problem classes (L1-pattern, version drift, evidence consumption) closed mechanically with 0 LLM cost.
- Capability-readiness preserved — installed-but-unused tools are not flagged.

### Negative / risks

- 4 new lefthook jobs add ~1–2s to pre-commit.
- Observer JSONL grows ~50–200KB/month; archival after 12 months is a manual task.
- C3 54-week threshold is long — if observer infra is broken silently, detection waits up to a year. Mitigator: C4 STATUS.md shows weekly read-counter.

### Neutral

- The decision is reversible at low cost: removing controllers = `lefthook.yml` revert; removing observer = unregister Stop-hook + archive `docs/observer/`.

## Enforcement

- C1 / C2 / C3 lefthook jobs fail-fast on commit when invariants break.
- C4 STATUS.md regeneration on post-commit (informational; not a gate).
- ADR-011 itself is enforced by **adr-judge** (lefthook job 9) — this section's existence is verified per-commit (regex `^## Enforcement$`).

## References

- spec: `docs/superpowers/specs/2026-05-19-brain-governance-design.md`
- plan: `docs/superpowers/plans/2026-05-19-brain-governance.md`
- ADR-010 (HK1 pre-check hard-rule)
- Pravila §12 / §14 / §15 (hard-floor for router procedure step 1)
- PSR_v1 R15 (off-phase routing extends to brain governance)
- memory: `feedback_brain_unused_tools_not_problem.md`, `project_brain_governance_design.md`
```

- [ ] **Step 4: Run adr-judge on staged ADR**

```bash
git add docs/adr/ADR-011-brain-governance.md
npx lefthook run pre-commit
```

Expected: `[adr-judge] OK — 0 violations`.

- [ ] **Step 5: Commit**

```bash
git commit -m "docs(adr): ADR-011 brain governance — router-only + observer + 4 controllers

Anchor ADR for governance design (spec dd5bded). Sets Accepted status,
captures 4 decisions: router-only, observer scope B, 4 controllers,
capability-readiness behavioral rule. Enforced via adr-judge.

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

---

### Task A2: router-procedure.md v1.0

**Files:**

- Create: `docs/router-procedure.md`

- [ ] **Step 1: Pre-flight sync** (re-run)

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

- [ ] **Step 2: Create file with 5-step procedure**

Create `docs/router-procedure.md`:

```markdown
# Router procedure v1.0

**Status:** active (introduced 2026-05-19, spec dd5bded, ADR-011)

**Owner:** Claude Code automatic at session start.

## Purpose

Single source of truth for «task → node(s)» routing. Replaces implicit routing scattered across Pravila §12/§14/§15, PSR_v1 R0–R15, Tooling §3, and routing-off-phase.md by explicitly listing the procedure executed every turn.

## Inputs

1. Active task from user prompt.
2. Node registry: [`docs/Tooling_v8_3.md`](Tooling_v8_3.md) Прил. Н §4.X (9 obligatory attributes per row — see §4.1 spec).
3. Off-phase routing table: [`docs/routing-off-phase.md`](routing-off-phase.md).
4. Hard-floor rules: [Pravila §12 / §14 / §15](Pravila_raboty_Claude_v1_1.md).
5. ADR boundaries: `docs/adr/*.md`.

## Procedure (5 steps, executed per turn)

### Step 1 — Hard-floor check (Pravila §12 / §14 / §15)

- Does the prompt contain `queen` / `королева`? → Pravila §14 (currently dormant per §14.9).
- Is the task in Pravila §12.2 map (TDD / debug / brainstorm / writing-plans / verification / discovery / migration / commit / review / UI-feature / arch-decision / refactor / parallel-sessions / worktree / writing-skills)? → invoke skill FIRST (hard-rule).
- Will any of the 8 normative files be edited? → §15.2 pre-flight sync MANDATORY.
- Subagent + git tasks? → §15.1 Sonnet/Opus only (NEVER Haiku).

If any hard-floor rule applies and is skipped — this is a violation, regardless of subsequent steps.

### Step 2 — Classification

- Phase-active (0/1/2/3) or off-phase?
- Type: TDD / debug / brainstorm / writing-plan / verification / discovery / migration / commit / review / UI-feature / arch-decision / refactor / docs / sync / other.
- Identify task triggers (keywords, file types touched, output requested).

### Step 3 — Trigger-based node selection

- Scan Tooling Прил. Н §4.X for rows whose `triggers` attribute matches the classified task.
- If ≥2 nodes match — apply ADR boundaries (the `boundaries` attribute points to the relevant ADR-NNN).
- If conflict remains — apply PSR_v1 R15.3 (specificity priority).

### Step 4 — Canonical chain check (if applicable)

- If the matched node-set corresponds to one of L1–L12 chains in `routing-off-phase.md` §4 — invoke the chain.
- If no chain matches — execute as ad-hoc combination. Observer will record `path_type: improvised`.

### Step 5 — Execution

- Invoke skill (if §12 applies) or apply selected node(s) by trigger.
- Document boundary decisions in inline comments OR in the observer log.

## What this procedure does NOT consult

- **No cache of «verified chains»** (history-based records). Such a cache was explicitly rejected in brainstorming turn 8 (2026-05-19).
- **No `last-used-on-real-task` attribute** on registry rows. Unused-status is not used to choose or skip a node (capability-readiness — see `memory/feedback_brain_unused_tools_not_problem.md`).
- **No forced-choice gate**. Nodes that don't match triggers are silently skipped.

## When this procedure is consulted

Every turn — implicitly by Claude at session start, explicitly when routing is ambiguous.

## Relationship to other documents

- Pravila §12/§14/§15 — hard-floor; this procedure step 1 enforces them.
- PSR_v1 R0–R14 — UI-stack apparatus; consulted in step 3 when task touches UI.
- PSR_v1 R15 — off-phase routing extension; consulted in step 3 for off-phase nodes.
- Tooling §3 / §4.X — node registry; the input to step 3.
- routing-off-phase.md — chains L1–L12; consulted in step 4.

## Changelog

- **v1.0 (2026-05-19)** — initial fixation. Replaces implicit-scattered routing. ADR-011.
```

- [ ] **Step 3: Stage and verify lint pass**

```bash
git add docs/router-procedure.md
npx lefthook run pre-commit
```

If cspell complains — add unknown Russian terms to `cspell-words.txt`.

- [ ] **Step 4: Commit**

```bash
git commit -m "docs(router): router-procedure.md v1.0 — explicit 5-step routing

Single SoT for task→node routing. Replaces implicit routing scattered
across Pravila/PSR_v1/Tooling/routing-off-phase.md. ADR-011.

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

---

### Task A3: Tooling Прил. Н — 9 obligatory attributes

**Files:**

- Modify: `docs/Tooling_v8_3.md` (Прил. Н §4.X — ~60 rows)

This is the largest mechanical edit in the plan. Split into a row-template-design step (once) and 6 mass-apply sub-batches.

- [ ] **Step 1: Pre-flight sync**

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

- [ ] **Step 2: Read one existing row as baseline**

Find one §4.X section to use as a baseline shape:

```bash
grep -n "^### §4\." docs/Tooling_v8_3.md | head -5
```

Read e.g. `§4.1` section verbatim. Note current free-form structure.

- [ ] **Step 3: Add §0.1 row template (one edit to Прил. Н §0)**

Edit `docs/Tooling_v8_3.md` — add subsection «§0.1 Row template (v2.17+)» right after §0 «КАНОН СЧЁТЧИКОВ» anchor:

```markdown
### §0.1 Row template (v2.17+ — per ADR-011)

Every row §4.X MUST include 9 obligatory attributes in a structured block at the top of the section:

| Attribute | Type | Required | Description |
|---|---|---|---|
| `id` | `#NN` | yes | Unique number (matches Прил. Н TOC) |
| `name` | string | yes | Canonical tool name |
| `kind` | enum | yes | `plugin` / `skill` / `mcp` / `hook` / `vendored-skill` / `composer-dep` / `npm-dep` / `agent` |
| `phase` | enum | yes | `0` / `1` / `2` / `3` / `off-phase` |
| `subcategory` | string | optional | Off-phase subcategory (absent for phase-active) |
| `triggers` | string | yes | Keywords/task-types that route to this node |
| `boundaries` | string | yes | ADR-cross-ref OR explicit «no neighbor» |
| `dormant` | boolean | yes | `false` (active) / `true` (artefacts kept, runtime disabled) |
| `last-touched` | ISO date | yes | Date of last registry update |

Block format (markdown table inside the §4.X section):

` ` ` markdown
### §4.NN <name>

**Атрибуты:**

| id | name | kind | phase | subcategory | triggers | boundaries | dormant | last-touched |
|---|---|---|---|---|---|---|---|---|
| #NN | <name> | <kind> | <phase> | <subcat or —> | «<triggers>» | <ADR-NNN or none> | false | 2026-05-19 |

<existing prose description follows>
` ` `
```

(NB: triple backticks in the template above are space-separated to avoid markdown nesting; in actual document use real triple backticks.)

- [ ] **Step 4: Apply template to all rows — 6 sub-batches**

For each sub-batch:

1. Open the file in editor and insert the attributes block at the top of each §4.X in the range, filling values from existing prose.
2. Verify `npx markdownlint-cli2 docs/Tooling_v8_3.md` after the sub-batch.
3. Stage + commit:

   ```bash
   git add docs/Tooling_v8_3.md
   git commit -m "docs(tooling): apply 9-attribute template to §4.<range> (ADR-011 A3 sub-batch N)"
   ```

Sub-batches:

- **Sub-batch 1: phase-0 (#1–#9)**
- **Sub-batch 2: phase-1 (#10–#18)**
- **Sub-batch 3: phase-2 (#19–#24, #30)**
- **Sub-batch 4: phase-3 (#25–#29)**
- **Sub-batch 5: off-phase #31–#42**
- **Sub-batch 6: off-phase #43–#60**

- [ ] **Step 5: Bump Tooling header version**

In `docs/Tooling_v8_3.md` header, bump «Прил. Н v2.16» → «v2.17» with §12 changelog entry «Прил. Н v2.17 — applied 9-attribute row template per ADR-011».

- [ ] **Step 6: Run full lefthook**

```bash
git add docs/Tooling_v8_3.md
npx lefthook run pre-commit
```

Expected: pass.

- [ ] **Step 7: Final commit for Tooling header bump**

```bash
git commit -m "docs(tooling): Прил. Н v2.16 → v2.17 — 9-attribute row template per ADR-011

Final header bump after 6 sub-batches of row-template application.
60+ rows now structured per spec §4.1.

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

---

### Task A4: Pravila — new §16 «Brain governance»

**Files:**

- Modify: `docs/Pravila_raboty_Claude_v1_1.md` (+ new §16)

- [ ] **Step 1: Pre-flight sync**

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

- [ ] **Step 2: Locate insertion point**

```bash
grep -n "^## §15\|^## §14" docs/Pravila_raboty_Claude_v1_1.md
```

Insert new §16 after §15 (parallel-sessions-coordination).

- [ ] **Step 3: Insert §16 block**

After §15.3 (or §15.X last subsection), add:

```markdown
## §16. Регламент «мозга» (brain governance)

**Hard-rule статус**: рекомендация уровня §13 (transitive через ADR-011 enforcement); НЕ override-floor §9. См. §16.5.

### §16.1. Router-only архитектура

Маршрутизация «задача → узел/узлы» исполняется ровно одной процедурой — [`docs/router-procedure.md`](../router-procedure.md). Никакого каталога «проверенных цепочек» нет; каждая задача — свежая сборка. Подробности — spec `docs/superpowers/specs/2026-05-19-brain-governance-design.md` §4.

### §16.2. Observer (scope B)

В Stop-event сессии Claude инвокирует хук `tools/observer-stop-hook.mjs`, который записывает одну JSONL-строку в `docs/observer/episodes-YYYY-MM.jsonl`. Дополнительные MD-заметки — `docs/observer/notes/YYYY-MM-DD-<slug>.md`.

Запись ОБЯЗАНА содержать 4 поля: `task_id` / `timestamps` / `path_type` / `outcome`. Структурированные события (`hook_fired` / `chain_divergence` / `skill_invoked` / `error` / `confusion_marker` / `time_burn`) — опционально в массиве `events[]`.

**ПДн-фильтр** через regex (phone `+7XXXXXXXXXX`, email `***@***`, токены gitleaks-style) — обязателен перед write.

**Граница**: observer **только пишет**, не правит нормативку. Решения принимаются вручную заказчиком через `/brain-retro` skill.

### §16.3. 4 контролёра

| # | Имя | Что закрывает | Реализация |
|---|---|---|---|
| C1 | L1-watcher | settings.json ↔ Tooling drift | lefthook + GitHub Actions weekly |
| C2 | Cross-ref consistency | version drift нормативных файлов | lefthook, regex |
| C3 | Observer-of-observer | observer evidence-loop устаревает | counter + lefthook warn, 54-week self-prune |
| C4 | STATUS.md | приборная панель | post-commit regen `docs/observer/STATUS.md` |

Все 4 — механические, 0 LLM-вызовов в hot path.

### §16.4. Поведенческое правило «не использован ≠ проблема»

Узел «мозга», не задействованный на реальной задаче, **не** считается проблемой и **не** подлежит автоматической пометке. Это — capability-readiness, осознанная стратегия заказчика. См. `memory/feedback_brain_unused_tools_not_problem.md`.

**Исключение**: deprecated upstream-пакеты или физически сломанные инструменты (отдельная категория, `npm audit` / `composer outdated`).

### §16.5. Не override-floor §9

§16 — рекомендация tier-уровня §13, НЕ explicit hard-rule вне §9. Тремя hard-rules вне §9 остаются §12 (Superpowers), §14 (Ruflo Queen — dormant), §15 (параллельные сессии).

ADR-011 enforcement через `adr-judge` lefthook job гарантирует существование секции `## Enforcement` в самом ADR.

### §16.6. Cross-refs

- ADR-011 `docs/adr/ADR-011-brain-governance.md`
- spec: `docs/superpowers/specs/2026-05-19-brain-governance-design.md`
- plan: `docs/superpowers/plans/2026-05-19-brain-governance.md`
- procedure: `docs/router-procedure.md`
- routing-table: `docs/routing-off-phase.md`
- evidence: `docs/observer/`
- memory: `feedback_brain_unused_tools_not_problem.md`, `project_brain_governance_design.md`
```

- [ ] **Step 4: Bump Pravila version**

In Pravila §0 header table bump `v1.30 → v1.31`. Add §10 changelog entry:

```markdown
### v1.31 (2026-05-19) — §16 brain governance

+§16 «Регламент «мозга»» (router-only + observer + 4 контролёра + поведенческое правило). Уровень рекомендации §13 — НЕ override-floor §9. См. ADR-011.

Связано: ADR-011, spec/plan brain-governance.
```

- [ ] **Step 5: Lint and commit**

```bash
git add docs/Pravila_raboty_Claude_v1_1.md
npx lefthook run pre-commit
git commit -m "docs(pravila): +§16 brain governance — router-only + observer + 4 controllers

Pravila v1.30 → v1.31. New §16 sub-sections 16.1-16.6. Level of §13
recommendation (not override-floor §9). Cross-refs ADR-011 / spec /
plan / router-procedure / routing-off-phase.

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

---

### Task A5: PSR_v1 — R16 «Brain evidence loop»

**Files:**

- Modify: `docs/Plugin_stack_rules_v1.md` (+ new R16)

- [ ] **Step 1: Pre-flight sync**

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

- [ ] **Step 2: Locate insertion point**

```bash
grep -n "^### R15\|^### R14" docs/Plugin_stack_rules_v1.md
```

Insert R16 after R15.

- [ ] **Step 3: Insert R16 block**

After R15.7 (or last R15 subsection), add:

```markdown
### R16. Brain evidence loop

**Status**: introduced PSR_v1 v3.16 (2026-05-19) per ADR-011.

#### R16.1. Observer scope

Observer Stop-hook (`tools/observer-stop-hook.mjs`) пишет evidence в `docs/observer/episodes-YYYY-MM.jsonl` каждую сессию. Поля: `task_id` / `timestamps` / `path_type` / `outcome` + optional `events[]`.

#### R16.2. Plugin stack-conscious events

Когда в сессии используется UI-фильтр стека (R6/R6.1) или off-phase узел (R15), observer записывает событие `skill_invoked` или `chain_divergence` с `node_id` (ссылка на Tooling Прил. Н §4.NN). Это позволяет `/brain-retro` проагрегировать «какие R6/R15 решения чаще всего применялись».

#### R16.3. Не override

R16 — evidence-сбор, не правило выбора. R0–R15 продолжают определять выбор узлов; R16 фиксирует историю.

#### R16.4. Cross-refs

- ADR-011
- Pravila §16
- spec/plan/router-procedure
```

- [ ] **Step 4: Bump PSR_v1 version**

Bump header `v3.15 → v3.16`. Add changelog entry.

- [ ] **Step 5: Lint and commit**

```bash
git add docs/Plugin_stack_rules_v1.md
npx lefthook run pre-commit
git commit -m "docs(psr): +R16 brain evidence loop — PSR_v1 v3.15 → v3.16

R16.1-R16.4: observer scope, stack-conscious events, non-override
status, cross-refs. Layered on top of R15 off-phase routing.

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

---

## Phase B — Observer infrastructure

### Task B1: docs/observer/ scaffolding

**Files:**

- Create: `docs/observer/README.md`
- Create: `docs/observer/.read-counter.json`
- Create: `docs/observer/episodes-2026-05.jsonl` (empty)
- Create: `docs/observer/notes/.gitkeep`
- Create: `docs/observer/STATUS.md` (placeholder)

- [ ] **Step 1: Create directory layout**

```bash
mkdir -p docs/observer/notes
touch docs/observer/notes/.gitkeep
```

- [ ] **Step 2: Create README**

Create `docs/observer/README.md`:

```markdown
# Observer infrastructure

Passive evidence-loop for the Лидерра «brain» per ADR-011.

## Files

- `episodes-YYYY-MM.jsonl` — append-only JSONL, one line per Claude session. Written by `tools/observer-stop-hook.mjs` on Stop-event.
- `notes/YYYY-MM-DD-<slug>.md` — optional MD notes for sessions with qualitative history.
- `STATUS.md` — auto-generated dashboard. Regenerated per-commit by `tools/status-md-generator.mjs`.
- `.read-counter.json` — C3 observer-of-observer counter. Updated on Read of observer files.

## Lifecycle

1. **Write**: every Claude session ends with a JSONL append (Stop-hook).
2. **Aggregate**: `/brain-retro` skill reads JSONL each sprint, proposes regulatory candidates.
3. **Surface**: `STATUS.md` shows controllers + monthly stats.
4. **Self-prune**: C3 warns if 54 weeks pass without any read of observer files.

## Privacy

PII filter (phone numbers, emails, tokens) is applied **before** every write — see `tools/observer-pii-filter.mjs`. gitleaks pre-push also scans observer files as part of full-history sweep.

## Don't

- Don't edit `episodes-*.jsonl` manually — it's append-only.
- Don't write outside `docs/observer/notes/` for hand-curated notes.
- Don't change `.read-counter.json` manually — it's maintained by hooks.
```

- [ ] **Step 3: Create .read-counter.json seed**

Create `docs/observer/.read-counter.json` with content:

```json
{
  "last_read_at": "2026-05-19T00:00:00+03:00",
  "read_count_last_period": 0,
  "period_start": "2026-05-19T00:00:00+03:00"
}
```

- [ ] **Step 4: Create empty current-month JSONL**

```bash
touch docs/observer/episodes-2026-05.jsonl
```

- [ ] **Step 5: Create STATUS.md placeholder**

Create `docs/observer/STATUS.md`:

```markdown
# Brain Status

Last updated: 2026-05-19T00:00:00+03:00 (placeholder)

Regenerator: `tools/status-md-generator.mjs` (post-commit hook).

| Контролёр | Состояние | Детали |
|---|---|---|
| C1 L1-watcher | ⚪ not yet run | Will run on first settings.json change or weekly cron |
| C2 Cross-ref consistency | ⚪ not yet run | Will run on first normative file edit |
| C3 Observer-of-observer | ⚪ not yet read | Counter: 0 reads. Self-prune in 54 weeks |
| C4 Сигнальный статус | ✅ | This file |

## Метрики (placeholder until first /brain-retro)

- Узлы, использованные за последние 30 дней: N/A (observer empty)
- Узлы, ни разу не использованные: N/A — **не проблема** (capability-readiness)
- Канонические связки L1–L12: N/A
- Improvised chains: N/A

⚪ — not yet run ・ ✅ — норма ・ ⚠️ — внимание ・ 🔴 — действие требуется
```

- [ ] **Step 6: Lint and commit**

```bash
git add docs/observer/
npx lefthook run pre-commit
git commit -m "feat(observer): docs/observer/ scaffolding — README + STATUS + counter + JSONL seed

Empty infrastructure. Hook + generators wire up in subsequent tasks.

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

---

### Task B2: PII filter — shared module

**Files:**

- Create: `tools/observer-pii-filter.mjs`
- Create: `tools/observer-pii-filter.test.mjs`

- [ ] **Step 1: Write failing test**

Create `tools/observer-pii-filter.test.mjs`:

```javascript
import { describe, it, expect } from 'vitest';
import { sanitize } from './observer-pii-filter.mjs';

describe('observer-pii-filter sanitize', () => {
  it('masks Russian phone numbers', () => {
    const input = 'Контакт: +79991234567 — позвонить';
    expect(sanitize(input)).toBe('Контакт: +7XXXXXXXXXX — позвонить');
  });

  it('masks email addresses', () => {
    const input = 'Mail: kpd9363@gmail.com';
    expect(sanitize(input)).toBe('Mail: ***@***');
  });

  it('masks Sentry-style tokens', () => {
    const input = 'token sntrys_abc123def456ghi789';
    expect(sanitize(input)).toContain('[REDACTED');
    expect(sanitize(input)).not.toContain('sntrys_abc123def456ghi789');
  });

  it('is idempotent on already-sanitized strings', () => {
    const sanitized = 'Контакт: +7XXXXXXXXXX, ***@***';
    expect(sanitize(sanitized)).toBe(sanitized);
  });

  it('handles empty string', () => {
    expect(sanitize('')).toBe('');
  });

  it('handles object input by sanitizing string fields recursively', () => {
    const input = { task_id: 'x', note: 'call +79991234567' };
    const out = sanitize(input);
    expect(out.note).toBe('call +7XXXXXXXXXX');
    expect(out.task_id).toBe('x');
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

```bash
npx vitest run tools/observer-pii-filter.test.mjs
```

Expected: FAIL — module not found OR `sanitize is not a function`.

- [ ] **Step 3: Implement minimal sanitize**

Create `tools/observer-pii-filter.mjs`:

```javascript
const RU_PHONE = /\+7\d{10}/g;
const EMAIL = /[\w.+-]+@[\w-]+\.[\w.-]+/g;
const SENTRY_TOKEN = /sntrys?_[A-Za-z0-9]{12,}/g;
const OPENAI_TOKEN = /sk-[A-Za-z0-9]{20,}/g;
const GENERIC_BEARER = /Bearer\s+[A-Za-z0-9._-]{20,}/g;

function sanitizeString(s) {
  if (typeof s !== 'string') return s;
  return s
    .replace(RU_PHONE, '+7XXXXXXXXXX')
    .replace(EMAIL, '***@***')
    .replace(SENTRY_TOKEN, '[REDACTED:sentry]')
    .replace(OPENAI_TOKEN, '[REDACTED:openai]')
    .replace(GENERIC_BEARER, '[REDACTED:bearer]');
}

export function sanitize(input) {
  if (typeof input === 'string') return sanitizeString(input);
  if (input === null || input === undefined) return input;
  if (Array.isArray(input)) return input.map(sanitize);
  if (typeof input === 'object') {
    const out = {};
    for (const [k, v] of Object.entries(input)) out[k] = sanitize(v);
    return out;
  }
  return input;
}
```

- [ ] **Step 4: Run test to verify it passes**

```bash
npx vitest run tools/observer-pii-filter.test.mjs
```

Expected: PASS, 6/6 tests.

- [ ] **Step 5: Commit**

```bash
git add tools/observer-pii-filter.mjs tools/observer-pii-filter.test.mjs
git commit -m "feat(observer): PII filter — phone/email/Sentry/OpenAI/Bearer masking

Used by Stop-hook before JSONL write. 6 Vitest cases including
idempotence and recursive object sanitization.

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

---

### Task B3: Stop-event hook

**Files:**

- Create: `tools/observer-stop-hook.mjs`
- Create: `tools/observer-stop-hook.test.mjs`

- [ ] **Step 1: Write failing test**

Create `tools/observer-stop-hook.test.mjs`:

```javascript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { writeFileSync, readFileSync, existsSync, mkdtempSync, rmSync, mkdirSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { appendEpisode, buildEpisodeFromContext } from './observer-stop-hook.mjs';

let workdir;

beforeEach(() => {
  workdir = mkdtempSync(join(tmpdir(), 'observer-test-'));
  mkdirSync(join(workdir, 'docs', 'observer'), { recursive: true });
});

afterEach(() => {
  rmSync(workdir, { recursive: true, force: true });
});

// Helper for v1.1 — primary_rationale fixture (factor analysis amendment)
const defaultRat = () => ({
  step: 1,
  node_chosen: '#1',
  triggers_matched: [],
  candidates_considered: [],
  boundaries_applied: [],
  hard_floor: { invoked: false, rules: [] },
  task_classification: 'other',
});

describe('appendEpisode', () => {
  it('appends one JSONL line to monthly file', () => {
    const ep = {
      task_id: 'abc-123',
      timestamps: { started_at: '2026-05-19T10:00:00+03:00', ended_at: '2026-05-19T10:05:00+03:00' },
      path_type: 'regulated',
      outcome: 'success',
      primary_rationale: defaultRat(),
    };
    appendEpisode(ep, workdir, '2026-05');
    const file = join(workdir, 'docs', 'observer', 'episodes-2026-05.jsonl');
    const content = readFileSync(file, 'utf-8');
    expect(content).toContain('"task_id":"abc-123"');
    expect(content).toContain('"primary_rationale"');
    expect(content.endsWith('\n')).toBe(true);
  });

  it('appends to existing file without overwrite', () => {
    appendEpisode({ task_id: 'a', timestamps: {}, path_type: 'regulated', outcome: 'success', primary_rationale: defaultRat() }, workdir, '2026-05');
    appendEpisode({ task_id: 'b', timestamps: {}, path_type: 'improvised', outcome: 'partial', primary_rationale: defaultRat() }, workdir, '2026-05');
    const lines = readFileSync(join(workdir, 'docs', 'observer', 'episodes-2026-05.jsonl'), 'utf-8').trim().split('\n');
    expect(lines).toHaveLength(2);
    expect(JSON.parse(lines[0]).task_id).toBe('a');
    expect(JSON.parse(lines[1]).task_id).toBe('b');
  });

  it('applies PII filter before write (including events[])', () => {
    appendEpisode({
      task_id: 'c',
      timestamps: {},
      path_type: 'regulated',
      outcome: 'success',
      primary_rationale: defaultRat(),
      events: [{ kind: 'error', message: 'call +79991234567 / mail x@y.com' }],
    }, workdir, '2026-05');
    const content = readFileSync(join(workdir, 'docs', 'observer', 'episodes-2026-05.jsonl'), 'utf-8');
    expect(content).toContain('+7XXXXXXXXXX');
    expect(content).toContain('***@***');
    expect(content).not.toContain('79991234567');
  });

  it('throws on missing required top-level fields', () => {
    expect(() => appendEpisode({}, workdir, '2026-05')).toThrow(/required/i);
    expect(() => appendEpisode({ task_id: 'x' }, workdir, '2026-05')).toThrow(/required/i);
    expect(() => appendEpisode({ task_id: 'x', timestamps: {}, path_type: 'regulated', outcome: 'success' }, workdir, '2026-05')).toThrow(/primary_rationale/i);
  });

  it('throws when primary_rationale field is missing', () => {
    const ep = {
      task_id: 'd',
      timestamps: {},
      path_type: 'regulated',
      outcome: 'success',
      primary_rationale: { step: 1, node_chosen: '#1' }, // missing other 5 fields
    };
    expect(() => appendEpisode(ep, workdir, '2026-05')).toThrow(/primary_rationale field missing/i);
  });

  it('persists routing_decision events with structured fields', () => {
    appendEpisode({
      task_id: 'e',
      timestamps: {},
      path_type: 'regulated',
      outcome: 'success',
      primary_rationale: defaultRat(),
      events: [
        { kind: 'routing_decision', step: 1, node_chosen: '#55', triggers_matched: ['discovery'],
          candidates_considered: [{ node_id: '#53', dropped_because: 'ADR-009' }],
          boundaries_applied: ['ADR-009'], hard_floor: { invoked: false, rules: [] },
          task_classification: 'discovery' },
      ],
    }, workdir, '2026-05');
    const line = JSON.parse(readFileSync(join(workdir, 'docs', 'observer', 'episodes-2026-05.jsonl'), 'utf-8').trim());
    expect(line.events[0].kind).toBe('routing_decision');
    expect(line.events[0].triggers_matched).toEqual(['discovery']);
    expect(line.events[0].candidates_considered[0].dropped_because).toBe('ADR-009');
    expect(line.events[0].boundaries_applied).toEqual(['ADR-009']);
  });
});

describe('buildEpisodeFromContext', () => {
  it('extracts 5 mandatory fields from context object', () => {
    const ctx = {
      sessionId: 'sess-1',
      started: '2026-05-19T09:00:00+03:00',
      ended: '2026-05-19T09:30:00+03:00',
      result: 'success',
    };
    const ep = buildEpisodeFromContext(ctx);
    expect(ep.task_id).toBe('sess-1');
    expect(ep.timestamps.started_at).toBe(ctx.started);
    expect(ep.outcome).toBe('success');
    expect(['regulated', 'improvised', 'alternative', 'mixed']).toContain(ep.path_type);
    expect(ep.primary_rationale).toBeDefined();
    expect(ep.primary_rationale.step).toBe(1);
    expect(ep.primary_rationale.hard_floor).toEqual({ invoked: false, rules: [] });
  });

  it('preserves user-provided primary_rationale unchanged', () => {
    const rat = {
      step: 1, node_chosen: '#55', triggers_matched: ['discovery'],
      candidates_considered: [], boundaries_applied: ['ADR-009'],
      hard_floor: { invoked: true, rules: ['Pravila §12'] },
      task_classification: 'discovery',
    };
    const ep = buildEpisodeFromContext({ sessionId: 'x', primary_rationale: rat });
    expect(ep.primary_rationale).toEqual(rat);
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

```bash
npx vitest run tools/observer-stop-hook.test.mjs
```

Expected: FAIL — module not found.

- [ ] **Step 3: Implement**

Create `tools/observer-stop-hook.mjs`:

```javascript
#!/usr/bin/env node
import { appendFileSync, existsSync, mkdirSync } from 'fs';
import { join } from 'path';
import { sanitize } from './observer-pii-filter.mjs';

const REQUIRED_FIELDS = ['task_id', 'timestamps', 'path_type', 'outcome', 'primary_rationale'];

const RATIONALE_FIELDS = ['step', 'node_chosen', 'triggers_matched', 'candidates_considered', 'boundaries_applied', 'hard_floor', 'task_classification'];

function validateRationale(rationale) {
  for (const f of RATIONALE_FIELDS) {
    if (rationale[f] === undefined) throw new Error(`primary_rationale field missing: ${f}`);
  }
}

export function appendEpisode(episode, baseDir = process.cwd(), month = currentMonth()) {
  for (const f of REQUIRED_FIELDS) {
    if (episode[f] === undefined) throw new Error(`required field missing: ${f}`);
  }
  validateRationale(episode.primary_rationale);
  const sanitized = sanitize(episode);
  const dir = join(baseDir, 'docs', 'observer');
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
  const file = join(dir, `episodes-${month}.jsonl`);
  appendFileSync(file, JSON.stringify(sanitized) + '\n', 'utf-8');
}

export function buildEpisodeFromContext(ctx = {}) {
  return {
    task_id: ctx.sessionId || ctx.task_id || `unknown-${Date.now()}`,
    timestamps: {
      started_at: ctx.started || ctx.started_at || new Date().toISOString(),
      ended_at: ctx.ended || ctx.ended_at || new Date().toISOString(),
    },
    path_type: ctx.path_type || 'regulated',
    outcome: ctx.result || ctx.outcome || 'success',
    primary_rationale: ctx.primary_rationale || {
      step: 1,
      node_chosen: ctx.node_chosen || ctx.skill_id || 'unknown',
      triggers_matched: ctx.triggers_matched || [],
      candidates_considered: ctx.candidates_considered || [],
      boundaries_applied: ctx.boundaries_applied || [],
      hard_floor: ctx.hard_floor || { invoked: false, rules: [] },
      task_classification: ctx.task_classification || 'other',
    },
    events: ctx.events || [],
  };
}

function currentMonth() {
  const d = new Date();
  return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}`;
}

// CLI entry: read JSON context from stdin (Claude Code hook contract)
if (import.meta.url === `file://${process.argv[1].replace(/\\/g, '/')}`) {
  let chunks = [];
  process.stdin.on('data', (c) => chunks.push(c));
  process.stdin.on('end', () => {
    let ctx = {};
    try {
      const raw = Buffer.concat(chunks).toString('utf-8');
      if (raw.trim()) ctx = JSON.parse(raw);
    } catch (e) {
      // best-effort: write minimal episode anyway
    }
    const ep = buildEpisodeFromContext(ctx);
    try {
      appendEpisode(ep);
      process.exit(0);
    } catch (err) {
      console.error(`[observer-stop-hook] error: ${err.message}`);
      process.exit(0); // never block Stop-event
    }
  });
}
```

- [ ] **Step 4: Run test to verify it passes**

```bash
npx vitest run tools/observer-stop-hook.test.mjs
```

Expected: PASS, all tests.

- [ ] **Step 5: Manual smoke**

```bash
echo '{"sessionId":"smoke-1","started":"2026-05-19T10:00:00+03:00","ended":"2026-05-19T10:01:00+03:00","result":"success"}' | node tools/observer-stop-hook.mjs
cat docs/observer/episodes-2026-05.jsonl
```

Expected: one JSONL line with `task_id: "smoke-1"`.

- [ ] **Step 6: Clean smoke artefact**

```bash
git checkout docs/observer/episodes-2026-05.jsonl
```

- [ ] **Step 7: Commit**

```bash
git add tools/observer-stop-hook.mjs tools/observer-stop-hook.test.mjs
git commit -m "feat(observer): Stop-event hook — JSONL append with PII filter

Hook contract: reads JSON ctx from stdin, builds episode with 4
mandatory fields, sanitizes via observer-pii-filter, appends to
docs/observer/episodes-YYYY-MM.jsonl. Never blocks Stop-event
(exit 0 on error).

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

---

### Task B4: HK1 pre-check — verify hook collision

**Files:**

- Read: `~/.claude/settings.json` AND `.claude/settings.json`

- [ ] **Step 1: Read current Stop-event handlers**

```bash
cat ~/.claude/settings.json 2>/dev/null | grep -A 5 "Stop" || echo "no user-level Stop"
cat .claude/settings.json 2>/dev/null | grep -A 5 "Stop" || echo "no project-level Stop"
```

- [ ] **Step 2: Inventory the 6-component economy/skill-discipline architecture**

Current arch components (per `memory/feedback_superpowers_hard_rule.md`):

1. skill-marker
2. skill-check
3. state-guard
4. postcompact
5. verifier
6. economy-mode

Verify none of these is registered on Stop-event.

- [ ] **Step 3: Confirm no collision**

If grep shows existing `Stop` handler(s) — design as **chain**: existing handler runs first, observer-stop-hook runs second.

If no existing `Stop` — observer-stop-hook is sole handler.

- [ ] **Step 4: Document HK1 result inline**

Append to `docs/observer/README.md` a one-line note: `HK1 pre-check 2026-05-19: <result>`.

- [ ] **Step 5: Stage and commit if README changed**

```bash
git add docs/observer/README.md
git commit -m "docs(observer): HK1 pre-check noted in README (ADR-010 compliance)"
```

If no change needed — skip.

---

### Task B5: Register Stop-hook in settings.json

**Files:**

- Modify: `.claude/settings.json` (project-level)

- [ ] **Step 1: Backup current settings**

```bash
cp .claude/settings.json .claude/settings.json.bak
```

- [ ] **Step 2: Read current settings structure**

```bash
cat .claude/settings.json
```

Identify the `hooks` block.

- [ ] **Step 3: Add observer-stop-hook entry**

Edit `.claude/settings.json` — within `hooks.Stop` array (create if missing), add:

```json
{
  "type": "command",
  "command": "node tools/observer-stop-hook.mjs",
  "timeout": 5
}
```

Validate against `https://json.schemastore.org/claude-code-settings.json` (per CLAUDE.md §3.8).

- [ ] **Step 4: Smoke Claude session** (semi-manual)

Trigger a small Claude prompt + Stop. Verify `docs/observer/episodes-2026-05.jsonl` gets a new line.

- [ ] **Step 5: Clean smoke entry**

```bash
git checkout docs/observer/episodes-2026-05.jsonl
rm .claude/settings.json.bak
```

- [ ] **Step 6: Commit settings change**

```bash
git add .claude/settings.json
git commit -m "feat(observer): register observer-stop-hook on Stop-event

HK1 pre-check passed in Task B4. Hook timeout 5s; never blocks (exit 0
on error). Per Pravila §16.2 and ADR-011.

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

---

### Task B6: /brain-retro skill

**Files:**

- Create: `.claude/skills/brain-retro/SKILL.md`
- Create: `.claude/skills/brain-retro/references/aggregation-template.md`

- [ ] **Step 1: Inventory existing project skills for style**

```bash
ls .claude/skills/
```

Pick `discovery-interview/SKILL.md` as style baseline.

- [ ] **Step 2: Create SKILL.md**

Create `.claude/skills/brain-retro/SKILL.md`:

```markdown
---
name: brain-retro
description: Use ONCE PER SPRINT (or by explicit user invocation "брейн-ретро") to aggregate evidence from docs/observer/episodes-*.jsonl + notes/*.md and propose regulatory candidates. Read-only — never edits Tooling/Pravila/PSR_v1 automatically; only proposes.
---

# Brain Retro

Aggregator over observer evidence. Reads JSONL + optional MD notes, surfaces candidates for normative updates. User decides what to apply.

## When to invoke

- Explicit user request: «брейн-ретро» / «сделай brain-retro» / `/brain-retro`.
- Periodic — owner discretion (e.g. end of sprint).
- NOT auto-invoked.

## What it does NOT do

- Does NOT edit `docs/Tooling_v8_3.md`, `docs/Pravila_raboty_Claude_v1_1.md`, `docs/Plugin_stack_rules_v1.md`, `CLAUDE.md`, or any normative file.
- Does NOT write to `docs/observer/episodes-*.jsonl` (read-only).
- Does NOT trigger automatic memory updates.

## Procedure

1. **Determine period**: ask user «за какой период» or default to «since last brain-retro» (find latest `docs/observer/notes/YYYY-MM-DD-brain-retro-*.md`).
2. **Read evidence**: glob `docs/observer/episodes-YYYY-MM.jsonl` for the period; read all lines as JSON.
3. **Read optional notes**: glob `docs/observer/notes/*.md` filtered by date.
4. **Update read-counter**: bump `docs/observer/.read-counter.json` `last_read_at` to now, increment `read_count_last_period`. (Side-effect — used by C3.)
5. **Aggregate** per `references/aggregation-template.md`.
6. **Propose candidates** — clearly separated section «Candidates for owner review». Each candidate has rationale + suggested edit + rejection-option.
7. **Save retro note**: `docs/observer/notes/YYYY-MM-DD-brain-retro.md` with full aggregation.
8. **Report to user**: high-signal summary.

## Output anatomy

See `references/aggregation-template.md`.

## Behavioral rule reminders

- **«Не использован ≠ проблема»** — when reporting node usage counts, NEVER mark unused nodes as «zombie» / «removal candidate». Cite `memory/feedback_brain_unused_tools_not_problem.md`.
- **No auto-edit** — every regulatory suggestion is a candidate, not an action.
```

- [ ] **Step 3: Create aggregation template**

Create `.claude/skills/brain-retro/references/aggregation-template.md`:

```markdown
# Brain-retro aggregation template

## Period

YYYY-MM-DD .. YYYY-MM-DD ({N} sessions)

## Path-type distribution

| path_type | count | % |
|---|---|---|
| regulated | A | x% |
| improvised | B | y% |
| alternative | C | z% |
| mixed | D | w% |

## Outcome distribution

| outcome | count |
|---|---|
| success | M |
| partial | N |
| failure | O |
| aborted | P |

## Top nodes used (from `skill_invoked` events)

| node | times used | first / last |
|---|---|---|

## Factor analysis matrix (v1.1+ — from `routing_decision` events + `primary_rationale`)

Per-node breakdown by 5 factor axes (per spec §5.2.1).

### Axis 1: triggers_matched → node

«Какие триггеры чаще всего ведут к этому узлу»

| node | trigger | count |
|---|---|---|

### Axis 2: candidates_considered.dropped_because → node

«Какие альтернативы и почему отбрасываются в пользу этого узла»

| node | dropped alternative | dropped_because (top reason) | count |
|---|---|---|---|

### Axis 3: boundaries_applied → node

«Какие ADR / R-rules чаще всего разруливают в пользу этого узла»

| node | boundary (ADR-NNN / PSR R-rule) | count |
|---|---|---|

### Axis 4: hard_floor.rules → invoked frequency

«Как часто узел вынуждается hard-floor §12/§14/§15»

| node | rule | invoked-count | total decisions | % forced |
|---|---|---|---|---|

### Axis 5: task_classification → node

«В каких классах задач узел доминирует»

| task_classification | top node | secondary nodes |
|---|---|---|

### Cross-tab: factor × factor

Pairs of factors that co-occur to resolve specific conflicts (e.g. «ADR-009 ↔ triggers_matched=['discovery']» — 8 раз).

| factor pair | combined count | interpretation |
|---|---|---|

## Canonical chains L1–L12 hit rate

| chain | times | notes |
|---|---|---|

## Improvised chains (path_type=improvised, repeated ≥2)

| node-set | times | candidate L13+? |
|---|---|---|

## chain_divergence cases

| canonical | chosen | reason | recurring? |
|---|---|---|---|

## Top error classes

| error class | count | recovery pattern |
|---|---|---|

## confusion_marker hot-spots

| context | count |
|---|---|

## Candidates for owner review

### Candidate 1: <title>

- **Type**: new canonical chain L13+ / new ADR / boundary clarification / etc.
- **Evidence**: refs to JSONL lines (file:line).
- **Suggested action**: <concrete edit>.
- **Cost / risk**: <brief>.

(repeat for each candidate; could be 0)

## Informational metrics (NOT alerts)

- Nodes used at least once this period: K / 60+
- Nodes never used since beginning of observer logs: L / 60+ — **not a problem** per [feedback_brain_unused_tools_not_problem](../../../memory/feedback_brain_unused_tools_not_problem.md)
```

- [ ] **Step 4: Commit**

```bash
git add .claude/skills/brain-retro/
git commit -m "feat(skills): /brain-retro — observer evidence aggregator

Read-only skill. Aggregates JSONL evidence + optional notes for owner
review. Side-effect: bumps .read-counter.json (used by C3). Never
auto-edits normative files. Per Pravila §16.2 and ADR-011.

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

---

## Phase C — 4 mechanical controllers

### Task C1: L1-watcher

**Files:**

- Create: `tools/l1-watcher.mjs`
- Create: `tools/l1-watcher.test.mjs`
- Create: `.github/workflows/brain-l1-watcher-weekly.yml`

- [ ] **Step 1: Write failing test**

Create `tools/l1-watcher.test.mjs`:

```javascript
import { describe, it, expect } from 'vitest';
import { detectDrift } from './l1-watcher.mjs';

describe('detectDrift', () => {
  it('finds plugins in settings but not in tooling', () => {
    const settings = { enabledPlugins: { 'foo@org': true, 'bar@org': true } };
    const tooling = 'Описание #56 foo@org интегрирован.';
    const drift = detectDrift(settings, tooling);
    expect(drift.missingInTooling).toEqual(['bar@org']);
    expect(drift.missingInSettings).toEqual([]);
  });

  it('finds plugins in tooling but not in settings', () => {
    const settings = { enabledPlugins: { 'foo@org': true } };
    const tooling = '#56 foo@org. #57 baz@org включён.';
    const drift = detectDrift(settings, tooling);
    expect(drift.missingInSettings).toEqual(['baz@org']);
  });

  it('returns empty arrays when in sync', () => {
    const settings = { enabledPlugins: { 'foo@org': true } };
    const tooling = '#56 foo@org описан.';
    const drift = detectDrift(settings, tooling);
    expect(drift.missingInTooling).toEqual([]);
    expect(drift.missingInSettings).toEqual([]);
  });

  it('handles disabled plugins (value false)', () => {
    const settings = { enabledPlugins: { 'foo@org': false, 'bar@org': true } };
    const tooling = '#56 bar@org.';
    const drift = detectDrift(settings, tooling);
    expect(drift.missingInTooling).toEqual([]);
  });
});
```

- [ ] **Step 2: Run test — verify it fails**

```bash
npx vitest run tools/l1-watcher.test.mjs
```

Expected: FAIL.

- [ ] **Step 3: Implement**

Create `tools/l1-watcher.mjs`:

```javascript
#!/usr/bin/env node
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';

const PLUGIN_NAME_PATTERN = /[a-z][\w-]*(?:@[\w-]+)?/g;

export function detectDrift(settings, toolingText) {
  const enabled = Object.entries(settings.enabledPlugins || {})
    .filter(([, v]) => v === true)
    .map(([k]) => k);
  const found = new Set();
  for (const m of toolingText.matchAll(PLUGIN_NAME_PATTERN)) found.add(m[0]);
  const missingInTooling = enabled.filter((p) => !found.has(p));
  const inToolingButNotSettings = [];
  const toolingNumbered = toolingText.match(/#\d+\s+([\w-]+(?:@[\w-]+)?)/g) || [];
  const toolingPluginNames = toolingNumbered.map((s) => s.split(/\s+/)[1]);
  for (const p of toolingPluginNames) {
    if (!enabled.includes(p)) inToolingButNotSettings.push(p);
  }
  return { missingInTooling, missingInSettings: inToolingButNotSettings };
}

function loadFileMaybe(path) {
  try {
    return existsSync(path) ? readFileSync(path, 'utf-8') : null;
  } catch {
    return null;
  }
}

export function loadInputs(projectRoot = process.cwd()) {
  const userSettings = JSON.parse(loadFileMaybe(join(homedir(), '.claude', 'settings.json')) || '{}');
  const projectSettings = JSON.parse(loadFileMaybe(join(projectRoot, '.claude', 'settings.json')) || '{}');
  const merged = {
    enabledPlugins: { ...(userSettings.enabledPlugins || {}), ...(projectSettings.enabledPlugins || {}) },
  };
  const tooling = loadFileMaybe(join(projectRoot, 'docs', 'Tooling_v8_3.md')) || '';
  return { settings: merged, tooling };
}

if (import.meta.url === `file://${process.argv[1].replace(/\\/g, '/')}`) {
  const { settings, tooling } = loadInputs();
  const drift = detectDrift(settings, tooling);
  if (drift.missingInTooling.length > 0) {
    console.error(`[l1-watcher] FAIL — plugins in settings but not formalized in Tooling Прил. Н:`);
    drift.missingInTooling.forEach((p) => console.error(`  - ${p}`));
    console.error(`Run /claude-md-management:claude-md-improver to formalize.`);
    process.exit(1);
  }
  if (drift.missingInSettings.length > 0) {
    console.warn(`[l1-watcher] WARN — plugins in Tooling but disabled in settings:`);
    drift.missingInSettings.forEach((p) => console.warn(`  - ${p}`));
  }
  console.log(`[l1-watcher] OK — 0 drift`);
  process.exit(0);
}
```

- [ ] **Step 4: Run test — verify it passes**

```bash
npx vitest run tools/l1-watcher.test.mjs
```

Expected: PASS, 4/4.

- [ ] **Step 5: Smoke run on real repo**

```bash
node tools/l1-watcher.mjs
```

Expected: `[l1-watcher] OK — 0 drift`.

- [ ] **Step 6: Create weekly GitHub Actions workflow**

Create `.github/workflows/brain-l1-watcher-weekly.yml`:

```yaml
name: brain-l1-watcher (weekly)

on:
  schedule:
    - cron: '0 6 * * 1'
  workflow_dispatch:

jobs:
  drift:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
      - name: run l1-watcher
        id: l1
        run: node tools/l1-watcher.mjs
        continue-on-error: true
      - name: open issue on drift
        if: steps.l1.outcome == 'failure'
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.create({
              owner: context.repo.owner,
              repo: context.repo.repo,
              title: `[l1-watcher] drift detected (weekly cron ${new Date().toISOString().slice(0,10)})`,
              body: `Run failed. Check workflow logs and run /claude-md-management:claude-md-improver.`,
              labels: ['brain', 'drift']
            });
```

- [ ] **Step 7: Commit**

```bash
git add tools/l1-watcher.mjs tools/l1-watcher.test.mjs .github/workflows/brain-l1-watcher-weekly.yml
git commit -m "feat(controller): C1 l1-watcher — settings.json ↔ Tooling drift detector

Pure regex/JSON, 0 LLM calls. Lefthook wire-up in Task C5. Weekly
GH Actions cron opens issue on drift. Per spec §6.1.

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

---

### Task C2: Cross-ref consistency

**Files:**

- Create: `tools/cross-ref-checker.mjs`
- Create: `tools/cross-ref-checker.test.mjs`

- [ ] **Step 1: Write failing test**

Create `tools/cross-ref-checker.test.mjs`:

```javascript
import { describe, it, expect } from 'vitest';
import { extractVersion, extractCrossRefs, detectMismatches } from './cross-ref-checker.mjs';

describe('extractVersion', () => {
  it('extracts version from header', () => {
    const text = '# Pravila v1.30 от 18.05.2026';
    expect(extractVersion(text)).toBe('1.30');
  });

  it('returns null if no version', () => {
    expect(extractVersion('# Untitled')).toBeNull();
  });
});

describe('extractCrossRefs', () => {
  it('extracts cross-refs like "Pravila v1.29"', () => {
    const text = 'See Pravila v1.29 and Tooling v2.15.';
    const refs = extractCrossRefs(text);
    expect(refs).toContainEqual({ name: 'Pravila', version: '1.29' });
    expect(refs).toContainEqual({ name: 'Tooling', version: '2.15' });
  });
});

describe('detectMismatches', () => {
  it('detects when fileA references fileB v1.29 but fileB header is v1.30', () => {
    const files = {
      'A.md': '# A v1.0 — cross-refs: Pravila v1.29',
      'docs/Pravila_raboty_Claude_v1_1.md': '# Pravila v1.30',
    };
    const m = detectMismatches(files);
    expect(m).toHaveLength(1);
    expect(m[0].from).toBe('A.md');
    expect(m[0].to).toBe('Pravila');
    expect(m[0].expected).toBe('1.30');
    expect(m[0].found).toBe('1.29');
  });

  it('passes when in sync', () => {
    const files = {
      'A.md': '# A v1.0 — Pravila v1.30',
      'docs/Pravila_raboty_Claude_v1_1.md': '# Pravila v1.30',
    };
    expect(detectMismatches(files)).toHaveLength(0);
  });
});
```

- [ ] **Step 2: Run test — verify failure**

```bash
npx vitest run tools/cross-ref-checker.test.mjs
```

Expected: FAIL.

- [ ] **Step 3: Implement**

Create `tools/cross-ref-checker.mjs`:

```javascript
#!/usr/bin/env node
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';

const NORMATIVE_FILES = {
  Pravila: 'docs/Pravila_raboty_Claude_v1_1.md',
  CLAUDE: 'CLAUDE.md',
  Tooling: 'docs/Tooling_v8_3.md',
  PSR_v1: 'docs/Plugin_stack_rules_v1.md',
  MEMORY: 'MEMORY.md',
};

const VERSION_RE = /v(\d+\.\d+(?:\.\d+)?)/;
const CROSS_REF_RE = /\b(Pravila|CLAUDE|Tooling|PSR_v1|MEMORY|PSR|Plugin_stack_rules|Pravila_raboty_Claude)\s+v(\d+\.\d+(?:\.\d+)?)/g;

export function extractVersion(text) {
  const lines = text.split('\n').slice(0, 10);
  for (const l of lines) {
    const m = VERSION_RE.exec(l);
    if (m) return m[1];
  }
  return null;
}

export function extractCrossRefs(text) {
  const refs = [];
  for (const m of text.matchAll(CROSS_REF_RE)) {
    let name = m[1];
    if (name === 'PSR' || name === 'Plugin_stack_rules') name = 'PSR_v1';
    if (name === 'Pravila_raboty_Claude') name = 'Pravila';
    refs.push({ name, version: m[2] });
  }
  return refs;
}

export function detectMismatches(files) {
  const headerVersions = {};
  for (const [shortName, path] of Object.entries(NORMATIVE_FILES)) {
    const entry = Object.entries(files).find(([k]) => k === path || k.endsWith(path));
    if (entry) headerVersions[shortName] = extractVersion(entry[1]);
  }
  const mismatches = [];
  for (const [path, text] of Object.entries(files)) {
    const refs = extractCrossRefs(text);
    for (const r of refs) {
      const expected = headerVersions[r.name];
      if (expected && expected !== r.version) {
        mismatches.push({ from: path, to: r.name, expected, found: r.version });
      }
    }
  }
  return mismatches;
}

function loadFiles(root = process.cwd()) {
  const out = {};
  for (const [, path] of Object.entries(NORMATIVE_FILES)) {
    const abs = join(root, path);
    if (existsSync(abs)) out[path] = readFileSync(abs, 'utf-8');
  }
  return out;
}

if (import.meta.url === `file://${process.argv[1].replace(/\\/g, '/')}`) {
  const files = loadFiles();
  const m = detectMismatches(files);
  if (m.length === 0) {
    console.log(`[cross-ref-checker] OK — 0 drift in ${Object.keys(files).length} files`);
    process.exit(0);
  }
  console.error(`[cross-ref-checker] FAIL — version drift detected:`);
  for (const x of m) {
    console.error(`  ${x.from} references ${x.to} v${x.found}, but ${x.to} header is v${x.expected}`);
  }
  console.error(`Update §0 cross-refs in offending files.`);
  process.exit(1);
}
```

- [ ] **Step 4: Run test — verify PASS**

```bash
npx vitest run tools/cross-ref-checker.test.mjs
```

Expected: 6/6 pass.

- [ ] **Step 5: Smoke on real repo**

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

Expected: `OK — 0 drift` (or surfaces real drift, which is the intended behaviour).

- [ ] **Step 6: Commit**

```bash
git add tools/cross-ref-checker.mjs tools/cross-ref-checker.test.mjs
git commit -m "feat(controller): C2 cross-ref-checker — version drift detector

Pure regex over 5 normative files. 0 LLM calls. Lefthook wire-up in
Task C5. Per spec §6.2.

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

---

### Task C3: Observer-of-observer

**Files:**

- Create: `tools/observer-of-observer.mjs`
- Create: `tools/observer-of-observer.test.mjs`

- [ ] **Step 1: Write failing test**

Create `tools/observer-of-observer.test.mjs`:

```javascript
import { describe, it, expect } from 'vitest';
import { isStale, weeksSince } from './observer-of-observer.mjs';

describe('weeksSince', () => {
  it('returns 0 for now', () => {
    const now = new Date('2026-05-19T00:00:00Z');
    expect(weeksSince(now.toISOString(), now)).toBe(0);
  });

  it('returns 1 for 7 days ago', () => {
    const past = new Date('2026-05-12T00:00:00Z');
    const now = new Date('2026-05-19T00:00:00Z');
    expect(weeksSince(past.toISOString(), now)).toBe(1);
  });

  it('returns 54 for ~1 year ago', () => {
    const past = new Date('2025-05-05T00:00:00Z');
    const now = new Date('2026-05-19T00:00:00Z');
    expect(weeksSince(past.toISOString(), now)).toBeGreaterThanOrEqual(54);
  });
});

describe('isStale', () => {
  it('false when last_read_at is recent', () => {
    const counter = { last_read_at: '2026-05-12T00:00:00Z' };
    const now = new Date('2026-05-19T00:00:00Z');
    expect(isStale(counter, 54, now)).toBe(false);
  });

  it('true when last_read_at is >54 weeks ago', () => {
    const counter = { last_read_at: '2025-05-05T00:00:00Z' };
    const now = new Date('2026-05-19T00:00:00Z');
    expect(isStale(counter, 54, now)).toBe(true);
  });
});
```

- [ ] **Step 2: Run test — verify failure**

```bash
npx vitest run tools/observer-of-observer.test.mjs
```

Expected: FAIL.

- [ ] **Step 3: Implement**

Create `tools/observer-of-observer.mjs`:

```javascript
#!/usr/bin/env node
import { readFileSync, writeFileSync, existsSync } from 'fs';

const COUNTER_PATH = 'docs/observer/.read-counter.json';
const SELF_PRUNE_WEEKS = 54;

export function weeksSince(isoTimestamp, now = new Date()) {
  const past = new Date(isoTimestamp);
  const ms = now - past;
  return Math.floor(ms / (1000 * 60 * 60 * 24 * 7));
}

export function isStale(counter, thresholdWeeks = SELF_PRUNE_WEEKS, now = new Date()) {
  if (!counter || !counter.last_read_at) return true;
  return weeksSince(counter.last_read_at, now) > thresholdWeeks;
}

export function recordRead(counterPath = COUNTER_PATH, now = new Date()) {
  let counter = { last_read_at: now.toISOString(), read_count_last_period: 0, period_start: now.toISOString() };
  if (existsSync(counterPath)) {
    try {
      counter = JSON.parse(readFileSync(counterPath, 'utf-8'));
    } catch {}
  }
  counter.last_read_at = now.toISOString();
  counter.read_count_last_period = (counter.read_count_last_period || 0) + 1;
  writeFileSync(counterPath, JSON.stringify(counter, null, 2) + '\n');
  return counter;
}

if (import.meta.url === `file://${process.argv[1].replace(/\\/g, '/')}`) {
  const mode = process.argv[2] || 'check';
  if (mode === 'record') {
    recordRead();
    console.log(`[observer-of-observer] recorded read`);
    process.exit(0);
  }
  if (!existsSync(COUNTER_PATH)) {
    console.warn(`[observer-of-observer] WARN — counter file missing: ${COUNTER_PATH}`);
    process.exit(0);
  }
  const counter = JSON.parse(readFileSync(COUNTER_PATH, 'utf-8'));
  if (isStale(counter)) {
    console.warn(`[observer-of-observer] WARN — observer infrastructure not read for >${SELF_PRUNE_WEEKS} weeks.`);
    console.warn(`  last_read_at: ${counter.last_read_at}`);
    console.warn(`  Consider self-pruning: archive docs/observer/ and remove Stop-hook.`);
    process.exit(0);
  }
  console.log(`[observer-of-observer] OK — last read ${weeksSince(counter.last_read_at)} week(s) ago`);
  process.exit(0);
}
```

- [ ] **Step 4: Run test — verify PASS**

```bash
npx vitest run tools/observer-of-observer.test.mjs
```

Expected: 5/5 pass.

- [ ] **Step 5: Smoke check + record**

```bash
node tools/observer-of-observer.mjs check
node tools/observer-of-observer.mjs record
cat docs/observer/.read-counter.json
```

Expected: counter updated; check reports OK.

- [ ] **Step 6: Restore counter**

```bash
git checkout docs/observer/.read-counter.json
```

- [ ] **Step 7: Commit**

```bash
git add tools/observer-of-observer.mjs tools/observer-of-observer.test.mjs
git commit -m "feat(controller): C3 observer-of-observer — self-prune counter

54-week threshold. Modes: check (lefthook warn) / record (manual or
hook-invoked on Read of observer files). Per spec §6.3.

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

---

### Task C4: STATUS.md generator

**Files:**

- Create: `tools/status-md-generator.mjs`
- Create: `tools/status-md-generator.test.mjs`

- [ ] **Step 1: Write failing test**

Create `tools/status-md-generator.test.mjs`:

```javascript
import { describe, it, expect } from 'vitest';
import { renderStatus } from './status-md-generator.mjs';

describe('renderStatus', () => {
  it('renders all 4 controllers + metrics', () => {
    const inputs = {
      now: '2026-05-19T10:00:00+03:00',
      c1: { status: 'ok', detail: 'no drift' },
      c2: { status: 'ok', detail: '0 version drift' },
      c3: { status: 'ok', detail: 'last read today, 54w remaining' },
      observer: { episodeCount: 12, piiMatches: 0 },
    };
    const md = renderStatus(inputs);
    expect(md).toContain('# Brain Status');
    expect(md).toContain('| C1 L1-watcher | ✅');
    expect(md).toContain('| C2 Cross-ref consistency | ✅');
    expect(md).toContain('| C3 Observer-of-observer | ✅');
    expect(md).toContain('| C4 Сигнальный статус | ✅');
    expect(md).toContain('12 episodes');
  });

  it('shows red status for failing controllers', () => {
    const inputs = {
      now: '2026-05-19T10:00:00+03:00',
      c1: { status: 'fail', detail: '2 plugins not formalized' },
      c2: { status: 'ok', detail: '' },
      c3: { status: 'ok', detail: '' },
      observer: { episodeCount: 0, piiMatches: 0 },
    };
    expect(renderStatus(inputs)).toContain('| C1 L1-watcher | 🔴');
  });

  it('mentions capability-readiness behavioral rule', () => {
    const inputs = {
      now: '2026-05-19T10:00:00+03:00',
      c1: { status: 'ok' }, c2: { status: 'ok' }, c3: { status: 'ok' },
      observer: { episodeCount: 0, piiMatches: 0 },
    };
    const md = renderStatus(inputs);
    expect(md).toContain('capability-readiness');
    expect(md).toContain('feedback_brain_unused_tools_not_problem');
  });
});
```

- [ ] **Step 2: Run test — verify failure**

```bash
npx vitest run tools/status-md-generator.test.mjs
```

- [ ] **Step 3: Implement (using execFileSync per Security Guidance #40)**

Create `tools/status-md-generator.mjs`:

```javascript
#!/usr/bin/env node
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { join } from 'path';
import { execFileSync } from 'child_process';

function iconFor(status) {
  return { ok: '✅', warn: '⚠️', fail: '🔴' }[status] || '⚪';
}

export function renderStatus(inputs) {
  const { now, c1, c2, c3, observer } = inputs;
  return `# Brain Status (auto-generated)

Last updated: ${now}

| Контролёр | Состояние | Детали |
|---|---|---|
| C1 L1-watcher | ${iconFor(c1.status)} | ${c1.detail || '—'} |
| C2 Cross-ref consistency | ${iconFor(c2.status)} | ${c2.detail || '—'} |
| C3 Observer-of-observer | ${iconFor(c3.status)} | ${c3.detail || '—'} |
| C4 Сигнальный статус | ✅ | This file (self-reference) |

## Метрики (информационные, не алерты)

- Observer evidence: ${observer.episodeCount} episodes this month, ${observer.piiMatches} PII matches before filter
- Использование узлов: см. \`/brain-retro\` (раз в спринт). **Неиспользованные узлы — не проблема** (capability-readiness; см. [feedback_brain_unused_tools_not_problem](../../memory/feedback_brain_unused_tools_not_problem.md)).

## Алерт-индикаторы

✅ — норма ・ ⚠️ — внимание ・ 🔴 — действие требуется ・ ⚪ — не запускалось
`;
}

function runControllerNode(scriptArgs) {
  try {
    const out = execFileSync('node', scriptArgs, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
    return { status: 'ok', detail: out.trim().split('\n').pop() };
  } catch (err) {
    return { status: 'fail', detail: (err.stderr || err.message || '').trim().split('\n').pop() };
  }
}

function countEpisodes() {
  const dir = 'docs/observer';
  if (!existsSync(dir)) return 0;
  const month = new Date().toISOString().slice(0, 7);
  const file = join(dir, `episodes-${month}.jsonl`);
  if (!existsSync(file)) return 0;
  return readFileSync(file, 'utf-8').trim().split('\n').filter(Boolean).length;
}

if (import.meta.url === `file://${process.argv[1].replace(/\\/g, '/')}`) {
  const inputs = {
    now: new Date().toISOString(),
    c1: runControllerNode(['tools/l1-watcher.mjs']),
    c2: runControllerNode(['tools/cross-ref-checker.mjs']),
    c3: runControllerNode(['tools/observer-of-observer.mjs', 'check']),
    observer: { episodeCount: countEpisodes(), piiMatches: 0 },
  };
  const md = renderStatus(inputs);
  writeFileSync('docs/observer/STATUS.md', md);
  console.log(`[status-md-generator] OK — wrote docs/observer/STATUS.md`);
}
```

NB: `execFileSync('node', [scriptPath, ...args])` is safe — no shell, no injection (Security Guidance #40 compliant).

- [ ] **Step 4: Run test — verify PASS**

```bash
npx vitest run tools/status-md-generator.test.mjs
```

Expected: 3/3 pass.

- [ ] **Step 5: Smoke run**

```bash
node tools/status-md-generator.mjs
cat docs/observer/STATUS.md
```

Expected: STATUS.md regenerated.

- [ ] **Step 6: Commit**

```bash
git add tools/status-md-generator.mjs tools/status-md-generator.test.mjs docs/observer/STATUS.md
git commit -m "feat(controller): C4 status-md-generator — dashboard

Aggregates C1/C2/C3 outputs (via execFileSync per SG #40) + observer
episode count. Behavioral rule embedded in metric copy. Per spec §6.4.

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

---

### Task C5: lefthook wire-up

**Files:**

- Modify: `lefthook.yml`

- [ ] **Step 1: Read current lefthook.yml structure**

```bash
cat lefthook.yml
```

Identify the pre-commit and post-commit sections.

- [ ] **Step 2: Add 3 new pre-commit + 1 post-commit jobs**

Edit `lefthook.yml` — append to `pre-commit.commands`:

```yaml
  l1-watcher:
    glob: '{.claude/settings.json,docs/Tooling_v8_3.md}'
    run: node tools/l1-watcher.mjs

  cross-ref-checker:
    glob: '{docs/Pravila_raboty_Claude_v1_1.md,docs/Tooling_v8_3.md,docs/Plugin_stack_rules_v1.md,CLAUDE.md,MEMORY.md}'
    run: node tools/cross-ref-checker.mjs

  observer-of-observer:
    run: node tools/observer-of-observer.mjs check
```

Append to `post-commit.commands` (create section if missing):

```yaml
post-commit:
  parallel: false
  commands:
    status-md:
      run: node tools/status-md-generator.mjs && git add docs/observer/STATUS.md
```

NOTE: `git add` in post-commit stages STATUS.md for next commit — intentional. Owner reviews STATUS.md before persisting.

- [ ] **Step 3: Verify lefthook syntax**

```bash
npx lefthook validate
```

Expected: OK.

- [ ] **Step 4: Trigger pre-commit on a no-op staged change**

```bash
touch docs/observer/STATUS.md
git add docs/observer/STATUS.md
npx lefthook run pre-commit
git restore --staged docs/observer/STATUS.md
git checkout docs/observer/STATUS.md
```

Expected: all jobs pass.

- [ ] **Step 5: Commit lefthook.yml**

```bash
git add lefthook.yml
git commit -m "build(lefthook): wire 4 brain controllers — C1/C2/C3/C4

pre-commit: l1-watcher, cross-ref-checker, observer-of-observer.
post-commit: status-md generator. Per spec §6.

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

---

## Phase D — Finalization

### Task D1: CLAUDE.md cross-refs + §0 sync

**Files:**

- Modify: `CLAUDE.md` (§0 cross-refs + §3.X + §9 changelog)

- [ ] **Step 1: Pre-flight sync**

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

- [ ] **Step 2: Identify changes**

- §0 cross-refs row Pravila: `v1.30 → v1.31`
- §0 cross-refs row Tooling: `v2.16 → v2.17`
- §0 cross-refs row PSR_v1: `v3.15 → v3.16`
- Add new §3.X entry «Brain governance — router-procedure.md cross-ref».
- §9 — append v2.18 entry.

- [ ] **Step 3: Determine edit method**

Per §5 п.10: normal checkout — use `/claude-md-management:claude-md-improver` skill. Worktree — direct Edit (worktree-эксцепшн).

- [ ] **Step 4: Apply edits**

Either invoke `/claude-md-management:claude-md-improver` with the change-set OR direct Edit.

- [ ] **Step 5: Verify lefthook (C2 cross-ref-checker NOW catches drift)**

```bash
git add CLAUDE.md
npx lefthook run pre-commit
```

Expected: cross-ref-checker PASS.

- [ ] **Step 6: Commit**

```bash
git commit -m "docs(claude-md): §0 cross-refs sync — Pravila v1.31 / Tooling v2.17 / PSR_v1 v3.16

+§3.X cross-ref to docs/router-procedure.md (brain governance).
§9 v2.18 changelog entry.

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

---

### Task D2: Smoke test — end-to-end

**Files:**

- None modified; verification only

- [ ] **Step 1: Trigger a synthetic Stop-event**

```bash
echo '{"sessionId":"e2e-smoke","started":"2026-05-19T12:00:00+03:00","ended":"2026-05-19T12:05:00+03:00","result":"success","path_type":"regulated","events":[{"kind":"skill_invoked","skill_id":"writing-plans"}]}' | node tools/observer-stop-hook.mjs
```

- [ ] **Step 2: Verify JSONL line written**

```bash
tail -1 docs/observer/episodes-2026-05.jsonl
```

Expected: JSON with task_id=`e2e-smoke`, 4 mandatory fields.

- [ ] **Step 3: Trigger STATUS.md regeneration**

```bash
node tools/status-md-generator.mjs
cat docs/observer/STATUS.md
```

Expected: STATUS.md shows ≥1 episode this month, all 4 controllers ✅.

- [ ] **Step 4: Trigger each controller manually**

```bash
node tools/l1-watcher.mjs
node tools/cross-ref-checker.mjs
node tools/observer-of-observer.mjs check
```

Expected: each prints OK.

- [ ] **Step 5: Deliberate red-path smoke (each controller separately, revert after)**

For each controller — introduce a synthetic bad input on a temp branch, verify red-path fires, revert.

- [ ] **Step 6: Clean smoke artefacts**

```bash
git checkout docs/observer/episodes-2026-05.jsonl docs/observer/.read-counter.json docs/observer/STATUS.md
```

- [ ] **Step 7: No commit** — smoke is verification, not artefact.

---

### Task D3: verification-before-completion

**Files:**

- None modified; verification only

- [ ] **Step 1: Invoke superpowers:verification-before-completion skill**

Use Skill tool with `skill=superpowers:verification-before-completion`.

- [ ] **Step 2: Walk through spec §10 — 13 verification criteria**

For each of 13 criteria in spec §10:

1. `docs/router-procedure.md` v1.0 exists → `head -5 docs/router-procedure.md`
2. Tooling Прил. Н §4.X — 9 attributes present (5-row sample) → grep on `### §4.5`, `§4.20`, `§4.40`, `§4.55`, `§4.60`
3. `docs/observer/` Stop-hook smoke → verified D2 step 2
4. C1 lefthook fail/pass paths → verified D2 step 5
5. C2 lefthook fail/pass → verified
6. C3 counter + warn → verified
7. C4 STATUS.md exists with 4 controllers → verified
8. `/brain-retro` smoke → invoke skill, check it reports "no data" or aggregates current month
9. ADR-011 status=Accepted → `grep "status: Accepted" docs/adr/ADR-011-brain-governance.md`
10. §15.2 pre-flight sync done before each normative edit → check commit ordering
11. HK1 pre-check done before Stop-hook → README note (Task B4)
12. Behavioral rule mentioned in ADR-011, STATUS.md, /brain-retro → grep each
13. Regression: Pest, Vitest, Larastan, lefthook — 0 regressions

- [ ] **Step 3: Full regression**

```bash
cd app && composer test && cd ..
npx vitest run
cd app && composer stan && cd ..
npx lefthook run pre-commit
```

Show full output (per economy 0% — no summary).

If ANY regression — STOP, fix, re-run.

- [ ] **Step 4: Report verification results**

Produce text block summarizing 13 criteria + regression numbers, file:line pins.

- [ ] **Step 5: No commit** — verification is read-only.

---

### Task D4: Memory updates

**Files:**

- Update: `memory/project_brain_governance_design.md` (outside repo)
- Update: `MEMORY.md` index (outside repo)

- [ ] **Step 1: Re-read current memory files**

- [ ] **Step 2: Update `project_brain_governance_design.md` description**: change «готов к написанию spec» → «реализован» + date. Add post-implementation summary section.

- [ ] **Step 3: Update `MEMORY.md` index entry** (one line, ≤200 chars).

- [ ] **Step 4: Memory files are outside the git repo** — no commit needed.

---

### Task D5: Final pre-push verification + push approval

**Files:**

- None

- [ ] **Step 1: Verify clean state**

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

Expected: list of atomic commits for this plan.

- [ ] **Step 2: Run pre-push gate locally**

```bash
npx lefthook run pre-push
```

Expected: gitleaks-full-history + lychee + larastan + pint + pest — all green.

- [ ] **Step 3: Ask user for push approval**

DO NOT push without explicit user instruction. Ask:

> «Phase A/B/C/D готовы (N commits ahead of origin/main). Pre-push gate зелёный. Пушим в origin/main или сначала PR? Стандартный паттерн репо: `git push origin <branch>:main` direct FF (см. memory `reference_github`).»

- [ ] **Step 4: Execute approved action**

If user approves direct push: `git push origin <branch>:main`.
If user wants PR: `gh pr create ...`.

- [ ] **Step 5: Verify post-push state**

```bash
git fetch origin
git log origin/main --oneline | head -5
```

---

## Self-Review

**Spec coverage:**

| Spec section | Plan task | Status |
|---|---|---|
| §4 Router | A1, A2, A3 | ✅ |
| §5 Observer scope B | B1, B2, B3, B5, B6 | ✅ |
| §5.2.1 routing_decision + primary_rationale (v1.1 amendment) | B3 (validation + buildEpisodeFromContext extraction); ADR-011 Decision 2 (A1) | ✅ |
| §5.4 PII filter | B2 | ✅ |
| §5.5 /brain-retro (incl. factor matrix) | B6 (aggregation-template Factor analysis matrix) | ✅ |
| §6.1 C1 L1-watcher | C1 + C5 | ✅ |
| §6.2 C2 cross-ref | C2 + C5 | ✅ |
| §6.3 C3 observer-of-observer 54 weeks | C3 + C5 | ✅ |
| §6.4 C4 STATUS.md | C4 + C5 | ✅ |
| §7 Behavioral rule | A1 (ADR), B6 (skill), C4 (STATUS copy) | ✅ |
| §8.1 HK1 pre-check | B4 | ✅ |
| §8.2 §15.2 pre-flight | A1/A2/A3/A4/A5/D1 step 1 each | ✅ |
| §8.3 Worktree exception | D1 step 3 conditional | ✅ |
| §8.5 §15 parallel-sessions integration | A4 §16, A5 R16.3 | ✅ |
| §10 13 verification criteria | D3 step 2 | ✅ |
| §11 Open questions O1–O7 | resolved inline: O1 Node, O2 regex inline, O3 manual, O4 GH Actions, O5 post-commit, O6 docs/, O7 ADR-011 | ✅ |

**Placeholder scan:** No TBD/TODO/«implement later» found in code blocks. The intentional `piiMatches: 0` in `status-md-generator.mjs` is the **default current value**, not a placeholder — PII match counting is an optional future enhancement that requires instrumenting Stop-hook to report counts; for now the field is shown as 0 informationally (this is documented behaviour, not a missing feature).

**Type consistency:**

- `sanitize` (B2) used in `appendEpisode` (B3) — consistent.
- `detectDrift` (C1) — input/output shape consistent.
- `extractVersion` / `extractCrossRefs` / `detectMismatches` (C2) — consistent.
- `weeksSince` / `isStale` / `recordRead` (C3) — consistent.
- `renderStatus` / `iconFor` / `runControllerNode` / `countEpisodes` (C4) — consistent.
- `appendEpisode` / `buildEpisodeFromContext` (B3) — consistent.

**Security Guidance #40 compliance:** All subprocess calls use `execFileSync('node', [path, ...args])`. No `exec`, no `execSync`, no string interpolation into commands.

---

## Execution Handoff

**Plan complete and saved to `docs/superpowers/plans/2026-05-19-brain-governance.md`. Two execution options:**

**1. Subagent-Driven (recommended)** — I dispatch a fresh subagent per task, review between tasks, fast iteration. Per Pravila §15.1: subagent + git tasks use Sonnet/Opus (never Haiku). `subagent-driven-development` wrapper-скил с git-safety-checklist.

**2. Inline Execution** — Execute tasks in this session using executing-plans, batch execution with checkpoints.

Какой подход?
