From 6a3c4741d6bebf669efe94df56d3a8cc71ce86ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Thu, 21 May 2026 08:24:38 +0300 Subject: [PATCH] =?UTF-8?q?docs(superpowers):=20plan=20=E2=80=94=20observe?= =?UTF-8?q?r=20missed=20activations=20(Pravila=20=C2=A716.4=20v1.36)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementation plan for conditional missed-activation detection. Architecture: hybrid mapping (manual classification map + auto-extracted dormancy from Tooling). 12 tasks, TDD-driven. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-05-21-observer-missed-activations.md | 1021 +++++++++++++++++ 1 file changed, 1021 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-21-observer-missed-activations.md diff --git a/docs/superpowers/plans/2026-05-21-observer-missed-activations.md b/docs/superpowers/plans/2026-05-21-observer-missed-activations.md new file mode 100644 index 00000000..3ed0643e --- /dev/null +++ b/docs/superpowers/plans/2026-05-21-observer-missed-activations.md @@ -0,0 +1,1021 @@ +# Observer Missed Activations — 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:** Расширить правило Pravila §16.4 «не использован ≠ проблема» симметрией missed activation: эпизоды с профильной классификацией без активации релевантного узла — показывать в STATUS.md (C5) и `/brain-retro` как сигнал (не блок). DEFERRED-узлы исключены. + +**Architecture:** Hybrid mapping. Manual JSON `tools/observer-classification-map.json` декларирует `classification → recommended_node_ids[]` (single source, ~30 строк, явная семантика). Auto-extracted JSON `tools/.node-dormancy.json` фиксирует `dormant: true/false` из Tooling Прил.Н §3.5/§4.X через pre-commit генератор (прецедент `.l1-watcher-aliases.txt`). Matcher в `brain-retro-analyzer.mjs` детерминирован, чистый: эпизод matches профильную классификацию + `node_chosen === 'direct'` + хотя бы один non-dormant релевантный узел → counted as missed. Порог — одиночный эпизод (§16.4 condition). STATUS.md C5 показывает `missed_activations: N`, ⚠️ при N>0. /brain-retro выводит per-node breakdown. + +**Tech Stack:** Node.js ESM, Vitest, lefthook, Markdown (Pravila/ADR/skills/CLAUDE.md). + +**Branch:** `feat/observer-missed-activations` (worktree). + +**Pre-flight gate (§15.2):** `git fetch origin && git log HEAD..origin/main --oneline` — нормативные файлы (Pravila/CLAUDE.md/Tooling/PSR_v1/MEMORY.md/Открытые_вопросы/docs/adr/*/db/schema.sql) должны быть синхронны с origin/main перед стартом и перед каждой нормативной правкой (Task 10-14). + +--- + +## File Structure + +**New files:** + +- `tools/observer-classification-map.json` — manual mapping classification → node_ids +- `tools/extract-node-dormancy.mjs` — Tooling §3.5/§4.X parser → `.node-dormancy.json` +- `tools/extract-node-dormancy.test.mjs` — Vitest cases +- `tools/.node-dormancy.json` — generated artifact (committed, like `.l1-watcher-aliases.txt`) +- `tools/missed-activations.mjs` — pure matcher (`detectMissedActivations`) +- `tools/missed-activations.test.mjs` — Vitest cases +- `docs/adr/ADR-011-brain-governance.md` — amendment section (existing file edit) + +**Modified files:** + +- `tools/brain-retro-analyzer.mjs` — import matcher, inject into `analyze()` output +- `tools/brain-retro-analyzer.test.mjs` — extend coverage for `missedActivations` field +- `tools/observer-coverage-checker.mjs` — surface missed count to STATUS.md +- `tools/observer-coverage-checker.test.mjs` — coverage for new surfacing +- `tools/status-md-generator.mjs` — render `missed_activations` in C5 detail + ⚠️ +- `tools/status-md-generator.test.mjs` — new render tests +- `lefthook.yml` — new pre-commit job `extract-node-dormancy` +- `docs/Pravila_raboty_Claude_v1_1.md` §16.4 + version bump v1.35→v1.36 +- `.claude/skills/brain-retro/SKILL.md` — behavior reminder update +- `.claude/skills/brain-retro/references/aggregation-template.md` — Missed Activations section +- `CLAUDE.md` §0 cross-ref + §9 entry (via `/claude-md-management:claude-md-improver`) + +**Outside-repo memory:** + +- `memory/feedback_brain_unused_tools_not_problem.md` — refresh under new conditional rule + +--- + +## Task 1: Classification → Node Mapping (data) + +**Files:** + +- Create: `tools/observer-classification-map.json` + +- [ ] **Step 1: Write the mapping file** + +```json +{ + "$schema_version": 1, + "description": "Mapping from observer transcript-parser task_classification values to recommended Tooling Прил.Н node IDs. Source of truth for missed-activation detection (Pravila §16.4 conditional rule). 'other' deliberately empty — no recommendation, never counts as missed. DEFERRED-узлы filtered out by .node-dormancy.json at runtime.", + "map": { + "refactor": ["#11", "#12", "#43", "#64", "#65"], + "bugfix": ["#18", "#34"], + "feature": ["#19"], + "planning": ["#19", "#41", "#42"], + "memory-sync": ["#33"], + "monitoring": ["#34", "#35"], + "analysis": ["#25", "#39", "#53"], + "cleanup": ["#11", "#12"], + "question": ["#60"], + "other": [] + } +} +``` + +Rationale per bucket (for reviewers, not in JSON): + +- `refactor` — Pint (style) + Larastan (types) + deptrac (layer-fitness) + Rector (auto-refactor) + PHP Insights (complexity) +- `bugfix` — Pest (test-driven), Sentry MCP (production runtime errors) +- `feature` — Superpowers brainstorming (entry-point for creative work via §12) +- `planning` — Superpowers writing-plans (§12 hard-rule) + CCPM + product-management +- `memory-sync` — claude-md-management (§5 п.10 mandatory channel) +- `monitoring` — Sentry MCP + Redis MCP (debug-runtime category) +- `analysis` — Semgrep + Trail of Bits + process-analysis +- `cleanup` — Pint (format) + Larastan (find dead types) +- `question` — context7 (library docs first choice) +- `other` — empty (genuinely unclassifiable prompts; never counts as miss) + +- [ ] **Step 2: Validate JSON syntax** + +Run: `node -e "JSON.parse(require('fs').readFileSync('tools/observer-classification-map.json', 'utf-8'))"` +Expected: no output, exit 0. + +- [ ] **Step 3: Commit** + +```bash +git add tools/observer-classification-map.json +git commit -m "feat(observer): add classification→node mapping for missed-activation detection" +``` + +--- + +## Task 2: Tooling Dormancy Extractor (data) + +**Files:** + +- Create: `tools/extract-node-dormancy.mjs` +- Test: `tools/extract-node-dormancy.test.mjs` + +- [ ] **Step 1: Write the failing test** + +```javascript +// tools/extract-node-dormancy.test.mjs +import { describe, it, expect } from 'vitest'; +import { extractDormancy } from './extract-node-dormancy.mjs'; + +describe('extractDormancy', () => { + it('parses 9-attribute row with dormant=false', () => { + const md = [ + '#### #10 Laravel Boost', + '', + '**Атрибуты:**', + '', + '| id | name | kind | phase | subcategory | triggers | boundaries | dormant | last-touched |', + '|---|---|---|---|---|---|---|---|---|', + '| #10 | Laravel Boost | composer-dep | 1 | — | «SQL, Eloquent» | replaces #1 PG MCP | false | 2026-05-19 |', + ].join('\n'); + expect(extractDormancy(md)).toEqual({ '#10': false }); + }); + + it('parses dormant=true (DEFERRED node)', () => { + const md = '| #17 | pg_partman | binary-dep | 1 | — | «partition mgmt» | none | true | 2026-05-19 |'; + expect(extractDormancy(md)).toEqual({ '#17': true }); + }); + + it('handles multiple nodes in one pass', () => { + const md = [ + '| #44 | Figma MCP | mcp-server | off-phase | design-tooling | «figma extract» | extract-only | true | 2026-05-17 |', + '| #45 | Universal Icons MCP | mcp-server | off-phase | design-tooling | «svg search» | non-Lucide | false | 2026-05-17 |', + ].join('\n'); + expect(extractDormancy(md)).toEqual({ '#44': true, '#45': false }); + }); + + it('ignores header/separator rows', () => { + const md = [ + '| id | name | kind | phase | subcategory | triggers | boundaries | dormant | last-touched |', + '|---|---|---|---|---|---|---|---|---|', + ].join('\n'); + expect(extractDormancy(md)).toEqual({}); + }); + + it('ignores non-numeric ids (template placeholders)', () => { + const md = '| #NN | | | | | «» | | false | 2026-05-19 |'; + expect(extractDormancy(md)).toEqual({}); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd app && npx vitest run ../tools/extract-node-dormancy.test.mjs` +Expected: FAIL with "Cannot find module './extract-node-dormancy.mjs'" + +- [ ] **Step 3: Write minimal implementation** + +```javascript +// tools/extract-node-dormancy.mjs +#!/usr/bin/env node +/** + * Tooling Прил.Н dormancy extractor — emits {id: dormant_bool} JSON for + * the missed-activation matcher (Pravila §16.4 conditional rule). + * Parses 9-attribute table rows; ignores headers/separators/templates. + * + * Security Guidance #40: pure parsing — no exec/execSync. + */ +import { readFileSync, writeFileSync } from 'fs'; + +const ROW_RE = /^\|\s*#(\d+)\s*\|[^|]+\|[^|]+\|[^|]+\|[^|]+\|[^|]+\|[^|]+\|\s*(true|false)\s*\|[^|]+\|$/gm; + +export function extractDormancy(md) { + const out = {}; + for (const m of md.matchAll(ROW_RE)) { + out[`#${m[1]}`] = m[2] === 'true'; + } + return out; +} + +if (process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/extract-node-dormancy.mjs')) { + const src = readFileSync('docs/Tooling_v8_3.md', 'utf-8'); + const dormancy = extractDormancy(src); + writeFileSync('tools/.node-dormancy.json', JSON.stringify(dormancy, null, 2) + '\n'); + console.log(`[extract-node-dormancy] OK — ${Object.keys(dormancy).length} nodes`); +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cd app && npx vitest run ../tools/extract-node-dormancy.test.mjs` +Expected: PASS 5/5. + +- [ ] **Step 5: Generate the artifact and verify shape** + +Run: `node tools/extract-node-dormancy.mjs` +Expected stdout: `[extract-node-dormancy] OK — N nodes` (N ≥ 60). +Verify: `tools/.node-dormancy.json` contains `"#17": true`, `"#44": true`, `"#50": true`, `"#54": true`, `"#67": true` and `"#10": false`. + +- [ ] **Step 6: Commit** + +```bash +git add tools/extract-node-dormancy.mjs tools/extract-node-dormancy.test.mjs tools/.node-dormancy.json +git commit -m "feat(observer): node-dormancy extractor + initial JSON snapshot" +``` + +--- + +## Task 3: Lefthook Job for Dormancy Regen + +**Files:** + +- Modify: `lefthook.yml` + +- [ ] **Step 1: Find the existing pre-commit jobs section** + +Run: `grep -n "extract-node-triggers\|pre-commit\|jobs:" lefthook.yml | head -20` + +- [ ] **Step 2: Add a new job after the L1-watcher / chain-map-checker block** + +```yaml + 17_extract_node_dormancy: + glob: "docs/Tooling_v8_3.md" + run: node tools/extract-node-dormancy.mjs && git add tools/.node-dormancy.json +``` + +(Job number 17 follows the existing 1-16 sequence. Confirm exact insertion point by reading `lefthook.yml` at the pre-commit section first.) + +- [ ] **Step 3: Smoke-test the hook** + +Run: `npx lefthook run pre-commit --files docs/Tooling_v8_3.md` +Expected: `17_extract_node_dormancy` runs, `tools/.node-dormancy.json` regenerated (no diff vs. Task 2 output). + +- [ ] **Step 4: Commit** + +```bash +git add lefthook.yml +git commit -m "build(lefthook): job 17 extract-node-dormancy on Tooling changes" +``` + +--- + +## Task 4: Missed-Activation Matcher (pure logic) + +**Files:** + +- Create: `tools/missed-activations.mjs` +- Test: `tools/missed-activations.test.mjs` + +- [ ] **Step 1: Write the failing test** + +```javascript +// tools/missed-activations.test.mjs +import { describe, it, expect } from 'vitest'; +import { detectMissedActivations } from './missed-activations.mjs'; + +const map = { + refactor: ['#11', '#12', '#43'], + bugfix: ['#18', '#34'], + feature: ['#19'], + other: [], +}; +const dormancy = { '#11': false, '#12': false, '#43': false, '#18': false, '#34': false, '#19': false }; + +function ep(classification, node_chosen) { + return { + schema_version: 2, + primary_rationale: { task_classification: classification, node_chosen }, + }; +} + +describe('detectMissedActivations', () => { + it('counts an episode with profile classification + node_chosen=direct as missed', () => { + const result = detectMissedActivations([ep('refactor', 'direct')], map, dormancy); + expect(result.totalMissed).toBe(1); + expect(result.byNode).toEqual({ '#11': 1, '#12': 1, '#43': 1 }); + }); + + it('does NOT count episode when the recommended node IS chosen', () => { + const result = detectMissedActivations([ep('refactor', '#11')], map, dormancy); + expect(result.totalMissed).toBe(0); + }); + + it('does NOT count episode when classification=other (empty list)', () => { + const result = detectMissedActivations([ep('other', 'direct')], map, dormancy); + expect(result.totalMissed).toBe(0); + }); + + it('excludes dormant (DEFERRED) nodes from recommendations', () => { + const dorm = { ...dormancy, '#43': true }; + const result = detectMissedActivations([ep('refactor', 'direct')], map, dorm); + expect(result.byNode).toEqual({ '#11': 1, '#12': 1 }); + expect(result.totalMissed).toBe(1); + }); + + it('returns totalMissed=0 when ALL recommended nodes are dormant', () => { + const dorm = { '#11': true, '#12': true, '#43': true }; + const result = detectMissedActivations([ep('refactor', 'direct')], map, dorm); + expect(result.totalMissed).toBe(0); + expect(result.byNode).toEqual({}); + }); + + it('ignores schema v1 episodes (no factor analysis)', () => { + const v1 = { schema_version: 1, primary_rationale: { task_classification: 'refactor', node_chosen: 'direct' } }; + const result = detectMissedActivations([v1], map, dormancy); + expect(result.totalMissed).toBe(0); + }); + + it('ignores observer_error markers', () => { + const err = { observer_error: true }; + const result = detectMissedActivations([err], map, dormancy); + expect(result.totalMissed).toBe(0); + }); + + it('ignores unknown classification (not in map)', () => { + const result = detectMissedActivations([ep('unknown-bucket', 'direct')], map, dormancy); + expect(result.totalMissed).toBe(0); + }); + + it('aggregates byClassification breakdown for the report', () => { + const eps = [ + ep('refactor', 'direct'), + ep('refactor', 'direct'), + ep('bugfix', 'direct'), + ]; + const result = detectMissedActivations(eps, map, dormancy); + expect(result.byClassification).toEqual({ refactor: 2, bugfix: 1 }); + expect(result.totalMissed).toBe(3); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd app && npx vitest run ../tools/missed-activations.test.mjs` +Expected: FAIL with "Cannot find module './missed-activations.mjs'". + +- [ ] **Step 3: Write minimal implementation** + +```javascript +// tools/missed-activations.mjs +#!/usr/bin/env node +/** + * Missed-activation matcher (Pravila §16.4 conditional rule). + * Pure deterministic — read-only, no exec, no fs. + * + * An episode is "missed" iff: + * 1. schema_version === 2 (v1 lacks factor data) + * 2. NOT observer_error + * 3. primary_rationale.task_classification ∈ map AND map[c].length > 0 + * 4. primary_rationale.node_chosen === 'direct' (no explicit node) + * 5. AT LEAST ONE recommended node is non-dormant + * + * Threshold: single episode (per Pravila §16.4 v1.36). + * DEFERRED-узлы filtered via dormancy registry. + */ + +export function detectMissedActivations(episodes, classificationMap, dormancy) { + const byNode = {}; + const byClassification = {}; + let totalMissed = 0; + + for (const e of episodes) { + if (!e || e.observer_error) continue; + if (e.schema_version !== 2) continue; + const pr = e.primary_rationale || {}; + const cls = pr.task_classification; + const chosen = pr.node_chosen; + if (!cls || chosen !== 'direct') continue; + + const recommended = classificationMap[cls]; + if (!Array.isArray(recommended) || recommended.length === 0) continue; + + const live = recommended.filter((id) => dormancy[id] === false); + if (live.length === 0) continue; + + totalMissed += 1; + byClassification[cls] = (byClassification[cls] || 0) + 1; + for (const id of live) { + byNode[id] = (byNode[id] || 0) + 1; + } + } + + return { totalMissed, byNode, byClassification }; +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cd app && npx vitest run ../tools/missed-activations.test.mjs` +Expected: PASS 9/9. + +- [ ] **Step 5: Commit** + +```bash +git add tools/missed-activations.mjs tools/missed-activations.test.mjs +git commit -m "feat(observer): missed-activation matcher (pure, deterministic)" +``` + +--- + +## Task 5: Integrate Matcher into brain-retro-analyzer + +**Files:** + +- Modify: `tools/brain-retro-analyzer.mjs:196-216` (the `analyze()` function and CLI block) +- Modify: `tools/brain-retro-analyzer.test.mjs` (extend existing suite) + +- [ ] **Step 1: Write the failing test (append to existing file)** + +```javascript +// tools/brain-retro-analyzer.test.mjs — new describe block at the end +describe('analyze() — missedActivations integration', () => { + it('includes missedActivations in the result', () => { + const eps = [ + { + schema_version: 2, + task_id: 't1', + timestamps: { started_at: '2026-05-21T00:00:00Z' }, + primary_rationale: { node_chosen: 'direct', task_classification: 'refactor' }, + events: [], + }, + ]; + const map = { refactor: ['#11'], other: [] }; + const dormancy = { '#11': false }; + const result = analyze(eps, { classificationMap: map, dormancy }); + expect(result.missedActivations).toBeDefined(); + expect(result.missedActivations.totalMissed).toBe(1); + expect(result.missedActivations.byNode).toEqual({ '#11': 1 }); + }); + + it('returns missedActivations.totalMissed=0 when no map/dormancy provided', () => { + const eps = [{ schema_version: 2, task_id: 't1', timestamps: { started_at: 'x' }, primary_rationale: { node_chosen: 'direct', task_classification: 'refactor' }, events: [] }]; + const result = analyze(eps); + expect(result.missedActivations.totalMissed).toBe(0); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd app && npx vitest run ../tools/brain-retro-analyzer.test.mjs -t "missedActivations integration"` +Expected: FAIL (result.missedActivations undefined). + +- [ ] **Step 3: Implement — extend imports and analyze()** + +Edit `tools/brain-retro-analyzer.mjs`: + +After the existing `import { readFileSync, existsSync } from 'fs';` line, add: + +```javascript +import { detectMissedActivations } from './missed-activations.mjs'; +``` + +Replace the `analyze` function signature and return block (lines 196-216) with: + +```javascript +/** Full deterministic aggregation: dedup → infer outcomes → group → chains → matrix → missed activations. */ +export function analyze(episodes, options = {}) { + const deduped = dedupeEpisodes(episodes); + const allNormal = deduped.filter((e) => !e.observer_error); + const normal = allNormal.filter((e) => e.schema_version === 2); + const v1SkippedCount = allNormal.length - normal.length; + for (const eps of bySessionSorted(normal).values()) { + eps.forEach((episode, i) => { + episode._inferredOutcome = inferOutcome(episode, eps[i + 1]); + }); + } + const classificationMap = options.classificationMap || {}; + const dormancy = options.dormancy || {}; + return { + episodeCount: normal.length, + v1SkippedCount, + observerErrorCount: deduped.length - allNormal.length, + tasks: groupEpisodesToTasks(normal), + causalChains: findCausalChains(normal), + factorMatrix: buildFactorMatrix(normal), + missedActivations: detectMissedActivations(normal, classificationMap, dormancy), + }; +} +``` + +In the CLI block at the bottom of the file (line 235), update to load both JSONs: + +```javascript +if (process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/brain-retro-analyzer.mjs')) { + const classificationMap = (() => { + try { + return JSON.parse(readFileSync('tools/observer-classification-map.json', 'utf-8')).map || {}; + } catch { return {}; } + })(); + const dormancy = (() => { + try { + return JSON.parse(readFileSync('tools/.node-dormancy.json', 'utf-8')); + } catch { return {}; } + })(); + const result = analyze(loadEpisodes(process.argv.slice(2)), { classificationMap, dormancy }); + console.log(JSON.stringify(result, null, 2)); + process.exit(0); +} +``` + +- [ ] **Step 4: Run tests to verify pass** + +Run: `cd app && npx vitest run ../tools/brain-retro-analyzer.test.mjs` +Expected: PASS for all existing + 2 new cases. + +- [ ] **Step 5: CLI smoke-test on real data** + +Run: `node tools/brain-retro-analyzer.mjs docs/observer/episodes-2026-05.jsonl | grep -A 10 missedActivations` +Expected: prints `"missedActivations": { "totalMissed": N, "byNode": {...}, "byClassification": {...} }` where N reflects May classifications. + +- [ ] **Step 6: Commit** + +```bash +git add tools/brain-retro-analyzer.mjs tools/brain-retro-analyzer.test.mjs +git commit -m "feat(observer): wire missed-activation matcher into analyze()" +``` + +--- + +## Task 6: Surface Missed Count to STATUS.md via C5 + +**Files:** + +- Modify: `tools/observer-coverage-checker.mjs:79-85` (`runCoverageChecker`) +- Modify: `tools/observer-coverage-checker.test.mjs` +- Modify: `tools/status-md-generator.mjs:107-130` (CLI block; render function unchanged in this task) + +- [ ] **Step 1: Extend observer-coverage-checker to compute missed count** + +Edit `tools/observer-coverage-checker.mjs`: + +Add imports at top: + +```javascript +import { detectMissedActivations } from './missed-activations.mjs'; +import { dedupeEpisodes } from './brain-retro-analyzer.mjs'; +``` + +After `countEpisodes`, add a helper: + +```javascript +function loadEpisodes(root) { + const month = new Date().toISOString().slice(0, 7); + const file = join(root, 'docs', 'observer', `episodes-${month}.jsonl`); + if (!existsSync(file)) return []; + const out = []; + for (const line of readFileSync(file, 'utf-8').split('\n')) { + const t = line.trim(); + if (!t) continue; + try { out.push(JSON.parse(t)); } catch { /* skip */ } + } + return out; +} + +function loadClassificationMap(root) { + try { + return JSON.parse(readFileSync(join(root, 'tools', 'observer-classification-map.json'), 'utf-8')).map || {}; + } catch { return {}; } +} + +function loadDormancy(root) { + try { + return JSON.parse(readFileSync(join(root, 'tools', '.node-dormancy.json'), 'utf-8')); + } catch { return {}; } +} +``` + +Replace `runCoverageChecker`: + +```javascript +export function runCoverageChecker(root = process.cwd()) { + const settings = readSettings(root); + const hookRegistered = isObserverStopRegistered(settings); + const coverage = checkCoverage(countEpisodes(root), hookRegistered); + const registration = checkRegistration(settings, existsSync(join(root, '.git', 'hooks', 'post-commit'))); + const episodes = loadEpisodes(root).filter((e) => e && e.schema_version === 2 && !e.observer_error); + const missed = detectMissedActivations( + dedupeEpisodes(episodes), + loadClassificationMap(root), + loadDormancy(root) + ); + return { coverage, registration, missed }; +} +``` + +- [ ] **Step 2: Update CLI block in observer-coverage-checker.mjs** + +```javascript +if (process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/observer-coverage-checker.mjs')) { + const { coverage, registration, missed } = runCoverageChecker(); + if (!coverage.ok) console.warn(`[observer-coverage-checker] WARN — coverage: ${coverage.detail}`); + if (!registration.ok) console.warn(`[observer-coverage-checker] WARN — registration: ${registration.detail}`); + if (missed.totalMissed > 0) { + console.warn(`[observer-coverage-checker] WARN — missed activations: ${missed.totalMissed} (see /brain-retro)`); + } + if (coverage.ok && registration.ok && missed.totalMissed === 0) { + console.log(`[observer-coverage-checker] OK — ${coverage.detail}; ${registration.detail}`); + } + process.exit(0); // warn-only — never blocks a commit +} +``` + +- [ ] **Step 3: Write failing test for observer-coverage-checker** + +```javascript +// tools/observer-coverage-checker.test.mjs — new describe block +import { describe, it, expect } from 'vitest'; +import { runCoverageChecker } from './observer-coverage-checker.mjs'; +// ... existing imports + +describe('runCoverageChecker — missed surfacing', () => { + it('returns a missed field with totalMissed', () => { + const { missed } = runCoverageChecker(); + expect(missed).toBeDefined(); + expect(typeof missed.totalMissed).toBe('number'); + }); +}); +``` + +- [ ] **Step 4: Run tests** + +Run: `cd app && npx vitest run ../tools/observer-coverage-checker.test.mjs` +Expected: PASS (existing + new). + +- [ ] **Step 5: Commit** + +```bash +git add tools/observer-coverage-checker.mjs tools/observer-coverage-checker.test.mjs +git commit -m "feat(observer): C5 surfaces missed-activation count via runCoverageChecker" +``` + +--- + +## Task 7: Render Missed Activations in STATUS.md + +**Files:** + +- Modify: `tools/status-md-generator.mjs:11-41` (`renderStatus`) + CLI block lines 107-130 +- Modify: `tools/status-md-generator.test.mjs` + +- [ ] **Step 1: Write failing test** + +```javascript +// tools/status-md-generator.test.mjs — new describe block +describe('renderStatus — missed activations', () => { + it('shows ✅ and missed=0 when there are no misses', () => { + const md = renderStatus(baseInputs({ missed: { totalMissed: 0, byNode: {}, byClassification: {} } })); + expect(md).toContain('| C5 Observer-coverage | ✅'); + expect(md).toContain('missed_activations: 0'); + }); + + it('shows ⚠️ and the count when misses occur', () => { + const md = renderStatus(baseInputs({ + missed: { totalMissed: 3, byNode: { '#11': 2, '#12': 1 }, byClassification: { refactor: 3 } }, + })); + expect(md).toContain('| C5 Observer-coverage | ⚠️'); + expect(md).toContain('missed_activations: 3'); + }); + + it('replaces the static «не проблема» line with the conditional rule', () => { + const md = renderStatus(baseInputs()); + expect(md).toContain('Неиспользованные узлы — не алерт'); + expect(md).toContain('если профильной задачи не было'); + expect(md).toContain('feedback_brain_unused_tools_not_problem'); + }); +}); +``` + +(Update existing `baseInputs()` helper at the top of the file to accept an optional `missed` override; if not provided, default to `{ totalMissed: 0, byNode: {}, byClassification: {} }`. Also update the existing `mentions the capability-readiness behavioral rule` test — assertion text now includes the conditional phrase.) + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd app && npx vitest run ../tools/status-md-generator.test.mjs` +Expected: FAIL on the new describe block. + +- [ ] **Step 3: Modify renderStatus** + +Edit `tools/status-md-generator.mjs`: + +Update `renderStatus` parameter destructuring (line 12): + +```javascript +const { now, c1, c2, c3, c5, observer, lastRetroDaysAgo } = inputs; +const missed = inputs.missed || { totalMissed: 0, byNode: {}, byClassification: {} }; +``` + +Update the C5 row icon logic — `c5.status` should already be `warn` when `missed.totalMissed > 0` (set in CLI block, Step 4 below). No row-template change needed except detail extension. + +Update the detail string for C5 in the CLI block (Step 4). + +Update the metrics paragraph (line 35): + +```javascript +- Использование узлов: см. \`/brain-retro\` (раз в спринт). missed_activations: ${missed.totalMissed}. **Неиспользованные узлы — не алерт, если профильной задачи не было** (Pravila §16.4 v1.36; см. memory \`feedback_brain_unused_tools_not_problem\` — outside-repo memory store). +``` + +- [ ] **Step 4: Update CLI block in status-md-generator.mjs** + +```javascript +if (process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/status-md-generator.mjs')) { + const cov = runCoverageChecker(); + const c5ok = cov.coverage.ok && cov.registration.ok && cov.missed.totalMissed === 0; + const c5detail = [ + cov.coverage.detail, + cov.registration.detail, + cov.missed.totalMissed > 0 ? `${cov.missed.totalMissed} missed activation(s) — see /brain-retro` : null, + ].filter(Boolean).join(' · '); + 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']), + c5: { status: c5ok ? 'ok' : 'warn', detail: c5detail }, + c6: runControllerNode(['tools/observer-chain-map-checker.mjs']), + observer: { + episodeCount: countEpisodes(), + observerErrors: countObserverErrors(), + piiMatches: countPiiMatches(), + v1Episodes: countV1Episodes(), + }, + missed: cov.missed, + lastRetroDaysAgo: lastRetroDaysAgo(), + }; + const md = renderStatus(inputs); + writeFileSync('docs/observer/STATUS.md', md); + console.log(`[status-md-generator] OK — wrote docs/observer/STATUS.md`); +} +``` + +- [ ] **Step 5: Run tests + CLI smoke** + +```bash +cd app && npx vitest run ../tools/status-md-generator.test.mjs && cd .. +node tools/status-md-generator.mjs +cat docs/observer/STATUS.md | head -40 +``` + +Expected: tests PASS; STATUS.md contains the new `missed_activations` line and the conditional phrasing. + +- [ ] **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(observer): render missed_activations in STATUS.md C5" +``` + +--- + +## Task 8: Pravila §16.4 Conditional Rule (v1.36) + +**Files:** + +- Modify: `docs/Pravila_raboty_Claude_v1_1.md:985-987` + version-bump header + +- [ ] **Step 1: Re-run pre-flight sync (§15.2)** + +```bash +git fetch origin +git log HEAD..origin/main --oneline -- docs/Pravila_raboty_Claude_v1_1.md +``` + +Expected: no commits ahead. If there are, halt and reconcile. + +- [ ] **Step 2: Replace §16.4 body** + +Replace lines 985-987 with: + +```markdown +### 16.4. Поведенческое правило «не использован ≠ проблема» (условное) + +Узел «мозга», не задействованный в реальной работе, **не** считается проблемой и **не** подлежит автоматической пометке **при условии, что профильной задачи для него в эпизодах не было**. Это — capability-readiness, осознанная стратегия заказчика. + +**Симметричное правило (missed activation):** если в эпизодах присутствует **хотя бы один** эпизод с `primary_rationale.task_classification`, соответствующим набору рекомендуемых узлов из `tools/observer-classification-map.json`, при этом `primary_rationale.node_chosen === 'direct'` и среди рекомендуемых узлов есть хотя бы один non-dormant (по `tools/.node-dormancy.json`, экстракт `dormant`-флага из [Tooling Прил.Н §3.5/§4.X](Tooling_v8_3.md)) — это **сигнал**, кандидат на разбор. Surface в STATUS.md (C5: `missed_activations: N`, ⚠️ при N>0) и в выводе `/brain-retro`. Не блок коммита, не auto-edit. + +**Исключения:** DEFERRED-узлы (на момент v1.36 — #17 pg_partman, #44 Figma MCP, #50 Jupyter MCP, #54 n8n-mcp, #67 NightOwl, признак `dormant: true` в Прил.Н) — для них «не активирован» = ожидаемое состояние, в missed activations не учитываются. + +См. `memory/feedback_brain_unused_tools_not_problem.md`. +``` + +- [ ] **Step 3: Bump version header** + +In the file header (top of Pravila): + +- Bump `v1.35` → `v1.36` +- Date → `21.05.2026` +- Headline note: `§16.4 расширен симметрией missed activation (условное правило); хранение mapping в tools/observer-classification-map.json + .node-dormancy.json` + +- [ ] **Step 4: Sync CLAUDE.md §0 cross-ref** + +Through `/claude-md-management:claude-md-improver` (CLAUDE.md §5 п.10) — update row `Pravila v1.35` → `v1.36` and §9 entry. (This is a separate session/skill invocation; for plan tracking treat it as part of this task.) + +- [ ] **Step 5: Commit** + +```bash +git add docs/Pravila_raboty_Claude_v1_1.md CLAUDE.md +git commit -m "docs(normative): Pravila §16.4 v1.36 — conditional missed-activation rule" +``` + +--- + +## Task 9: ADR-011 Amendment + +**Files:** + +- Modify: `docs/adr/ADR-011-brain-governance.md` + +- [ ] **Step 1: Append an Amendment section at the end of the ADR** + +```markdown +## Amendment 2026-05-21: Conditional missed-activation rule (§16.4 v1.36) + +The original §16.4 stated unconditionally that an unused node is not a problem. Real-world episodes show this is too permissive: when a profile-classified task (e.g. `refactor`) runs with `node_chosen === 'direct'` and a relevant non-dormant node exists in Tooling Прил.Н, the absence of activation IS a signal (router miss, not a problem in the node itself). + +The rule now reads: + +- **Unused + no profile task** → still not an alert (capability-readiness). +- **Unused + profile task present** → "missed activation", surfaced in STATUS.md C5 and `/brain-retro`. Not a commit block. + +**Mapping artefacts:** +- `tools/observer-classification-map.json` — manual mapping `classification → recommended_node_ids[]` (single SoT). +- `tools/.node-dormancy.json` — generated from Прил.Н by `tools/extract-node-dormancy.mjs` (pre-commit job 17 in lefthook). +- `tools/missed-activations.mjs` — pure deterministic matcher. + +**Detection threshold:** single episode (per user decision 2026-05-21). + +**DEFERRED exclusion:** nodes with `dormant: true` (currently #17 / #44 / #50 / #54 / #67) are filtered before counting. + +**Linkage:** Pravila §16.4 v1.36, plan `docs/superpowers/plans/2026-05-21-observer-missed-activations.md`. +``` + +- [ ] **Step 2: Commit** + +```bash +git add docs/adr/ADR-011-brain-governance.md +git commit -m "docs(adr): ADR-011 amendment — conditional missed-activation rule" +``` + +--- + +## Task 10: brain-retro Skill + Aggregation Template + +**Files:** + +- Modify: `.claude/skills/brain-retro/SKILL.md:41` +- Modify: `.claude/skills/brain-retro/references/aggregation-template.md` + +- [ ] **Step 1: Update SKILL.md behavior reminder** + +Replace line 41 with: + +```markdown +- **«Не использован ≠ проблема» (условное)** — when reporting node usage counts, distinguish two cases: + 1. **Unused + no profile task in episodes** → capability-readiness, do NOT flag. + 2. **Unused + profile task present (missed activation)** → mandatory section in the report. Cite `tools/observer-classification-map.json` for the mapping and `tools/.node-dormancy.json` for DEFERRED exclusions. +- **No auto-edit** — every regulatory suggestion is a candidate, not an action. +``` + +- [ ] **Step 2: Read existing aggregation-template.md to find the right insertion point** + +Run: `head -40 .claude/skills/brain-retro/references/aggregation-template.md` + +- [ ] **Step 3: Insert a new "Missed Activations" section after the factor-matrix template** + +```markdown +## Missed Activations (Pravila §16.4 v1.36) + +Surface candidates where a profile-classified task ran with `node_chosen === 'direct'` and at least one non-dormant recommended node was available. + +**Pulled from:** `analyze(episodes, { classificationMap, dormancy }).missedActivations`. + +| Node | Episodes missed | Classifications | +|---|---|---| +| #NN | N | refactor (a), bugfix (b) | + +**Interpretation guide:** + +- High count on one node → router miss pattern; suggest the node in `/brain-retro` aggregation as a candidate update to `observer-classification-map.json` or as a workflow nudge. +- Spread across many nodes with classification=other-leaning → classification dictionary may need refinement (separate concern, not a missed activation). +- All zero → either no profile work this period, or the router is operating cleanly. + +**NOT to be auto-applied:** these are candidates for human review in retro, not commits or hook blocks. +``` + +- [ ] **Step 4: Commit** + +```bash +git add .claude/skills/brain-retro/SKILL.md .claude/skills/brain-retro/references/aggregation-template.md +git commit -m "docs(brain-retro): conditional rule + Missed Activations section" +``` + +--- + +## Task 11: Outside-Repo Memory Refresh + +**Files:** + +- Modify: `C:\Users\Administrator\.claude\projects\c---------------------crm-------------\memory\feedback_brain_unused_tools_not_problem.md` + +- [ ] **Step 1: Read current memory file** + +Run: Read tool on `C:\Users\Administrator\.claude\projects\c---------------------crm-------------\memory\feedback_brain_unused_tools_not_problem.md`. + +- [ ] **Step 2: Rewrite under new conditional rule** + +```markdown +--- +name: feedback-brain-unused-tools-not-problem +description: Conditional rule — unused brain nodes are not a problem only when no profile task was attempted; with a profile task they become missed-activation candidates surfaced in STATUS.md C5 and /brain-retro +metadata: + type: feedback +--- + +**Rule:** A brain node not activated in real work is NOT a problem **only if there was no profile task for it in the episodes**. With a profile task present (`task_classification` matching the node per `tools/observer-classification-map.json`) and `node_chosen === 'direct'`, it becomes a **missed activation** — surfaced in STATUS.md C5 (⚠️) and `/brain-retro`. + +**Why:** Original v1.0 of the rule (2026-05-19) was unconditional. 2026-05-21 user clarification — capability-readiness applies only to genuinely-not-needed nodes; when a profile task ran without the relevant node, the router missed. + +**How to apply:** + +- Use `tools/missed-activations.mjs` for detection (pure matcher). +- DEFERRED-узлы (#17 / #44 / #50 / #54 / #67) excluded via `tools/.node-dormancy.json`. +- Threshold: single episode (no smoothing). +- Surface, do not block — never an auto-edit, never a commit gate. + +See [[pravila-16-4]] (Pravila §16.4 v1.36) and ADR-011 Amendment 2026-05-21. +``` + +- [ ] **Step 3: Update memory MEMORY.md entry if outdated** + +Read `MEMORY.md`; if the line for `feedback_brain_unused_tools_not_problem` needs the description tightened to the conditional form, update it (one line under 200 chars). + +- [ ] **Step 4: No git commit — outside-repo memory is not tracked** + +Just verify the file was written. + +--- + +## Task 12: Regression + Push + +- [ ] **Step 1: Run full local regression** + +```bash +cd app && npx vitest run ../tools/ && cd .. +node tools/extract-node-dormancy.mjs +node tools/status-md-generator.mjs +git diff docs/observer/STATUS.md +``` + +Expected: all Vitest GREEN; STATUS.md diff shows new `missed_activations` line and conditional phrasing. + +- [ ] **Step 2: Run lefthook full pre-commit** + +```bash +npx lefthook run pre-commit +``` + +Expected: all jobs GREEN incl. new job 17. + +- [ ] **Step 3: Verify no normative file lag (§15.2 final)** + +```bash +git fetch origin +git log HEAD..origin/main --oneline -- docs/Pravila_raboty_Claude_v1_1.md docs/Tooling_v8_3.md CLAUDE.md docs/Plugin_stack_rules_v1.md +``` + +Expected: no commits ahead. + +- [ ] **Step 4: Push to origin/main** + +```bash +git push origin feat/observer-missed-activations:main +``` + +(Or open PR if user prefers PR-flow.) + +--- + +## Self-Review + +**Spec coverage** — verified against original requirements: + +- Conditional rule formulation → Task 8 (Pravila §16.4 v1.36). +- DEFERRED exclusion (#44/#50/#54/#67/+#17) → Task 2 (dormancy extract) + Task 4 (matcher filters live = non-dormant). +- Single-episode threshold → Task 4 (matcher counts every qualifying episode, no smoothing). +- STATUS.md proactive surface → Task 6 (C5 detail) + Task 7 (rendering + ⚠️). +- `/brain-retro` retro section → Task 5 (analyzer output) + Task 10 (template). +- Hybrid C architecture (manual classification map + auto-generated dormancy) → Tasks 1, 2, 3. +- Lefthook automation → Task 3. +- ADR amendment → Task 9. +- Skill update → Task 10. +- Memory refresh → Task 11. +- Regression + push → Task 12. + +**Placeholder scan** — no TBD/TODO/«similar to»/«handle edge cases». Every code step contains real code. + +**Type consistency** — `detectMissedActivations(episodes, classificationMap, dormancy)` signature stable across Tasks 4, 5, 6. Return shape `{ totalMissed, byNode, byClassification }` stable across all consumers. `analyze(episodes, options)` second-arg matches Task 5 test and Task 7 CLI. + +**Open items for human review during execution:** + +- Job number 17 in lefthook — confirm by reading actual `lefthook.yml` (estimate; may need to be 18 if 16 is taken). +- Classification map (Task 1) — initial bucket-to-nodes assignment is a judgment call; reviewer may want to adjust before merging. + +--- + +## Execution Handoff + +Save complete. Two execution options: + +**1. Subagent-Driven (recommended for this multi-task plan)** — fresh subagent per task, two-stage review, controller (Opus) verifies each commit per Pravila §15.1. + +**2. Inline Execution** — execute in this session via `superpowers:executing-plans`, batch with checkpoints. + +Which approach?