diff --git a/docs/observer/STATUS.md b/docs/observer/STATUS.md index ca1e9ee5..05394e2a 100644 --- a/docs/observer/STATUS.md +++ b/docs/observer/STATUS.md @@ -1,6 +1,6 @@ # Brain Status (auto-generated) -Last updated: 2026-05-24T08:32:29.613Z +Last updated: 2026-05-24T12:42:18.762Z | Контролёр | Состояние | Детали | |---|---|---| @@ -8,14 +8,14 @@ Last updated: 2026-05-24T08:32:29.613Z | C2 Cross-ref consistency | ✅ | [cross-ref-checker] OK — 0 drift in 4 files | | C3 Observer-of-observer | ✅ | [observer-of-observer] OK — last read 0 week(s) ago | | C4 Сигнальный статус | ✅ | This file (self-reference) | -| C5 Observer-coverage | ⚠️ | 134 episode(s) this month · .git/hooks/post-commit not installed (run: npx lefthook install --force) · 17 missed activation(s) — see /brain-retro | +| C5 Observer-coverage | ⚠️ | 135 episode(s) this month · .git/hooks/post-commit not installed (run: npx lefthook install --force) · 17 missed activation(s) — see /brain-retro | | C6 Chain map sync | ✅ | [chain-map-checker] OK — 16 chains in sync | ## Метрики (информационные, не алерты) -- Observer evidence: 134 episodes this month, 0 observer_error markers, 5 PII matches before filter -- Legacy v1 episodes (not in factor analysis): 10 -- Last /brain-retro: 0 day(s) ago +- Observer evidence: 135 episodes this month, 0 observer_error markers, 6 PII matches before filter +- Legacy v1 episodes (not in factor analysis): 11 +- Last /brain-retro: 1 day(s) ago - Использование узлов: см. `/brain-retro` (раз в спринт). missed_activations: 17. **Неиспользованные узлы — не алерт, если профильной задачи не было** (Pravila §16.4 v1.36; capability-readiness; см. memory `feedback_brain_unused_tools_not_problem` — outside-repo memory store). ## Метрики дисциплины @@ -32,17 +32,17 @@ Baseline дисциплины роутера (этап 2 router discipline overh | cleanup | 1 | 0.0% | 0.0% | | monitoring | 1 | 0.0% | 0.0% | -Router step distribution: 1: 129 ⚠️ suspicious — >90% эпизодов остановились на step=1 (вероятный sentinel-bug парсера) +Router step distribution: 1: 55, 2: 45, 3: 12, 5: 18 -Boundaries applied (ADR / границы): 13 of 129 эпизодов (10.1%). +Boundaries applied (ADR / границы): 13 of 130 эпизодов (10.0%). ## Активные многоэтапные проекты - **Router discipline overhaul** ([spec](../superpowers/specs/2026-05-23-router-discipline-overhaul-design.md)) - Этап 1 (машиночитаемый реестр) ✅ закрыт 2026-05-23 — `docs/registry/nodes.yaml` (83 узла + 16 chains L1-L16), `tools/registry-load.mjs` + `tools/registry-render.mjs` (16 тестов), auto-render Tooling §4.0 + routing-off-phase, lefthook job 17 (warn-only). - - Этап 2 (измерения + классификатор-парсер) ✅ закрыт 2026-05-24 — discipline-metrics (3 среза), brain-retro-analyzer переключён на реестр, STATUS.md блок «Метрики дисциплины», baseline snapshot `docs/observer/baselines/2026-05-24-pre-enforcement.md`. Plan: `docs/superpowers/plans/2026-05-24-router-overhaul-stage-2-measurements.md`. - - Этап 3 (принуждение — хук на routing) ⏸ ждёт «продолжаем» от заказчика после ревью baseline-цифр. - - Этап 4 (уборка правил) — не начат. + - Этап 2 (измерения + классификатор-парсер) ✅ закрыт 2026-05-24 + влит в main 2026-05-24 — discipline-metrics (3 среза), brain-retro-analyzer переключён на реестр, STATUS.md блок «Метрики дисциплины», baseline snapshot `docs/observer/baselines/2026-05-24-pre-enforcement.md`. Plan: `docs/superpowers/plans/2026-05-24-router-overhaul-stage-2-measurements.md`. + - Этап 3 (принуждение — хук на routing) — Phase A+B (классификатор + 3 хука: router-prehook/tool-gate/stop-gate в `.claude/settings.json`) ✅ + влит в main 2026-05-24. Гейт работает в режиме **`warn-only`** (только stderr-предупреждения, никакой блокировки). Bug-fix `bec69aa5`: `deriveRouterStep` в `tools/discipline-metrics.mjs` — шаг роутера теперь выводится из наблюдаемых признаков (был захардкоженной константой 1). CHECKPOINT B: дать warn-only накопить реальные наблюдения (план говорит «минимум 24 часа»), затем Task 9 — переключение в `enforce` + 2 новых метрики (domain-hit-rate / chain-completion). Plan: `docs/superpowers/plans/2026-05-24-router-overhaul-stage-3-enforcement.md`. + - Этап 4 (уборка устаревших правил, deprecation `observer-classification-map.json` → удаление) — не начат. ## Алерт-индикаторы diff --git a/tools/observer-state-enricher.mjs b/tools/observer-state-enricher.mjs new file mode 100644 index 00000000..32ff493b --- /dev/null +++ b/tools/observer-state-enricher.mjs @@ -0,0 +1,44 @@ +#!/usr/bin/env node +/** + * Router state enricher for observer episodes. + * Reads ~/.claude/runtime/router-state-.json and exposes pure + * extraction helpers for primary_rationale enrichment. + * + * Pure-ish — fs is parameterized via options.baseDir for testability. + * + * Per spec: docs/superpowers/specs/2026-05-24-router-stage3-three-fixes-design.md + */ + +import { readFileSync, existsSync } from 'fs'; +import { join } from 'path'; +import { homedir } from 'os'; + +function defaultBaseDir() { + return join(homedir(), '.claude', 'runtime'); +} + +export function readRouterState(sessionId, options = {}) { + if (!sessionId || typeof sessionId !== 'string') return null; + const baseDir = options.baseDir || defaultBaseDir(); + const path = join(baseDir, `router-state-${sessionId}.json`); + if (!existsSync(path)) return null; + try { + const content = readFileSync(path, 'utf-8'); + return JSON.parse(content); + } catch { + return null; + } +} + +export function extractRouterFields(state) { + if (!state || typeof state !== 'object') { + return { recommended_node: null, recommended_chain: null, chain_progress: [], chain_completed: false }; + } + const cls = state.classification || {}; + return { + recommended_node: cls.recommendedNode || null, + recommended_chain: cls.recommendedChain || null, + chain_progress: Array.isArray(state.chainProgress) ? state.chainProgress : [], + chain_completed: state.chainCompleted === true, + }; +} diff --git a/tools/observer-state-enricher.test.mjs b/tools/observer-state-enricher.test.mjs new file mode 100644 index 00000000..f85b209a --- /dev/null +++ b/tools/observer-state-enricher.test.mjs @@ -0,0 +1,98 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdtempSync, writeFileSync, rmSync } from 'fs'; +import { tmpdir } from 'os'; +import { join } from 'path'; +import { readRouterState } from './observer-state-enricher.mjs'; + +describe('readRouterState', () => { + let baseDir; + + beforeEach(() => { + baseDir = mkdtempSync(join(tmpdir(), 'router-state-test-')); + }); + + afterEach(() => { + rmSync(baseDir, { recursive: true, force: true }); + }); + + it('returns null when state file does not exist', () => { + expect(readRouterState('abc-123', { baseDir })).toBeNull(); + }); + + it('reads state file when present', () => { + const state = { + sessionId: 'abc-123', + classification: { recommendedNode: '#62', recommendedChain: '#13' }, + chainProgress: ['brainstorming'], + chainCompleted: false, + }; + writeFileSync(join(baseDir, 'router-state-abc-123.json'), JSON.stringify(state)); + const result = readRouterState('abc-123', { baseDir }); + expect(result).toEqual(state); + }); + + it('returns null on malformed JSON', () => { + writeFileSync(join(baseDir, 'router-state-broken.json'), 'not-json'); + expect(readRouterState('broken', { baseDir })).toBeNull(); + }); + + it('returns null on missing sessionId', () => { + expect(readRouterState(null, { baseDir })).toBeNull(); + expect(readRouterState('', { baseDir })).toBeNull(); + }); + + it('uses ~/.claude/runtime/ as default baseDir', () => { + // Smoke-check: default baseDir resolution doesn't throw. + // Real-file reading covered above with explicit baseDir. + const result = readRouterState('non-existent-session-xyz'); + // Either null (file doesn't exist there) or object — both fine. + expect(result === null || typeof result === 'object').toBe(true); + }); +}); + +describe('extractRouterFields', () => { + it('extracts the four fields from state, defaulting to null/empty', async () => { + const { extractRouterFields } = await import('./observer-state-enricher.mjs'); + const state = { + classification: { recommendedNode: '#62', recommendedChain: '#13' }, + chainProgress: ['brainstorming', 'writing-plans'], + chainCompleted: false, + }; + expect(extractRouterFields(state)).toEqual({ + recommended_node: '#62', + recommended_chain: '#13', + chain_progress: ['brainstorming', 'writing-plans'], + chain_completed: false, + }); + }); + + it('returns nulls/empty when state is null', async () => { + const { extractRouterFields } = await import('./observer-state-enricher.mjs'); + expect(extractRouterFields(null)).toEqual({ + recommended_node: null, + recommended_chain: null, + chain_progress: [], + chain_completed: false, + }); + }); + + it('handles missing classification block', async () => { + const { extractRouterFields } = await import('./observer-state-enricher.mjs'); + expect(extractRouterFields({ chainProgress: ['x'], chainCompleted: true })).toEqual({ + recommended_node: null, + recommended_chain: null, + chain_progress: ['x'], + chain_completed: true, + }); + }); + + it('treats empty string recommendedNode/recommendedChain as null', async () => { + const { extractRouterFields } = await import('./observer-state-enricher.mjs'); + expect(extractRouterFields({ classification: { recommendedNode: '', recommendedChain: '' } })).toEqual({ + recommended_node: null, + recommended_chain: null, + chain_progress: [], + chain_completed: false, + }); + }); +});