54 lines
2.6 KiB
JavaScript
54 lines
2.6 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* judge-go-store (фикс дедлока печати плана) — судья (PreToolUse) пломбировать план НЕ может:
|
|
* freeze-gate требует вердикт наставника, а тот рождается в PostToolUse (после судьи). Поэтому
|
|
* судья лишь СОХРАНЯЕТ свой GO (подписанный, привязанный к plan_hash), а печать плана делает
|
|
* наставник в Post — там вердикт уже свежий для текущего плана. Этот модуль — канал GO судьи.
|
|
*/
|
|
import fsDefault from 'node:fs';
|
|
import { assertSafeSessionId } from './action-journal.mjs';
|
|
import { signPayload, verifyReceipt, RECEIPT_DOMAINS } from './receipt-sign.mjs';
|
|
|
|
const DOMAIN = RECEIPT_DOMAINS.JUDGE_GO;
|
|
|
|
function judgeGoPath(runtimeDir, sessionId) {
|
|
assertSafeSessionId(sessionId);
|
|
const sep = runtimeDir.endsWith('/') ? '' : '/';
|
|
return `${runtimeDir}${sep}judge-go-${sessionId}.json`;
|
|
}
|
|
|
|
/** Чистая сборка подписанной записи GO судьи для плана (plan_hash — binding нах.F4). */
|
|
export function buildJudgeGo({ planHash, judgedHash, judgeMode = null, key, nowMs = null }) {
|
|
const base = {
|
|
plan_hash: planHash ?? null,
|
|
judged_hash: judgedHash ?? null,
|
|
decision: 'GO',
|
|
wired: true,
|
|
judge_mode: judgeMode ?? null,
|
|
at: typeof nowMs === 'number' ? nowMs : null,
|
|
};
|
|
return { ...base, sig: signPayload(base, key, DOMAIN) };
|
|
}
|
|
|
|
/** Запись валидна И принадлежит ЭТОМУ плану И подпись цела? Иначе false (fail-closed). */
|
|
export function judgeGoValidFor(record, { planHash, key } = {}) {
|
|
if (!record || typeof record !== 'object') return false;
|
|
if (record.plan_hash !== planHash) return false;
|
|
if (record.decision !== 'GO' || record.wired !== true) return false;
|
|
return verifyReceipt(record, key, DOMAIN);
|
|
}
|
|
|
|
/** Атомарная запись GO судьи в ~/.claude/runtime/judge-go-<sess>.json. */
|
|
export function persistJudgeGo({ record, sessionId, runtimeDir, fsImpl = fsDefault }) {
|
|
const p = judgeGoPath(runtimeDir, sessionId);
|
|
const tmp = `${p}.tmp`;
|
|
fsImpl.writeFileSync(tmp, JSON.stringify(record));
|
|
fsImpl.renameSync(tmp, p);
|
|
}
|
|
|
|
/** Загрузка GO судьи (нет файла → null). */
|
|
export function loadJudgeGo({ sessionId, runtimeDir, fsImpl = fsDefault }) {
|
|
try { return JSON.parse(fsImpl.readFileSync(judgeGoPath(runtimeDir, sessionId), 'utf8')); }
|
|
catch (e) { if (e && e.code === 'ENOENT') return null; throw e; }
|
|
}
|