f22f8bd2ef
Новый router-pin-store: пин совета роутера по (task_id, goalHash) пер-сессионно. on-plan-write пин-aware: пин-попадание по неизменной цели → совет переиспользуется, classifyImpl НЕ зовётся; промах/смена цели → classify + сохранение пина. Проводка в активный наставник-хук инъекцией реального стора с sessionId (инъекция-выкл по умолчанию, старое поведение/тесты целы). Хвост спеки роутера §4 (пининг по goalHash), эпик роутер-реестр этап 3, item 2. Граница не тронута (recommended_chain, цепочки, observer-stop-hook, owner-seal). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
48 lines
2.6 KiB
JavaScript
48 lines
2.6 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* router-pin-store — пер-сессионный пининг совета роутера по (task_id, goalHash).
|
|
* Пока цель (тело «## Цель») не менялась, совет роутера переиспользуется — LLM-классификатор
|
|
* не зовётся заново на переписку шагов плана (спека роутер-реестр §4, пининг по goalHash).
|
|
* Ключ записи — task_id; запись несёт goalHash + классификацию. Чтение возвращает совет только
|
|
* при совпавшем goalHash, иначе null. Любой сбой I/O — мягкий (load→null, save→false). goalHash
|
|
* считается из текста цели тем же способом, что якорь цели судьи (canonicalJson + sha256).
|
|
*/
|
|
import { homedir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
import { createHash } from 'node:crypto';
|
|
import { canonicalJson } from './receipt-sign.mjs';
|
|
|
|
export function goalHashOf(goal) {
|
|
return createHash('sha256').update(canonicalJson(String(goal ?? ''))).digest('hex');
|
|
}
|
|
|
|
function baseOf(baseDir) { return baseDir || join(homedir(), '.claude', 'runtime'); }
|
|
function pinPath(sessionId, baseDir) { return join(baseOf(baseDir), `router-pin-${sessionId || 'unknown'}.json`); }
|
|
|
|
/** Совет роутера по (taskId, текущая цель) — только если сохранённый goalHash совпал; иначе null. */
|
|
export function loadRouterPin({ taskId, planGoal, sessionId, baseDir } = {}) {
|
|
try {
|
|
const p = pinPath(sessionId, baseDir);
|
|
if (!existsSync(p)) return null;
|
|
const obj = JSON.parse(readFileSync(p, 'utf8'));
|
|
const rec = obj && obj[taskId];
|
|
if (rec && rec.goalHash === goalHashOf(planGoal)) return rec.classification ?? null;
|
|
return null;
|
|
} catch { return null; }
|
|
}
|
|
|
|
/** Записать пин для taskId (перезапись своей записи). Best-effort: сбой → false, без throw. */
|
|
export function saveRouterPin({ taskId, planGoal, classification, sessionId, baseDir, nowMs = null } = {}) {
|
|
try {
|
|
const dir = baseOf(baseDir);
|
|
mkdirSync(dir, { recursive: true });
|
|
const p = pinPath(sessionId, baseDir);
|
|
let obj = existsSync(p) ? JSON.parse(readFileSync(p, 'utf8')) : {};
|
|
if (!obj || typeof obj !== 'object') obj = {};
|
|
obj[taskId] = { goalHash: goalHashOf(planGoal), classification, ts: nowMs ?? Date.now() };
|
|
writeFileSync(p, JSON.stringify(obj));
|
|
return true;
|
|
} catch { return false; }
|
|
}
|