Files
portal/tools/status-md-generator.mjs
T
Дмитрий fc4ee35021 feat(m7-phase7): доска Ф6 — judgeMode ← judgeGateMode() (реальный режим судьи: inert до активации)
CLI status-md-generator передаёт в доску «кто на посту» реальный режим судьи через
judgeGateMode() вместо хардкода 'inert'. До активации владельцем (флаг+ключ) → 'inert'
(видимое поведение не меняется); после активации доска покажет shadow/live-block.
Структурный тест фиксирует проводку (импорт + judgeMode: judgeGateMode(), не хардкод). 51/51 GREEN.
2026-06-08 14:46:12 +03:00

593 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
import { readFileSync, writeFileSync, existsSync } 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';
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/блоков. Чистая,
* ничего не блокирует.
*/
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;
return `## Кто на посту (оборона М1–М6)
${post}
Судья М4: **${judgeMode}** (inert $0 / shadow / floor-only / live-block)
| Машина / страж | Хук | Зарегистрирован |
|---|---|---|
${rows || '| (нет требуемых хуков) | — | — |'}
Недавние escape владельца: ${esc} · Недавние блоки: ${blk}
`;
}
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 c6 = inputs.c6 || { status: 'ok', detail: '—' };
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 || '—'} |
| C6 Chain map sync | ${iconFor(c6.status)} | ${c6.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` : ''}
## Алерт-индикаторы
✅ — норма ・ ⚠️ — внимание ・ 🔴 — действие требуется ・ ⚪ — не запускалось
`;
}
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;
}
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; }
}
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; }
}
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;
}
/** 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;
}
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;
}
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(),
activeProjects: existsSync('docs/observer/active-projects.md')
? readFileSync('docs/observer/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('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 = '(нет данных)'; }
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;
// §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: [], recentBlocks: [] });
} catch (err) { console.warn('[status-md-generator] guardBoardBlock skipped:', err.message); guardBoardBlock = null; }
inputs.guardBoardBlock = guardBoardBlock;
const md = renderStatus(inputs);
writeFileSync('docs/observer/STATUS.md', md);
console.log(`[status-md-generator] OK — wrote docs/observer/STATUS.md`);
}