#!/usr/bin/env node import { readFileSync, writeFileSync, existsSync, readdirSync } from 'fs'; import { join } from 'path'; import { execFileSync } from 'child_process'; import { homedir } from 'os'; import { runCoverageChecker } from './observer-coverage-checker.mjs'; import { analyze } from './brain-retro-analyzer.mjs'; import { loadRegistry } from './registry-load.mjs'; import { buildClassificationMap, buildDormancyMap } from './registry-to-classification-map.mjs'; import { computeOverrideUsageBlock } from './enforce-override-monitor.mjs'; import { queryRunningProcesses, computeSystemHealthBlock } from './system-health.mjs'; import { checkManifest } from './floor-manifest-check.mjs'; import { judgeGateMode } from './judge-gate-config.mjs'; import { loadQueue, statusSignal, pendingCount } from './router-learning-queue.mjs'; import { auditDoors, auditExempt, extractGateMatcher, CANONICAL_MUTATING_TOOLS, isMutatingTool } from './door-coverage.mjs'; import { SEED_TOOLS } from './enforce-supreme-gate.mjs'; import { loadJournal, verifyChain } from './action-journal.mjs'; import { resolveReceiptKey } from './receipt-key-config.mjs'; import { loadRecentBlocks, loadRecentEscapes } from './guard-block-log.mjs'; const PRICING = { sonnet46: { input_per_mtok: 3.0, output_per_mtok: 15.0 }, opus47: { input_per_mtok: 15.0, output_per_mtok: 75.0 }, }; function iconFor(status) { return { ok: '✅', warn: '⚠️', fail: '🔴' }[status] || '⚪'; } const GUARD_LABELS = { 'enforce-floor.mjs': 'М5 Пол (вето-до-плана / content-floor)', 'enforce-supreme-gate.mjs': 'М2 Стена (действие = шаг плана)', 'enforce-judge-gate.mjs': 'М4 Судья (приёмка + надзор)', 'enforce-snapshot.mjs': 'М6 Снимок (точка отката)', 'enforce-floor-escape-consume.mjs': 'М6 Escape владельца (законная дверь)', 'enforce-read-path-deny.mjs': 'М5 Read-exfil страж', 'enforce-mcp-classification.mjs': 'М5 Egress-exfil страж', 'enforce-normative-content-rules.mjs': 'М1/М5 Нормативный страж (КАРТА/ЗАКОН)', 'enforce-skill-journaler.mjs': 'М1 Журналер навыков', }; /** * §7 доска «кто на посту» — read-only снимок обороны М1–М6 (Кусок 3 М7). Манифест (checkManifest) * даёт registered/missing; missing → «⚠️ ПОСТ ПУСТОЙ» (нельзя объявить «оборона стоит» без * подтверждения регистрации, SE-B/Δ8). + режим судьи М4 + счётчики недавних escape/блоков. Чистая, * ничего не блокирует. */ /** Экранировать значение для одной ячейки markdown-таблицы (anti-injection, sharp-edges SE-3). */ function escapeCell(v) { return String(v ?? '').replace(/\r?\n/g, ' ').replace(/\|/g, '\\|').slice(0, 120); } export function computeGuardBoardBlock({ manifest, judgeMode = 'inert', recentEscapes = [], recentBlocks = [] } = {}) { const m = manifest || { registered: [], missing: [], ok: false }; const reg = Array.isArray(m.registered) ? m.registered : []; const miss = Array.isArray(m.missing) ? m.missing : []; const post = m.ok && miss.length === 0 ? '✅ ПОСТ ПОЛНЫЙ — все стражи М1–М6 зарегистрированы' : `⚠️ **ПОСТ ПУСТОЙ** — не зарегистрированы: ${miss.join(', ') || '(неизвестно)'} (оборона НЕ подтверждена; SE-B/Δ8)`; const rows = [...reg, ...miss].map((h) => { const isReg = reg.includes(h); return `| ${GUARD_LABELS[h] || h} | \`${h}\` | ${isReg ? '✅' : '🔴'} |`; }).join('\n'); const esc = Array.isArray(recentEscapes) ? recentEscapes.length : 0; const blk = Array.isArray(recentBlocks) ? recentBlocks.length : 0; const detailTable = (events, title) => { if (!Array.isArray(events) || events.length === 0) return ''; const trows = events .map((e) => `| ${escapeCell(e && e.ts)} | ${escapeCell(e && e.action)} | ${escapeCell(e && e.reason)} |`) .join('\n'); return `\n**${title}:**\n\n| Время | Действие | Причина |\n|---|---|---|\n${trows}\n`; }; const detail = detailTable(recentEscapes, 'Недавние escape владельца (детали)') + detailTable(recentBlocks, 'Недавние блоки (детали)'); return `## Кто на посту (оборона М1–М6) ${post} Судья М4: **${judgeMode}** (inert $0 / shadow / floor-only / live-block) | Машина / страж | Хук | Зарегистрирован | |---|---|---| ${rows || '| (нет требуемых хуков) | — | — |'} Недавние escape владельца: ${esc} · Недавние блоки: ${blk} ${detail}`; } /** * R-09 (Блок B Класс 1) — read-only блок очереди обучения роутера в STATUS. * Реюз `statusSignal`/`pendingCount` из router-learning-queue. Наполнение фонда — * ТОЛЬКО по явному «да» владельца (hard-rule модуля); здесь только показываем pending. */ export function computeLearningQueueBlock({ queue } = {}) { const q = Array.isArray(queue) ? queue : []; if (pendingCount(q) === 0) { return `## Очередь обучения роутера\n\nОчередь пуста — нет кандидатов на одобрение.`; } const lines = q.filter((e) => e && e.status === 'pending') .map((e) => `- [${escapeCell(e.id)}] (${escapeCell(e.kind)}) ${escapeCell(e.summary)}`) .join('\n'); return `## Очередь обучения роутера\n\n${statusSignal(q)} — наполнение фонда ТОЛЬКО по явному «да» владельца (см. \`/brain-retro\`).\n\n${lines}`; } /** * R-24 (Блок B Класс 2) — read-only блок покрытия дверей. Инструмент, не покрытый matcher'ом * верховной стены и не семя = «забытая дверь» (урок F1: PowerShell мимо Bash). auditExempt * страхует: семя, чья способность мутирующая, = опасное исключение. Гейт не трогается. */ export function computeDoorCoverageBlock({ settings } = {}) { const s = settings || {}; const matcher = extractGateMatcher(s, 'enforce-supreme-gate.mjs'); const seeds = [...SEED_TOOLS]; const doors = auditDoors({ tools: CANONICAL_MUTATING_TOOLS, matcher, seeds }); const exempt = auditExempt({ exempt: seeds, isMutating: isMutatingTool }); const matcherStr = matcher.includes('*') ? '*' : (matcher.join(', ') || '(хук НЕ зарегистрирован)'); if (doors.ok && exempt.ok) { return `## Покрытие дверей\n\n✅ Все двери покрыты верховной стеной М2 (matcher: ${escapeCell(matcherStr)}).`; } const parts = [`## Покрытие дверей\n\n⚠️ Есть забытые двери (matcher: ${escapeCell(matcherStr)}).`]; if (!doors.ok) { parts.push(`\nНепокрытые мутирующие инструменты: ${doors.uncovered.map(escapeCell).join(', ')}`); } if (!exempt.ok) { parts.push(`\n🔴 Опасные исключения (семя с мутирующей способностью): ${exempt.flagged.map(escapeCell).join(', ')}`); } return parts.join('\n'); } /** * R-30 (Блок B Класс 3) — read-only блок целостности журналов действий. Требование * «verifyChain живьём» выполнено observation-only: sweep по ~/.claude/runtime/action-journal-* * делает main, сюда приходят уже посчитанные results. Без новых block-путей, fail-CLOSE * примитива verifyChain не понижается. keyAvailable=false → не «битые», а «проверка недоступна». */ export function computeJournalIntegrityBlock({ results, keyAvailable } = {}) { if (keyAvailable === false) { return `## Целостность журналов действий\n\nКлюч подписанта не provisioned — проверка цепи недоступна (ключ — owner-шаг A3).`; } const r = Array.isArray(results) ? results : []; if (r.length === 0) { return `## Целостность журналов действий\n\nЖурналов сессий не найдено (\`~/.claude/runtime/action-journal-*.jsonl\`).`; } const broken = r.filter((x) => x && !x.ok); if (broken.length === 0) { return `## Целостность журналов действий\n\n✅ Все цепочки целы (${r.length} сессий, verifyChain live).`; } const rows = broken.map((x) => `| \`${escapeCell(x.sessionId)}\` | ${x.brokenAt === null || x.brokenAt === undefined ? '—' : escapeCell(x.brokenAt)} |`).join('\n'); return `## Целостность журналов действий\n\n🔴 Битые цепочки (${broken.length} из ${r.length}):\n\n| session | broken at seq |\n|---|---|\n${rows}`; } export function computeCostBlock(episodes, pricing = PRICING) { let classifierInputTok = 0, classifierOutputTok = 0; let selfInputTok = 0, selfOutputTok = 0; let reviewerSubagentUsd = 0, reviewerInputTok = 0, reviewerOutputTok = 0, reviewerFallbackUsd = 0; for (const ep of episodes) { const tc = ep.task_cost; if (!tc) continue; classifierInputTok += tc.classifier_input_tokens || 0; classifierOutputTok += tc.classifier_output_tokens || 0; selfInputTok += tc.self_assessment_input_tokens || 0; selfOutputTok += tc.self_assessment_output_tokens || 0; reviewerSubagentUsd += tc.reviewer_subagent_usd || 0; reviewerInputTok += tc.reviewer_input_tokens || 0; reviewerOutputTok += tc.reviewer_output_tokens || 0; reviewerFallbackUsd += tc.reviewer_direct_fallback_usd || 0; } const s46 = pricing.sonnet46; const o47 = pricing.opus47; const classifierUsd = (classifierInputTok / 1e6) * s46.input_per_mtok + (classifierOutputTok / 1e6) * s46.output_per_mtok; const selfUsd = (selfInputTok / 1e6) * s46.input_per_mtok + (selfOutputTok / 1e6) * s46.output_per_mtok; const reviewerUsd = reviewerSubagentUsd + (reviewerInputTok / 1e6) * o47.input_per_mtok + (reviewerOutputTok / 1e6) * o47.output_per_mtok + reviewerFallbackUsd; const totalUsd = classifierUsd + selfUsd + reviewerUsd; return `## Стоимость месяца | Компонент | Токены (in/out) | USD | |---|---|---| | Classifier (Sonnet 4.6) | ${classifierInputTok}/${classifierOutputTok} | $${classifierUsd.toFixed(2)} | | Self-assessment (Sonnet 4.6) | ${selfInputTok}/${selfOutputTok} | $${selfUsd.toFixed(2)} | | Reviewer (Opus 4.7 + fallback) | ${reviewerInputTok}/${reviewerOutputTok} | $${reviewerUsd.toFixed(2)} | | **Итого** | | **$${totalUsd.toFixed(2)}** | `; } export function computeAnomalyBlock(episodes) { const values = episodes .map(ep => ep.task_cost?.classifier_output_tokens || 0) .filter(v => v > 0); let anomalyLine = 'Аномалий нет.'; if (values.length > 0) { const sorted = [...values].sort((a, b) => a - b); const mid = Math.floor(sorted.length / 2); const median = sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid]; const threshold = Math.max(median * 3, 5000); const outliers = values .map((v, i) => ({ v, i })) .filter(({ v }) => v > threshold) .sort((a, b) => b.v - a.v) .slice(0, 5); if (outliers.length > 0) { const rows = outliers .map(({ v, i }) => `| episode[${i}] | ${v} | медиана ${median.toFixed(0)}, порог ${threshold.toFixed(0)} |`) .join('\n'); anomalyLine = `| Эпизод | classifier_output_tokens | Примечание |\n|---|---|---|\n${rows}`; } } return `## Аномалии классификатора ${anomalyLine} `; } export function computeSelfRetrospectBlock(counterPath, fsImpl = { existsSync, readFileSync }) { if (!fsImpl.existsSync(counterPath)) { return `## Авто-ретроспектива Last self-retrospect: never `; } try { const data = JSON.parse(fsImpl.readFileSync(counterPath, 'utf-8')); const lastRunAt = data.last_run_at || null; const episodesSince = data.episodes_since_last ?? 0; const threshold = data.threshold ?? 10; const daysAgo = lastRunAt ? Math.floor((Date.now() - new Date(lastRunAt).getTime()) / 86400000) : null; const retroLine = daysAgo === null ? 'never' : `${daysAgo} day(s) ago`; const warn = episodesSince >= threshold ? ` ⚠️ (${episodesSince} эпизодов с последнего запуска, порог ${threshold})` : ''; return `## Авто-ретроспектива Last self-retrospect: ${retroLine}${warn} Episodes since last run: ${episodesSince} / threshold: ${threshold} `; } catch { return `## Авто-ретроспектива Last self-retrospect: never `; } } /** * Brain-retro #5 candidate B (2026-05-26): session-length warning. * * Long sessions correlate with discipline drift — reviewer pass on retro #5 * showed regulated rate dropped 19% → 4.5% during a long session. * * Algorithm: group episodes by task_id (session id), compute MAX * session_turn per session over the current calendar day (UTC), surface * sessions with turn count >= threshold. * * Pure — takes episodes array, returns markdown string. No I/O. */ export function computeSessionLengthBlock(episodes, opts = {}) { const threshold = opts.threshold ?? 50; const now = opts.now ? new Date(opts.now) : new Date(); const todayUtc = now.toISOString().slice(0, 10); if (!Array.isArray(episodes) || episodes.length === 0) { return `## Длинные сессии\n\n(нет данных)`; } const sessions = new Map(); for (const e of episodes) { if (!e || !e.task_id || !e.timestamps?.started_at) continue; if (e.timestamps.started_at.slice(0, 10) !== todayUtc) continue; const turn = Number(e.environment?.session_turn); if (!Number.isFinite(turn)) continue; const id = e.task_id; const cur = sessions.get(id) || { maxTurn: 0, lastSeen: '', regulated: 0, total: 0 }; if (turn > cur.maxTurn) cur.maxTurn = turn; if (e.timestamps.started_at > cur.lastSeen) cur.lastSeen = e.timestamps.started_at; cur.total++; if (e.path_type === 'regulated') cur.regulated++; sessions.set(id, cur); } const longOnes = [...sessions.entries()] .filter(([, v]) => v.maxTurn >= threshold) .sort((a, b) => b[1].maxTurn - a[1].maxTurn); if (longOnes.length === 0) { return `## Длинные сессии\n\nНи одной сессии с >${threshold} ходов сегодня (UTC). ✅`; } const rows = longOnes.map(([id, v]) => { const regPct = v.total > 0 ? ((v.regulated / v.total) * 100).toFixed(0) : '—'; const shortId = id.slice(0, 8); return `| \`${shortId}\` | ${v.maxTurn} | ${regPct}% | ${v.lastSeen} |`; }).join('\n'); return `## Длинные сессии ⚠️ Сегодня (${todayUtc} UTC) есть сессии с ≥${threshold} ходов — корреляция с падением дисциплины роутинга (retro #5 candidate B). | session_id | макс. ход | % regulated | последний эпизод | |---|---|---|---| ${rows} Long sessions correlate with discipline drift. Если % regulated просел в текущей сессии — рассмотри перезапуск.`; } export function computeReviewerBlock(episodes) { const reviewed = episodes.filter(ep => ep.review?.reviewed_at !== null && ep.review?.reviewed_at !== undefined); const total = episodes.length; const reviewedCount = reviewed.length; if (reviewedCount === 0) { return `## Reviewer: субагент vs fallback 0 эпизодов проверено из ${total}. `; } const counts = {}; let errors = 0; for (const ep of reviewed) { const r = ep.review?.reviewer || 'unknown'; counts[r] = (counts[r] || 0) + 1; if (ep.review?.reviewer_error) errors++; } const rows = Object.entries(counts) .sort((a, b) => b[1] - a[1]) .map(([name, cnt]) => `| ${name} | ${cnt} | ${((cnt / reviewedCount) * 100).toFixed(1)}% |`) .join('\n'); return `## Reviewer: субагент vs fallback Проверено: ${reviewedCount} из ${total} эпизодов (${((reviewedCount / total) * 100).toFixed(1)}%). Ошибок ревьюера: ${errors}. | Reviewer | Эпизодов | % от проверенных | |---|---|---| ${rows} `; } /** * brain-retro #7 C4 (2026-05-27): surface reviewer findings in STATUS.md. * * Owner reported "I don't notice wrong_skill cases between retros". This block * gives the dashboard a per-period view of: * - error_root_cause histogram (wrong_skill / wrong_chain_order / external_failure / n/a) * - top alternative_better suggestions from reviewer * - node_quality histogram (correct / disputable / wrong_node) * * Read-only, deterministic, pure. No filtering by time — operates on whatever * episodes are loaded by the caller (current month per loadCurrentMonthEpisodes). */ export function computeReviewerFindingsBlock(episodes) { // Detect "reviewed" via either signal: // - ep.review.reviewed_at (subagent / canonical schema), OR // - ep.outcome_reviewed_source (batch-reviewer: "direct_api_batch" / "direct_api_fallback"), // while excluding any episode whose reviewer reported an error. const reviewed = (episodes || []).filter((ep) => { if (!ep?.review || ep?.review?.reviewer_error) return false; if (ep.review.reviewed_at) return true; if (typeof ep.outcome_reviewed_source === 'string' && ep.outcome_reviewed_source.startsWith('direct_api')) return true; if (typeof ep.outcome_reviewed_source === 'string' && ep.outcome_reviewed_source === 'subagent') return true; return false; }); if (reviewed.length === 0) { return `## Reviewer findings (нет проверенных эпизодов в текущем периоде) `; } const causeCounts = {}; const altCounts = {}; const qualityCounts = {}; let actionable = 0; for (const ep of reviewed) { const r = ep.review || {}; const cause = r.error_root_cause || 'n/a'; causeCounts[cause] = (causeCounts[cause] || 0) + 1; if (cause === 'wrong_skill' || cause === 'wrong_chain_order') actionable++; if (r.alternative_better) { altCounts[r.alternative_better] = (altCounts[r.alternative_better] || 0) + 1; } const q = r.node_quality || 'unknown'; qualityCounts[q] = (qualityCounts[q] || 0) + 1; } if (actionable === 0) { // No wrong_skill / wrong_chain_order — surface short "all clean" line. return `## Reviewer findings Проверено: ${reviewed.length}. **Всё чисто** — нет wrong_skill / wrong_chain_order. `; } const causeRows = Object.entries(causeCounts) .sort((a, b) => b[1] - a[1]) .map(([k, v]) => `| ${k} | ${v} |`) .join('\n'); const altRows = Object.entries(altCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 5) .map(([k, v]) => `| ${k} | ${v} |`) .join('\n'); const qualityRows = Object.entries(qualityCounts) .sort((a, b) => b[1] - a[1]) .map(([k, v]) => `| ${k} | ${v} |`) .join('\n'); return `## Reviewer findings Проверено: ${reviewed.length} эпизодов. **${actionable} actionable** (wrong_skill + wrong_chain_order). ### error_root_cause | cause | count | |---|---:| ${causeRows} ### Топ alternative_better | recommended | count | |---|---:| ${altRows || '| — | 0 |'} ### node_quality | judgment | count | |---|---:| ${qualityRows} `; } export function renderStatus(inputs) { const { now, c1, c2, c3, c5, observer, lastRetroDaysAgo, discipline } = inputs; const missed = inputs.missed || { totalMissed: 0, byNode: {}, byClassification: {} }; function formatPercent(p) { return `${(p * 100).toFixed(1)}%`; } let disciplineBlock = ''; if (discipline) { const rows = Object.entries(discipline.byClassification || {}) .sort((a, b) => b[1].episodes - a[1].episodes) .map(([cls, b]) => `| ${cls} | ${b.episodes} | ${formatPercent(b.pctTriggerMatch)} | ${formatPercent(b.pctViaSkill)} |`) .join('\n'); const stepDist = Object.entries(discipline.routerStep?.distribution || {}) .map(([k, v]) => `${k}: ${v}`).join(', '); const suspicious = discipline.routerStep?.suspicious ? ' ⚠️ suspicious — >90% эпизодов остановились на step=1 (вероятный sentinel-bug парсера)' : ''; const boundariesPct = formatPercent(discipline.boundariesRate?.rate || 0); disciplineBlock = ` ## Метрики дисциплины Baseline дисциплины роутера (этап 2 router discipline overhaul, spec 2026-05-23). Цель — увидеть «точку До» перед enforcement-хуком этапа 3. | Тип задачи | Эпизодов | % с триггер-матчем | % через скил | |---|---|---|---| ${rows || '| (no data) | 0 | 0% | 0% |'} Router step distribution: ${stepDist || '(empty)'}${suspicious} Boundaries applied (ADR / границы): ${discipline.boundariesRate?.withBoundaries || 0} of ${discipline.boundariesRate?.total || 0} эпизодов (${boundariesPct}). `; } const activeProjects = (inputs.activeProjects || '').trim(); const projectsBlock = activeProjects ? `\n## Активные многоэтапные проекты\n\n${activeProjects}\n` : ''; const retroLine = (lastRetroDaysAgo === null || lastRetroDaysAgo === undefined) ? 'never' : `${lastRetroDaysAgo} day(s) ago`; 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) | | C5 Observer-coverage | ${iconFor(c5.status)} | ${c5.detail || '—'} | ${inputs.guardBoardBlock ? `\n${inputs.guardBoardBlock.trim()}\n` : ''} ## Метрики (информационные, не алерты) - Observer evidence: ${observer.episodeCount} episodes this month, ${observer.observerErrors} observer_error markers, ${observer.piiMatches} PII matches before filter - Legacy v1 episodes (not in factor analysis): ${observer.v1Episodes || 0} - Last /brain-retro: ${retroLine} - Использование узлов: см. \`/brain-retro\` (раз в спринт). missed_activations: ${missed.totalMissed}. **Неиспользованные узлы — не алерт, если профильной задачи не было** (Pravila §16.4 v1.36; capability-readiness; см. memory \`feedback_brain_unused_tools_not_problem\` — outside-repo memory store). ${disciplineBlock}${projectsBlock}${inputs.sessionLengthBlock ? `\n${inputs.sessionLengthBlock.trim()}\n` : ''}${inputs.costBlock ? `\n${inputs.costBlock.trim()}\n` : ''}${inputs.anomalyBlock ? `\n${inputs.anomalyBlock.trim()}\n` : ''}${inputs.selfRetrospectBlock ? `\n${inputs.selfRetrospectBlock.trim()}\n` : ''}${inputs.reviewerBlock ? `\n${inputs.reviewerBlock.trim()}\n` : ''}${inputs.reviewerFindingsBlock ? `\n${inputs.reviewerFindingsBlock.trim()}\n` : ''}${inputs.overrideUsageBlock ? `\n${inputs.overrideUsageBlock.trim()}\n` : ''}${inputs.systemHealthBlock ? `\n${inputs.systemHealthBlock.trim()}\n` : ''}${inputs.learningQueueBlock ? `\n${inputs.learningQueueBlock.trim()}\n` : ''}${inputs.doorCoverageBlock ? `\n${inputs.doorCoverageBlock.trim()}\n` : ''}${inputs.journalIntegrityBlock ? `\n${inputs.journalIntegrityBlock.trim()}\n` : ''} ## Алерт-индикаторы ✅ — норма ・ ⚠️ — внимание ・ 🔴 — действие требуется ・ ⚪ — не запускалось `; } 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() }; } } // Config-seam (Task 7): расположение журнала наблюдателя. Дефолт docs/observer // (backward-compat); CLI-блок переопределяет из brain.local.md (resolveStateDir). let STATE_DIR = 'docs/observer'; function countEpisodes() { const dir = STATE_DIR; 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; } function countPiiMatches() { const file = join(STATE_DIR,'.pii-counters.json'); if (!existsSync(file)) return 0; try { const data = JSON.parse(readFileSync(file, 'utf-8')); const month = new Date().toISOString().slice(0, 7); const monthCounts = data[month] || {}; return Object.values(monthCounts).reduce((s, n) => s + n, 0); } catch { return 0; } } function lastRetroDaysAgo() { const file = join(STATE_DIR,'.read-counter.json'); if (!existsSync(file)) return null; try { const data = JSON.parse(readFileSync(file, 'utf-8')); if (!data.last_read_at) return null; return Math.floor((Date.now() - new Date(data.last_read_at).getTime()) / 86400000); } catch { return null; } } function countObserverErrors() { const dir = STATE_DIR; 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((l) => l.includes('"observer_error":true')).length; } /** Legacy v1 episode count — lines without schema_version=2, not observer_error markers. */ function countV1Episodes() { const dir = STATE_DIR; 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((l) => l && !l.includes('"schema_version":2') && !l.includes('"observer_error":true')).length; } function loadCurrentMonthEpisodes() { const month = new Date().toISOString().slice(0, 7); const file = join(STATE_DIR,`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; } if (process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/status-md-generator.mjs')) { try { const { loadConfig, resolveStateDir } = await import('./brain-config.mjs'); STATE_DIR = resolveStateDir(loadConfig().state_dir).stateDir; } catch { /* brain-config недоступен → STATE_DIR остаётся docs/observer */ } 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 }, observer: { episodeCount: countEpisodes(), observerErrors: countObserverErrors(), piiMatches: countPiiMatches(), v1Episodes: countV1Episodes(), }, missed: cov.missed, lastRetroDaysAgo: lastRetroDaysAgo(), activeProjects: existsSync(join(STATE_DIR, 'active-projects.md')) ? readFileSync(join(STATE_DIR, 'active-projects.md'), 'utf-8') : '', discipline: (() => { try { const registry = loadRegistry({ useCache: false }); const classificationMap = buildClassificationMap(registry); const dormancy = buildDormancyMap(registry); const eps = loadCurrentMonthEpisodes(); const a = analyze(eps, { classificationMap, dormancy }); return { byClassification: a.disciplineByClassification, routerStep: a.routerStep, boundariesRate: a.boundariesRate, }; } catch (err) { console.warn('[status-md-generator] discipline calc skipped:', err.message); return null; } })(), }; const eps = loadCurrentMonthEpisodes(); let costBlock = null, anomalyBlock = null, selfRetrospectBlock = null, reviewerBlock = null, reviewerFindingsBlock = null, sessionLengthBlock = null, overrideUsageBlock = null; try { costBlock = computeCostBlock(eps, PRICING); } catch (err) { console.warn('[status-md-generator] costBlock skipped:', err.message); costBlock = '(нет данных)'; } try { anomalyBlock = computeAnomalyBlock(eps); } catch (err) { console.warn('[status-md-generator] anomalyBlock skipped:', err.message); anomalyBlock = '(нет данных)'; } try { selfRetrospectBlock = computeSelfRetrospectBlock(join(STATE_DIR,'.self-retrospect-counter.json')); } catch (err) { console.warn('[status-md-generator] selfRetrospectBlock skipped:', err.message); selfRetrospectBlock = '(нет данных)'; } try { reviewerBlock = computeReviewerBlock(eps); } catch (err) { console.warn('[status-md-generator] reviewerBlock skipped:', err.message); reviewerBlock = '(нет данных)'; } try { reviewerFindingsBlock = computeReviewerFindingsBlock(eps); } catch (err) { console.warn('[status-md-generator] reviewerFindingsBlock skipped:', err.message); reviewerFindingsBlock = '(нет данных)'; } try { sessionLengthBlock = computeSessionLengthBlock(eps); } catch (err) { console.warn('[status-md-generator] sessionLengthBlock skipped:', err.message); sessionLengthBlock = '(нет данных)'; } try { const logPath = join(homedir(), '.claude', 'runtime', 'override-usage.jsonl'); const raw = existsSync(logPath) ? readFileSync(logPath, 'utf-8') : ''; overrideUsageBlock = computeOverrideUsageBlock(raw); } catch (err) { console.warn('[status-md-generator] overrideUsageBlock skipped:', err.message); overrideUsageBlock = '(нет данных)'; } inputs.costBlock = costBlock; inputs.anomalyBlock = anomalyBlock; inputs.selfRetrospectBlock = selfRetrospectBlock; inputs.reviewerBlock = reviewerBlock; inputs.reviewerFindingsBlock = reviewerFindingsBlock; inputs.sessionLengthBlock = sessionLengthBlock; inputs.overrideUsageBlock = overrideUsageBlock; let systemHealthBlock = null; try { systemHealthBlock = computeSystemHealthBlock(queryRunningProcesses()); } catch (err) { console.warn('[status-md-generator] systemHealthBlock skipped:', err.message); systemHealthBlock = null; } inputs.systemHealthBlock = systemHealthBlock; let learningQueueBlock = null; try { const qPath = join(homedir(), '.claude', 'runtime', 'router-learning-queue.json'); const queue = existsSync(qPath) ? loadQueue({ path: qPath }) : []; learningQueueBlock = computeLearningQueueBlock({ queue }); } catch (err) { console.warn('[status-md-generator] learningQueueBlock skipped:', err.message); learningQueueBlock = null; } inputs.learningQueueBlock = learningQueueBlock; let doorCoverageBlock = null; try { let settings = {}; try { settings = JSON.parse(readFileSync('.claude/settings.json', 'utf-8')); } catch { settings = {}; } doorCoverageBlock = computeDoorCoverageBlock({ settings }); } catch (err) { console.warn('[status-md-generator] doorCoverageBlock skipped:', err.message); doorCoverageBlock = null; } inputs.doorCoverageBlock = doorCoverageBlock; let journalIntegrityBlock = null; try { const runtimeDir = join(homedir(), '.claude', 'runtime'); const key = resolveReceiptKey(); if (key === null || key === undefined) { journalIntegrityBlock = computeJournalIntegrityBlock({ keyAvailable: false }); } else { const files = existsSync(runtimeDir) ? readdirSync(runtimeDir) : []; const results = []; for (const f of files) { const m = /^action-journal-(.+)\.jsonl$/.exec(f); if (!m) continue; const sessionId = m[1]; try { const { entries, headSig } = loadJournal({ sessionId, runtimeDir }); const v = verifyChain(entries, headSig, { key }); results.push({ sessionId, ok: v.ok, brokenAt: v.brokenAt }); } catch { results.push({ sessionId, ok: false, brokenAt: null }); } } journalIntegrityBlock = computeJournalIntegrityBlock({ results, keyAvailable: true }); } } catch (err) { console.warn('[status-md-generator] journalIntegrityBlock skipped:', err.message); journalIntegrityBlock = null; } inputs.journalIntegrityBlock = journalIntegrityBlock; // §7 доска «кто на посту»: манифест из .claude/settings.json (read-only). Судья М4 пока inert // (live-режим — Ф7 ИИ-проводка); escape/блоки — детализация follow-up (счётчики []). let guardBoardBlock = null; try { let manifestSettings = {}; try { manifestSettings = JSON.parse(readFileSync('.claude/settings.json', 'utf-8')); } catch { manifestSettings = {}; } const manifest = checkManifest({ settings: manifestSettings }); guardBoardBlock = computeGuardBoardBlock({ manifest, judgeMode: judgeGateMode(), recentEscapes: loadRecentEscapes(), recentBlocks: loadRecentBlocks() }); } catch (err) { console.warn('[status-md-generator] guardBoardBlock skipped:', err.message); guardBoardBlock = null; } inputs.guardBoardBlock = guardBoardBlock; const md = renderStatus(inputs); writeFileSync(join(STATE_DIR, 'STATUS.md'), md); console.log(`[status-md-generator] OK — wrote ${join(STATE_DIR, 'STATUS.md')}`); }