// lib/stats/event-emit.mjs // Atomic JSONL append for autonomy-lifecycle events (plan-v2 Step 6). // // Writes one line per event to ${CLAUDE_PLUGIN_DATA}/ultraexecute-stats.jsonl // (or override via CLAUDE_PLUGIN_DATA env var; falls back to silent skip if // the directory doesn't exist — stats failures must NEVER block workflow). // // Every emission carries: // - ts : ISO-8601 timestamp (REQUIRED per SC4 contract) // - event : the requested event name // - known_event : true for recognized events, false otherwise // - payload : caller-supplied object (may be {}) // // Recognized events: brief-approved, main-merge-gate, user_input. // Unknown event names are still emitted (with known_event: false) so that // the audit trail is complete; downstream consumers filter as needed. // // CLI shim: // node lib/stats/event-emit.mjs --event brief-approved --payload '{...}' // → exit 0 (always); silent on stat dir absence. import { appendFileSync, existsSync, mkdirSync } from 'node:fs'; import { dirname, join } from 'node:path'; export const KNOWN_EVENTS = Object.freeze(new Set([ 'brief-approved', 'main-merge-gate', 'user_input', ])); const STATS_FILENAME = 'ultraexecute-stats.jsonl'; /** * Resolve the stats file path. Honors CLAUDE_PLUGIN_DATA env var. * Returns null if no plugin-data dir is configured (silent-skip mode). */ export function resolveStatsPath(env = process.env) { const dir = env.CLAUDE_PLUGIN_DATA; if (!dir || typeof dir !== 'string' || dir.length === 0) return null; return join(dir, STATS_FILENAME); } /** * Build the JSON record. Pure — no I/O. */ export function buildRecord(event, payload = {}, now = new Date()) { if (typeof event !== 'string' || event.length === 0) { throw new TypeError('event must be a non-empty string'); } return { ts: now.toISOString(), event, known_event: KNOWN_EVENTS.has(event), payload: (payload && typeof payload === 'object') ? payload : {}, }; } /** * Emit an event. Never throws — stat failures are swallowed silently * because lifecycle telemetry must not block the user's workflow. * * @returns {{ written: boolean, path: string | null, reason?: string }} */ export function emit(event, payload = {}, opts = {}) { const env = opts.env || process.env; const now = opts.now || new Date(); let record; try { record = buildRecord(event, payload, now); } catch (e) { return { written: false, path: null, reason: `record-build: ${e.message}` }; } const path = opts.path || resolveStatsPath(env); if (!path) return { written: false, path: null, reason: 'CLAUDE_PLUGIN_DATA unset' }; try { const dir = dirname(path); if (!existsSync(dir)) { // Best-effort dir creation; if it fails, swallow and skip. try { mkdirSync(dir, { recursive: true }); } catch { return { written: false, path, reason: 'dir-mkdir-failed' }; } } appendFileSync(path, JSON.stringify(record) + '\n'); return { written: true, path }; } catch (e) { return { written: false, path, reason: `append-failed: ${e.message}` }; } } // ---- CLI shim ---------------------------------------------------------------- function parseArgs(argv) { const out = {}; for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (a === '--event') out.event = argv[++i]; else if (a === '--payload') out.payload = argv[++i]; } return out; } if (import.meta.url === `file://${process.argv[1]}`) { const args = parseArgs(process.argv.slice(2)); if (!args.event) { process.stdout.write(JSON.stringify({ written: false, reason: 'usage: --event NAME [--payload JSON]' }) + '\n'); process.exit(0); // never block: usage error still exits clean } let payload = {}; if (args.payload) { try { payload = JSON.parse(args.payload); } catch { process.stdout.write(JSON.stringify({ written: false, reason: 'payload-not-json' }) + '\n'); process.exit(0); } } const result = emit(args.event, payload); process.stdout.write(JSON.stringify(result) + '\n'); process.exit(0); }