db0cde0593
Surfaces top-3 long-running processes (CPU > 1h) in STATUS.md dashboard. Closes brain-retro #9 sanity-Q2 — observer was blind to orphan background processes (e.g. PID 6444 python adr-judge spinning 7h+ undetected). Read-only PowerShell Get-Process probe with 5s timeout; gracefully degrades on non-Windows OS (returns empty array). Closes brain-retro #9 candidate 5.
535 lines
22 KiB
JavaScript
535 lines
22 KiB
JavaScript
#!/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';
|
|
|
|
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] || '⚪';
|
|
}
|
|
|
|
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 || '—'} |
|
|
|
|
## Метрики (информационные, не алерты)
|
|
|
|
- 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;
|
|
|
|
const md = renderStatus(inputs);
|
|
writeFileSync('docs/observer/STATUS.md', md);
|
|
console.log(`[status-md-generator] OK — wrote docs/observer/STATUS.md`);
|
|
}
|