#!/usr/bin/env node /** * C5 observer-coverage-checker (brain governance, observer factor-analysis * spec §5.2). Warn-only — always exits 0. Two checks: * 1. Coverage — recent git commits but 0 observer episodes this month. * 2. Registration integrity — observer Stop-hook present in * .claude/settings.json and .git/hooks/post-commit installed. * Findings are surfaced in docs/observer/STATUS.md (C4 generator); this * controller never blocks a commit. * * Security Guidance #40: git is invoked via execFileSync (argument array, * no shell) — no exec/execSync. */ import { readFileSync, existsSync } from 'fs'; import { join } from 'path'; import { execFileSync } from 'child_process'; const RECENT_WINDOW = '14 days ago'; /** @returns {{ok: boolean, detail: string}} */ export function checkCoverage(episodeCount, recentCommitCount) { if (recentCommitCount > 0 && episodeCount === 0) { return { ok: false, detail: `${recentCommitCount} commit(s) in the last 2 weeks but 0 observer episodes this month`, }; } return { ok: true, detail: `${episodeCount} episode(s), ${recentCommitCount} recent commit(s)` }; } /** @returns {{ok: boolean, detail: string}} */ export function checkRegistration(settingsJson, postCommitExists) { const problems = []; const stopHooks = (((settingsJson || {}).hooks || {}).Stop) || []; const hasObserverStop = stopHooks.some((entry) => ((entry && entry.hooks) || []).some((h) => String((h && h.command) || '').includes('observer-stop-hook')) ); if (!hasObserverStop) { problems.push('observer-stop-hook NOT registered in .claude/settings.json Stop hook'); } if (!postCommitExists) { problems.push('.git/hooks/post-commit not installed (run: npx lefthook install --force)'); } return { ok: problems.length === 0, detail: problems.length ? problems.join('; ') : 'Stop-hook + post-commit OK', }; } function countEpisodes(root) { const month = new Date().toISOString().slice(0, 7); const file = join(root, 'docs', 'observer', `episodes-${month}.jsonl`); if (!existsSync(file)) return 0; return readFileSync(file, 'utf-8').trim().split('\n').filter(Boolean).length; } function countRecentCommits(root) { try { const out = execFileSync('git', ['log', `--since=${RECENT_WINDOW}`, '--oneline'], { cwd: root, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], }); return out.trim() ? out.trim().split('\n').length : 0; } catch { return 0; } } export function runCoverageChecker(root = process.cwd()) { const coverage = checkCoverage(countEpisodes(root), countRecentCommits(root)); let settings = {}; try { settings = JSON.parse(readFileSync(join(root, '.claude', 'settings.json'), 'utf-8')); } catch { settings = {}; } const registration = checkRegistration(settings, existsSync(join(root, '.git', 'hooks', 'post-commit'))); return { coverage, registration }; } if (process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/observer-coverage-checker.mjs')) { const { coverage, registration } = runCoverageChecker(); if (!coverage.ok) console.warn(`[observer-coverage-checker] WARN — coverage: ${coverage.detail}`); if (!registration.ok) console.warn(`[observer-coverage-checker] WARN — registration: ${registration.detail}`); if (coverage.ok && registration.ok) { console.log(`[observer-coverage-checker] OK — ${coverage.detail}; ${registration.detail}`); } process.exit(0); // warn-only — never blocks a commit }