#!/usr/bin/env node /** * Pure reader of router-gate v4 runtime signals, scoped to one episode's * [startMs, endMs] turn window. Feeds observer-transcript-parser (new * episode.v4_signals block + task_cost.judge_spend_usd). No exec/exit. * * Sources (all in ~/.claude/runtime/, per-session): * - rationalization-flags-.jsonl ({ts, kind, evidence}) * - llm-judge-verdicts-.jsonl ({ts, tool, verdict}) * - safe-baseline-actions-.jsonl ({ts, tool, action}) * - llm-judge-budget-.json ({calls}) */ import { readFileSync, existsSync } from 'node:fs'; import { join } from 'node:path'; import { homedir } from 'node:os'; function rtDir(override) { return override || join(homedir(), '.claude', 'runtime'); } function readJsonl(path) { if (!existsSync(path)) return []; try { return readFileSync(path, 'utf-8').split('\n').filter(Boolean).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean); } catch { return []; } } function inWindow(rec, startMs, endMs) { const ms = Date.parse(rec && rec.ts); return Number.isFinite(ms) && ms >= startMs && ms <= endMs; } // Severity order: hard_block > soft_flag > allow. const ACTION_RANK = { allow: 0, soft_flag: 1, hard_block: 2 }; function worstAction(records) { let best = null; for (const r of records) { const a = r && r.action; if (a == null) continue; if (best === null || (ACTION_RANK[a] ?? -1) > (ACTION_RANK[best] ?? -1)) best = a; } return best; } export function extractV4Signals(sessionId, { startMs, endMs, baseDir } = {}) { const dir = rtDir(baseDir); const sess = sessionId || 'unknown'; const flags = readJsonl(join(dir, `rationalization-flags-${sess}.jsonl`)) .filter((r) => inWindow(r, startMs, endMs)); const verdicts = readJsonl(join(dir, `llm-judge-verdicts-${sess}.jsonl`)) .filter((r) => inWindow(r, startMs, endMs)); const judge_verdict = verdicts.length ? (verdicts[verdicts.length - 1].verdict ?? null) : null; const actions = readJsonl(join(dir, `safe-baseline-actions-${sess}.jsonl`)) .filter((r) => inWindow(r, startMs, endMs)); const safe_baseline_action = worstAction(actions); let judge_calls = 0; const bp = join(dir, `llm-judge-budget-${sess}.json`); if (existsSync(bp)) { try { judge_calls = Number(JSON.parse(readFileSync(bp, 'utf-8')).calls) || 0; } catch { /* keep 0 */ } } return { rationalization_flag_count: flags.length, judge_verdict, safe_baseline_action, judge_calls, }; }