81da2e2c45
Новый observer-verdicts: читает персистентный verdict-snapshot-<sid>.json и сводит к четырём звеньям (последний вердикт по ts на звено). Эпизод наблюдателя получил поле verdicts из снимка текущей сессии → по логам восстановимо, на каком звене план отскочил. Раньше в эпизоде был только сигнал роутера. Граница не тронута (observer-stop-hook, recommended_chain, цепочки). Хвост спеки роутера §7 (логирование решающих), эпик роутер-реестр этап 3, item 3. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
59 lines
2.7 KiB
JavaScript
59 lines
2.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* observer-verdicts — извлечение четырёх вердиктов (роутер / наставник / судья / gate3) для
|
|
* эпизода наблюдателя из персистентного снимка решений verdict-snapshot-<sid>.json (его пишет
|
|
* writeStage из verdict-surface-store). Чистые функции; любой сбой I/O — мягкий ({}/null).
|
|
* Спека роутер-реестр §7 (логирование решающих — восстановимо, на каком звене план отскочил).
|
|
*/
|
|
import { readFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { homedir } from 'node:os';
|
|
|
|
/**
|
|
* Читает снимок решений сессии и агрегирует по каждой стадии последнюю запись (max ts).
|
|
* @returns {{ [stage:string]: {status, reason, ts} }} либо {} (нет файла / сбой).
|
|
*/
|
|
export function readVerdictSnapshot(sessionId, baseDir) {
|
|
try {
|
|
const dir = baseDir || join(homedir(), '.claude', 'runtime');
|
|
const obj = JSON.parse(readFileSync(join(dir, `verdict-snapshot-${sessionId}.json`), 'utf8'));
|
|
if (!obj || typeof obj !== 'object') return {};
|
|
const latest = {};
|
|
for (const hash of Object.keys(obj)) {
|
|
const stages = obj[hash];
|
|
if (!stages || typeof stages !== 'object') continue;
|
|
for (const stage of Object.keys(stages)) {
|
|
const v = stages[stage];
|
|
if (!v || typeof v !== 'object') continue;
|
|
const ts = Number(v.ts) || 0;
|
|
if (!latest[stage] || ts >= (latest[stage].ts || 0)) {
|
|
latest[stage] = { status: v.status ?? null, reason: v.reason ?? '', ts };
|
|
}
|
|
}
|
|
}
|
|
return latest;
|
|
} catch { return {}; }
|
|
}
|
|
|
|
/**
|
|
* Сводит агрегированный снимок к четырём звеньям. Для звена с вариантами стадий берётся запись
|
|
* с наибольшим ts. Поле = {status, reason} либо null (звено не выносило вердикта).
|
|
*/
|
|
export function extractFourVerdicts(snapshot) {
|
|
const s = snapshot && typeof snapshot === 'object' ? snapshot : {};
|
|
const pick = (...stages) => {
|
|
let best = null;
|
|
for (const st of stages) {
|
|
const v = s[st];
|
|
if (v && (!best || (Number(v.ts) || 0) >= (Number(best.ts) || 0))) best = v;
|
|
}
|
|
return best ? { status: best.status ?? null, reason: best.reason ?? '' } : null;
|
|
};
|
|
return {
|
|
router: pick('router'),
|
|
mentor: pick('mentor:plan', 'mentor:spec'),
|
|
judge: pick('judge:plan', 'judge:spec'),
|
|
gate3: pick('judge:gate3', 'judge:gate3card'),
|
|
};
|
|
}
|