← back to Dw Yolo Loop

scripts/canary-meta-watchdog/canary-meta-watchdog.js

135 lines

#!/usr/bin/env node
/**
 * Canary Meta-Watchdog — proves the canaries themselves actually RAN.
 *
 * The fleet's worst failures are silent monitoring deaths: the dw_unified
 * pg_dump wrote 0 bytes for 12 days unnoticed, the Kamatera health-watchdog
 * went down, and ≥6 launchd jobs are failing — each a case of "the thing that
 * should have warned us is the thing that broke." This meta-watchdog reads
 * every com.steve.* LaunchAgent, derives its expected cadence, and FAILs
 * (report-only) if a job is unloaded, last-exited non-zero, or hasn't produced
 * fresh log output within its expected interval (× a grace factor).
 *
 * READ-ONLY: parses plists + stats log files + reads `launchctl print`. Writes
 * only a local report JSON. No prod writes, no network. Cost: $0 (local).
 *
 *   node canary-meta-watchdog.js            # human table + write report JSON
 *   node canary-meta-watchdog.js --json     # machine JSON to stdout
 */
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

const HOME = process.env.HOME;
const LA_DIR = path.join(HOME, 'Library/LaunchAgents');
const UID = parseInt(execSync('id -u').toString().trim(), 10);
const GRACE = 1.5; // allow 1.5× the expected interval before flagging stale
const OUT = path.join(HOME, '.claude/yolo-queue', `canary-meta-watchdog-${new Date().toISOString().slice(0,10)}.json`);
const JSON_ONLY = process.argv.includes('--json');

const now = Date.now();
const sec = (ms) => ms / 1000;

function plistToObj(file) {
  try { return JSON.parse(execSync(`plutil -convert json -o - ${JSON.stringify(file)}`, { stdio: ['ignore','pipe','ignore'] }).toString()); }
  catch (e) { return null; }
}

// Expected period in seconds from StartInterval or StartCalendarInterval.
function expectedPeriodSec(p) {
  if (typeof p.StartInterval === 'number') return p.StartInterval;
  const sci = p.StartCalendarInterval;
  if (sci) {
    const entries = Array.isArray(sci) ? sci : [sci];
    // crude cadence inference: Minute-only => hourly; Hour set => daily; Weekday set => weekly; Day set => monthly
    const e = entries[0] || {};
    if ('Weekday' in e) return 7 * 86400;
    if ('Day' in e) return 30 * 86400;
    if ('Hour' in e) return 86400;            // runs at a fixed hour daily
    if ('Minute' in e) return 3600;           // runs each hour at a minute
    return 86400;
  }
  return null; // RunAtLoad-only / on-demand — no cadence to check
}

// Last-run proxy = newest mtime among the job's stdout/stderr log paths.
function lastRunMs(p) {
  const paths = [p.StandardOutPath, p.StandardErrorPath].filter(Boolean);
  let newest = 0;
  for (const f of paths) {
    try { const st = fs.statSync(f); newest = Math.max(newest, st.mtimeMs); } catch (_) {}
  }
  return newest || null;
}

// launchctl state: loaded? last exit code? pid?
function launchctlState(label) {
  try {
    const out = execSync(`launchctl print gui/${UID}/${label} 2>/dev/null`, { stdio: ['ignore','pipe','ignore'] }).toString();
    const pid = (out.match(/pid = (\d+)/) || [])[1] || null;
    const lastExit = (out.match(/last exit code = (\d+)/) || [])[1];
    return { loaded: true, pid, lastExit: lastExit !== undefined ? parseInt(lastExit, 10) : null };
  } catch (e) {
    // fall back to `launchctl list` membership
    try {
      const line = execSync(`launchctl list | grep ${JSON.stringify(label)} 2>/dev/null`, { stdio: ['ignore','pipe','ignore'] }).toString().trim();
      if (!line) return { loaded: false, pid: null, lastExit: null };
      const cols = line.split(/\s+/);
      return { loaded: true, pid: cols[0] === '-' ? null : cols[0], lastExit: cols[1] === '-' ? null : parseInt(cols[1], 10) };
    } catch (_) { return { loaded: false, pid: null, lastExit: null }; }
  }
}

const files = fs.readdirSync(LA_DIR).filter(f => /^com\.steve\..*\.plist$/.test(f));
const results = [];

for (const f of files) {
  const full = path.join(LA_DIR, f);
  const p = plistToObj(full);
  if (!p || !p.Label) { results.push({ label: f.replace(/\.plist$/, ''), verdict: 'WARN', why: 'unparseable plist' }); continue; }
  const label = p.Label;
  const period = expectedPeriodSec(p);
  const last = lastRunMs(p);
  const st = launchctlState(label);
  const ageSec = last ? sec(now - last) : null;

  let verdict = 'PASS', why = [];
  if (!st.loaded) { verdict = 'FAIL'; why.push('NOT LOADED in launchd'); }
  if (st.lastExit && st.lastExit !== 0) { verdict = 'FAIL'; why.push(`last exit = ${st.lastExit}`); }
  if (period && ageSec !== null) {
    if (ageSec > period * GRACE) { verdict = 'FAIL'; why.push(`STALE: last log ${(ageSec/3600).toFixed(1)}h ago vs ${(period/3600).toFixed(1)}h cadence`); }
  } else if (period && last === null) {
    if (verdict === 'PASS') verdict = 'WARN'; why.push('no log output found (never ran or logs elsewhere)');
  } else if (!period) {
    if (verdict === 'PASS') verdict = 'INFO'; why.push('on-demand/RunAtLoad — no cadence to check');
  }
  results.push({
    label, verdict,
    cadence_h: period ? +(period/3600).toFixed(1) : null,
    last_run_age_h: ageSec !== null ? +(ageSec/3600).toFixed(1) : null,
    loaded: st.loaded, last_exit: st.lastExit, pid: st.pid,
    why: why.join('; ') || 'ok',
  });
}

const rank = { FAIL: 0, WARN: 1, INFO: 2, PASS: 3 };
results.sort((a, b) => (rank[a.verdict] - rank[b.verdict]) || a.label.localeCompare(b.label));
const summary = results.reduce((m, r) => (m[r.verdict] = (m[r.verdict]||0)+1, m), {});
const report = { generated_at: new Date().toISOString(), launch_agents_dir: LA_DIR, grace_factor: GRACE, total: results.length, summary, results };

fs.writeFileSync(OUT, JSON.stringify(report, null, 2));

if (JSON_ONLY) { console.log(JSON.stringify(report, null, 2)); process.exit(0); }

console.log(`\nCanary Meta-Watchdog — ${results.length} com.steve.* jobs  (${new Date().toISOString().slice(0,16)})`);
console.log(`Summary: ${Object.entries(summary).map(([k,v])=>`${k}=${v}`).join('  ')}`);
console.log('─'.repeat(96));
for (const r of results) {
  const tag = { FAIL:'🔴', WARN:'🟠', INFO:'⚪', PASS:'🟢' }[r.verdict];
  console.log(`${tag} ${r.verdict.padEnd(4)} ${r.label.padEnd(42)} ${r.why}`);
}
console.log('─'.repeat(96));
console.log(`Report: ${OUT}`);
const fails = results.filter(r => r.verdict === 'FAIL');
if (fails.length) console.log(`\n⚠️  ${fails.length} canary FAIL(s) — these monitors are not provably running.`);