#!/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 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` : ''}${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() };
  }
}

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;

  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('docs/observer/STATUS.md', md);
  console.log(`[status-md-generator] OK — wrote docs/observer/STATUS.md`);
}
