2026-05-19 06:37:27 +03:00
#!/usr/bin/env node
2026-06-09 04:06:53 +03:00
import { readFileSync , writeFileSync , existsSync , readdirSync } from 'fs' ;
2026-05-19 06:37:27 +03:00
import { join } from 'path' ;
import { execFileSync } from 'child_process' ;
2026-05-26 11:16:16 +03:00
import { homedir } from 'os' ;
2026-05-19 10:41:17 +03:00
import { runCoverageChecker } from './observer-coverage-checker.mjs' ;
2026-05-24 07:03:41 +03:00
import { analyze } from './brain-retro-analyzer.mjs' ;
import { loadRegistry } from './registry-load.mjs' ;
import { buildClassificationMap , buildDormancyMap } from './registry-to-classification-map.mjs' ;
2026-05-26 11:16:16 +03:00
import { computeOverrideUsageBlock } from './enforce-override-monitor.mjs' ;
2026-05-28 10:19:24 +03:00
import { queryRunningProcesses , computeSystemHealthBlock } from './system-health.mjs' ;
2026-06-08 14:28:56 +03:00
import { checkManifest } from './floor-manifest-check.mjs' ;
2026-06-08 14:46:12 +03:00
import { judgeGateMode } from './judge-gate-config.mjs' ;
2026-06-09 03:59:32 +03:00
import { loadQueue , statusSignal , pendingCount } from './router-learning-queue.mjs' ;
2026-06-09 04:05:15 +03:00
import { auditDoors , auditExempt , extractGateMatcher , CANONICAL _MUTATING _TOOLS , isMutatingTool } from './door-coverage.mjs' ;
import { SEED _TOOLS } from './enforce-supreme-gate.mjs' ;
2026-06-09 04:06:53 +03:00
import { loadJournal , verifyChain } from './action-journal.mjs' ;
import { resolveReceiptKey } from './receipt-key-config.mjs' ;
2026-06-10 04:28:53 +03:00
import { loadRecentBlocks , loadRecentEscapes } from './guard-block-log.mjs' ;
2026-05-19 06:37:27 +03:00
2026-05-25 13:59:40 +03:00
const PRICING = {
sonnet46 : { input _per _mtok : 3.0 , output _per _mtok : 15.0 } ,
opus47 : { input _per _mtok : 15.0 , output _per _mtok : 75.0 } ,
} ;
2026-05-19 06:37:27 +03:00
function iconFor ( status ) {
return { ok : '✅' , warn : '⚠️' , fail : '🔴' } [ status ] || '⚪' ;
}
2026-06-08 14:28:56 +03:00
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/блоков. Чистая,
* ничего не блокирует.
*/
2026-06-09 03:21:29 +03:00
/** Экранировать значение для одной ячейки markdown-таблицы (anti-injection, sharp-edges SE-3). */
function escapeCell ( v ) {
return String ( v ? ? '' ) . replace ( /\r?\n/g , ' ' ) . replace ( /\|/g , '\\|' ) . slice ( 0 , 120 ) ;
}
2026-06-08 14:28:56 +03:00
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 ;
2026-06-09 03:21:29 +03:00
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 , 'Недавние блоки (детали)' ) ;
2026-06-08 14:28:56 +03:00
return ` ## Кто на посту (оборона М1–М6)
${ post }
Судья М 4: ** ${ judgeMode } ** (inert $ 0 / shadow / floor-only / live-block)
| Машина / страж | Хук | Зарегистрирован |
|---|---|---|
${ rows || '| (нет требуемых хуков) | — | — |' }
Недавние escape владельца: ${ esc } · Недавние блоки: ${ blk }
2026-06-09 03:21:29 +03:00
${ detail } ` ;
2026-06-08 14:28:56 +03:00
}
2026-06-09 03:59:32 +03:00
/**
* 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 } ` ;
}
2026-06-09 04:05:15 +03:00
/**
* 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' ) ;
}
2026-06-09 04:06:53 +03:00
/**
* 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 } ` ;
}
2026-05-25 13:59:40 +03:00
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
` ;
}
}
2026-05-26 10:52:35 +03:00
/**
* 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 просел в текущей сессии — рассмотри перезапуск. ` ;
}
2026-05-25 13:59:40 +03:00
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 }
` ;
}
2026-05-27 06:46:55 +03:00
/**
* 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 }
` ;
}
2026-05-19 06:37:27 +03:00
export function renderStatus ( inputs ) {
2026-05-24 07:03:41 +03:00
const { now , c1 , c2 , c3 , c5 , observer , lastRetroDaysAgo , discipline } = inputs ;
2026-05-21 04:54:00 +03:00
const c6 = inputs . c6 || { status : 'ok' , detail : '—' } ;
2026-05-21 08:38:21 +03:00
const missed = inputs . missed || { totalMissed : 0 , byNode : { } , byClassification : { } } ;
2026-05-24 07:03:41 +03:00
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 } ).
` ;
}
2026-05-23 19:39:29 +03:00
const activeProjects = ( inputs . activeProjects || '' ) . trim ( ) ;
const projectsBlock = activeProjects
? ` \n ## Активные многоэтапные проекты \n \n ${ activeProjects } \n `
: '' ;
2026-05-20 13:19:22 +03:00
const retroLine = ( lastRetroDaysAgo === null || lastRetroDaysAgo === undefined )
? 'never'
: ` ${ lastRetroDaysAgo } day(s) ago ` ;
2026-05-19 06:37:27 +03:00
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) |
2026-05-19 10:41:17 +03:00
| C5 Observer-coverage | ${ iconFor ( c5 . status ) } | ${ c5 . detail || '—' } |
2026-05-21 04:54:00 +03:00
| C6 Chain map sync | ${ iconFor ( c6 . status ) } | ${ c6 . detail || '—' } |
2026-06-08 14:28:56 +03:00
${ inputs . guardBoardBlock ? ` \n ${ inputs . guardBoardBlock . trim ( ) } \n ` : '' }
2026-05-19 06:37:27 +03:00
## Метрики (информационные, не алерты)
2026-05-19 10:41:17 +03:00
- Observer evidence: ${ observer . episodeCount } episodes this month, ${ observer . observerErrors } observer_error markers, ${ observer . piiMatches } PII matches before filter
2026-05-20 13:26:29 +03:00
- Legacy v1 episodes (not in factor analysis): ${ observer . v1Episodes || 0 }
2026-05-20 13:19:22 +03:00
- Last /brain-retro: ${ retroLine }
2026-05-21 08:38:21 +03:00
- Использование узлов: см. \` /brain-retro \` (раз в спринт). missed_activations: ${ missed . totalMissed } . **Неиспользованные узлы — не алерт, если профильной задачи не было** (Pravila §16.4 v1.36; capability-readiness; см. memory \` feedback_brain_unused_tools_not_problem \` — outside-repo memory store).
2026-06-09 04:06:53 +03:00
${ 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 ` : '' }
2026-05-19 06:37:27 +03:00
## Алерт-индикаторы
✅ — норма ・ ⚠️ — внимание ・ 🔴 — действие требуется ・ ⚪ — не запускалось
` ;
}
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 ;
}
2026-05-20 12:51:28 +03:00
function countPiiMatches ( ) {
const file = join ( 'docs' , 'observer' , '.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 ; }
}
2026-05-20 13:19:22 +03:00
function lastRetroDaysAgo ( ) {
const file = join ( 'docs' , 'observer' , '.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 ; }
}
2026-05-19 10:41:17 +03:00
function countObserverErrors ( ) {
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 ( ( l ) => l . includes ( '"observer_error":true' ) ) . length ;
}
2026-05-20 13:26:29 +03:00
/** Legacy v1 episode count — lines without schema_version=2, not observer_error markers. */
function countV1Episodes ( ) {
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 ( ( l ) => l && ! l . includes ( '"schema_version":2' ) && ! l . includes ( '"observer_error":true' ) ) . length ;
}
2026-05-24 07:03:41 +03:00
function loadCurrentMonthEpisodes ( ) {
const month = new Date ( ) . toISOString ( ) . slice ( 0 , 7 ) ;
const file = join ( '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 ;
}
2026-05-19 06:37:27 +03:00
if ( process . argv [ 1 ] && process . argv [ 1 ] . replace ( /\\/g , '/' ) . endsWith ( '/status-md-generator.mjs' ) ) {
2026-05-19 10:41:17 +03:00
const cov = runCoverageChecker ( ) ;
2026-05-21 08:38:21 +03:00
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 ( ' · ' ) ;
2026-05-19 06:37:27 +03:00
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' ] ) ,
2026-05-21 08:38:21 +03:00
c5 : { status : c5ok ? 'ok' : 'warn' , detail : c5detail } ,
2026-05-21 04:54:00 +03:00
c6 : runControllerNode ( [ 'tools/observer-chain-map-checker.mjs' ] ) ,
2026-05-19 10:41:17 +03:00
observer : {
episodeCount : countEpisodes ( ) ,
observerErrors : countObserverErrors ( ) ,
2026-05-20 12:51:28 +03:00
piiMatches : countPiiMatches ( ) ,
2026-05-20 13:26:29 +03:00
v1Episodes : countV1Episodes ( ) ,
2026-05-19 10:41:17 +03:00
} ,
2026-05-21 08:38:21 +03:00
missed : cov . missed ,
2026-05-20 13:19:22 +03:00
lastRetroDaysAgo : lastRetroDaysAgo ( ) ,
2026-05-23 19:39:29 +03:00
activeProjects : existsSync ( 'docs/observer/active-projects.md' )
? readFileSync ( 'docs/observer/active-projects.md' , 'utf-8' )
: '' ,
2026-05-24 07:03:41 +03:00
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 ;
}
} ) ( ) ,
2026-05-19 06:37:27 +03:00
} ;
2026-05-25 13:59:40 +03:00
const eps = loadCurrentMonthEpisodes ( ) ;
2026-05-27 06:46:55 +03:00
let costBlock = null , anomalyBlock = null , selfRetrospectBlock = null , reviewerBlock = null , reviewerFindingsBlock = null , sessionLengthBlock = null , overrideUsageBlock = null ;
2026-05-25 13:59:40 +03:00
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 ( 'docs' , 'observer' , '.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 = '(нет данных)' ; }
2026-05-27 06:46:55 +03:00
try { reviewerFindingsBlock = computeReviewerFindingsBlock ( eps ) ; } catch ( err ) { console . warn ( '[status-md-generator] reviewerFindingsBlock skipped:' , err . message ) ; reviewerFindingsBlock = '(нет данных)' ; }
2026-05-26 10:52:35 +03:00
try { sessionLengthBlock = computeSessionLengthBlock ( eps ) ; } catch ( err ) { console . warn ( '[status-md-generator] sessionLengthBlock skipped:' , err . message ) ; sessionLengthBlock = '(нет данных)' ; }
2026-05-26 11:16:16 +03:00
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 = '(нет данных)' ; }
2026-05-25 13:59:40 +03:00
inputs . costBlock = costBlock ;
inputs . anomalyBlock = anomalyBlock ;
inputs . selfRetrospectBlock = selfRetrospectBlock ;
inputs . reviewerBlock = reviewerBlock ;
2026-05-27 06:46:55 +03:00
inputs . reviewerFindingsBlock = reviewerFindingsBlock ;
2026-05-26 10:52:35 +03:00
inputs . sessionLengthBlock = sessionLengthBlock ;
2026-05-26 11:48:16 +03:00
inputs . overrideUsageBlock = overrideUsageBlock ;
2026-05-25 13:59:40 +03:00
2026-05-28 10:19:24 +03:00
let systemHealthBlock = null ;
try { systemHealthBlock = computeSystemHealthBlock ( queryRunningProcesses ( ) ) ; } catch ( err ) { console . warn ( '[status-md-generator] systemHealthBlock skipped:' , err . message ) ; systemHealthBlock = null ; }
inputs . systemHealthBlock = systemHealthBlock ;
2026-06-09 03:59:32 +03:00
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 ;
2026-06-09 04:05:15 +03:00
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 ;
2026-06-09 04:06:53 +03:00
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 ;
2026-06-08 14:28:56 +03:00
// §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 } ) ;
2026-06-10 04:28:53 +03:00
guardBoardBlock = computeGuardBoardBlock ( { manifest , judgeMode : judgeGateMode ( ) , recentEscapes : loadRecentEscapes ( ) , recentBlocks : loadRecentBlocks ( ) } ) ;
2026-06-08 14:28:56 +03:00
} catch ( err ) { console . warn ( '[status-md-generator] guardBoardBlock skipped:' , err . message ) ; guardBoardBlock = null ; }
inputs . guardBoardBlock = guardBoardBlock ;
2026-05-19 06:37:27 +03:00
const md = renderStatus ( inputs ) ;
writeFileSync ( 'docs/observer/STATUS.md' , md ) ;
console . log ( ` [status-md-generator] OK — wrote docs/observer/STATUS.md ` ) ;
}