diff --git a/docs/superpowers/plans/2026-05-19-brain-governance.md b/docs/superpowers/plans/2026-05-19-brain-governance.md new file mode 100644 index 00000000..8cb5cac1 --- /dev/null +++ b/docs/superpowers/plans/2026-05-19-brain-governance.md @@ -0,0 +1,2340 @@ +# 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. + +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) " +``` + +--- + +### 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) " +``` + +--- + +### 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 + +**Атрибуты:** + +| id | name | kind | phase | subcategory | triggers | boundaries | dormant | last-touched | +|---|---|---|---|---|---|---|---|---| +| #NN | | | | | «» | | false | 2026-05-19 | + + +` ` ` +``` + +(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. (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) " +``` + +--- + +### 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-.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) " +``` + +--- + +### 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) " +``` + +--- + +## 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-.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) " +``` + +--- + +### 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) " +``` + +--- + +### 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 }); +}); + +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', + }; + 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.endsWith('\n')).toBe(true); + }); + + it('appends to existing file without overwrite', () => { + appendEpisode({ task_id: 'a', timestamps: {}, path_type: 'regulated', outcome: 'success' }, workdir, '2026-05'); + appendEpisode({ task_id: 'b', timestamps: {}, path_type: 'improvised', outcome: 'partial' }, 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', () => { + appendEpisode({ + task_id: 'c', + timestamps: {}, + path_type: 'regulated', + outcome: 'success', + 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 fields', () => { + expect(() => appendEpisode({}, workdir, '2026-05')).toThrow(/required/i); + expect(() => appendEpisode({ task_id: 'x' }, workdir, '2026-05')).toThrow(/required/i); + }); +}); + +describe('buildEpisodeFromContext', () => { + it('extracts 4 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); + }); +}); +``` + +- [ ] **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']; + +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}`); + } + 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', + 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) " +``` + +--- + +### 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: `. + +- [ ] **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) " +``` + +--- + +### 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 | +|---|---|---| + +## 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: + +- **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.4 PII filter | B2 | ✅ | +| §5.5 /brain-retro | B6 | ✅ | +| §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. + +Какой подход?