sonnet-overseer.js
1 /** 2 * Sonnet Overseer — DEPRECATED as standalone LLM cron job. 3 * 4 * LLM analysis is now handled by the Claude orchestrator: 5 * scripts/claude-orchestrator.sh --type oversee 6 * 7 * This file is retained only because src/cron.js references runSonnetOverseer(). 8 * The cron registration in cron.js should be removed — this stub logs a warning 9 * and exits cleanly so nothing breaks in the meantime. 10 * 11 * Data collection functions are kept here as they may be used by other modules. 12 */ 13 14 import { getAll } from '../utils/db.js'; 15 import { execSync } from 'child_process'; 16 import { appendFileSync, readFileSync, existsSync } from 'fs'; 17 import { join, dirname } from 'path'; 18 import { fileURLToPath } from 'url'; 19 import '../utils/load-env.js'; 20 21 const __filename = fileURLToPath(import.meta.url); 22 const __dirname = dirname(__filename); 23 const PROJECT_ROOT = join(__dirname, '../..'); 24 25 const LOG_FILE = '/tmp/sonnet-overseer.log'; 26 const STATUS_FILE = join(PROJECT_ROOT, 'logs/pipeline-status.txt'); 27 28 function log(msg) { 29 const ts = new Date().toISOString(); 30 const line = `[${ts}] [SonnetOverseer] ${msg}`; 31 console.log(line); 32 try { 33 appendFileSync(LOG_FILE, `${line}\n`); 34 } catch { 35 /* ignore */ 36 } 37 } 38 39 function statusLog(section) { 40 try { 41 appendFileSync(STATUS_FILE, `${section}\n`); 42 } catch { 43 /* ignore */ 44 } 45 } 46 47 // ─── Data collection (kept for potential direct use) ───────────────────────── 48 49 export function collectServiceStatus() { 50 const services = ['333method-pipeline', 'mmo-cron.timer', '333method-dashboard']; 51 const results = {}; 52 for (const svc of services) { 53 try { 54 const status = execSync(`systemctl --user is-active ${svc}`, { 55 encoding: 'utf8', 56 timeout: 8000, 57 }).trim(); 58 results[svc] = status; 59 } catch (err) { 60 results[svc] = (err.stdout || '').trim() || 'inactive'; 61 } 62 } 63 return results; 64 } 65 66 export function collectRecentErrors() { 67 const today = new Date().toISOString().slice(0, 10); 68 const logFile = join(PROJECT_ROOT, `logs/pipeline-${today}.log`); 69 if (!existsSync(logFile)) return []; 70 71 try { 72 const cutoff = new Date(Date.now() - 30 * 60 * 1000); 73 const raw = readFileSync(logFile, 'utf8'); 74 const lines = raw.split('\n'); 75 const errors = []; 76 const seen = new Map(); 77 78 for (const line of lines.slice(-400)) { 79 if (!line.includes('[ERROR]') && !line.includes('[WARN]')) continue; 80 81 const tsMatch = line.match(/^(\[[\d\-T:.+Z]+\])/); 82 if (tsMatch) { 83 try { 84 const lineTs = new Date(tsMatch[1].slice(1, -1)); 85 if (lineTs < cutoff) continue; 86 } catch { 87 /* keep */ 88 } 89 } 90 91 const trimmed = line.slice(0, 200); 92 const key = trimmed.slice(0, 80); 93 const count = (seen.get(key) || 0) + 1; 94 seen.set(key, count); 95 if (count === 1) errors.push(trimmed); 96 else if (count <= 3) errors[errors.length - 1] = `${trimmed} (×${count})`; 97 } 98 99 return errors.slice(-30); 100 } catch { 101 return []; 102 } 103 } 104 105 // ─── Main (stub — LLM work moved to orchestrator) ──────────────────────────── 106 107 export async function runSonnetOverseer() { 108 log( 109 'NOTICE: runSonnetOverseer() is deprecated — LLM analysis now runs via claude-orchestrator.sh --type oversee. Remove this cron job from cron.js.' 110 ); 111 statusLog( 112 `[${new Date().toISOString()}] [SonnetOverseer] NOTICE: Cron stub — LLM analysis delegated to orchestrator` 113 ); 114 115 // Log a basic data snapshot so the status file still has useful info 116 try { 117 const services = collectServiceStatus(); 118 const errors = collectRecentErrors(); 119 const dist = await getAll( 120 `SELECT status, COUNT(*) as n FROM sites GROUP BY status ORDER BY n DESC LIMIT 5` 121 ); 122 statusLog( 123 `Services: ${JSON.stringify(services)} | Errors(30m): ${errors.length} | Sites: ${dist.map(r => `${r.status}=${r.n}`).join(', ')}` 124 ); 125 } catch { 126 /* non-fatal — deprecated stub */ 127 } 128 129 return { summary: 'Delegated to orchestrator', severity: 'ok', actions_taken: 0 }; 130 } 131 132 // Allow direct invocation (prints deprecation notice) 133 if (process.argv[1] === fileURLToPath(import.meta.url)) { 134 runSonnetOverseer() 135 .then(result => { 136 console.log('\nResult:', JSON.stringify(result, null, 2)); 137 process.exit(0); 138 }) 139 .catch(err => { 140 console.error('Fatal:', err); 141 process.exit(1); 142 }); 143 }