397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
75 lines
2.5 KiB
JavaScript
75 lines
2.5 KiB
JavaScript
#!/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-<sess>.jsonl ({ts, kind, evidence})
|
|
* - llm-judge-verdicts-<sess>.jsonl ({ts, tool, verdict})
|
|
* - safe-baseline-actions-<sess>.jsonl ({ts, tool, action})
|
|
* - llm-judge-budget-<sess>.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,
|
|
};
|
|
}
|