#!/usr/bin/env node /** * mentor-go-store (способ B, Фаза 2) — наставник в Post записывает подписанное «я одобрил * ЭТОТ план» (привязка к plan_hash, нах.F4). Судья (хук ПОСЛЕ наставника) читает запись и * судит/печатает ТОЛЬКО при валидном mentor-GO; нет одобрения наставника → судья молчит * (fail-safe). Зеркало judge-go-store, домен подписи MENTOR_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.MENTOR_GO; function mentorGoPath(runtimeDir, sessionId) { assertSafeSessionId(sessionId); const sep = runtimeDir.endsWith('/') ? '' : '/'; return `${runtimeDir}${sep}mentor-go-${sessionId}.json`; } /** Чистая сборка подписанной записи «наставник одобрил» для плана (plan_hash — binding нах.F4). */ export function buildMentorGo({ planHash, judgeMode = null, key, nowMs = null }) { const base = { plan_hash: planHash ?? null, approved: true, at: typeof nowMs === 'number' ? nowMs : null, }; return { ...base, sig: signPayload(base, key, DOMAIN) }; } /** Запись валидна И принадлежит ЭТОМУ плану И подпись цела? Иначе false (fail-closed). */ export function mentorGoValidFor(record, { planHash, key } = {}) { if (!record || typeof record !== 'object') return false; if (record.plan_hash !== planHash) return false; if (record.approved !== true) return false; return verifyReceipt(record, key, DOMAIN); } /** Атомарная запись одобрения наставника в ~/.claude/runtime/mentor-go-.json. */ export function persistMentorGo({ record, sessionId, runtimeDir, fsImpl = fsDefault }) { const p = mentorGoPath(runtimeDir, sessionId); const tmp = `${p}.tmp`; fsImpl.writeFileSync(tmp, JSON.stringify(record)); fsImpl.renameSync(tmp, p); } /** Загрузка одобрения наставника (нет файла → null). */ export function loadMentorGo({ sessionId, runtimeDir, fsImpl = fsDefault }) { try { return JSON.parse(fsImpl.readFileSync(mentorGoPath(runtimeDir, sessionId), 'utf8')); } catch (e) { if (e && e.code === 'ENOENT') return null; throw e; } }