Files
brain/tools/router-task-id.mjs
T

39 lines
2.1 KiB
JavaScript

#!/usr/bin/env node
/**
* router-task-id (✅O17) — persisted first-plan-anchor. task-id присваивается при ПЕРВОМ
* плане задачи, ПЕРСИСТИТСЯ, НЕ меняется через re-issue (новый plan-hash не сбрасывает).
* Чистое ядро; персист (loadTaskId/saveTaskId) — отдельно, инъектируемый fs,
* assertSafeSessionId guard (N3-shared, path-injection).
*/
import { assertSafeSessionId } from './action-journal.mjs';
/** Стабильный task-id: существующий → возвращается как есть (персист); иначе якорь из
* firstPlanHash (первый план задачи). Нет ни того ни другого → null. */
export function deriveTaskId({ existingTaskId = null, firstPlanHash = '' } = {}) {
if (typeof existingTaskId === 'string' && existingTaskId.trim()) return existingTaskId;
const h = String(firstPlanHash || '').trim();
return h ? `task:${h}` : null;
}
function taskIdPath(runtimeDir, sessionId) {
assertSafeSessionId(sessionId);
const sep = runtimeDir.endsWith('/') ? '' : '/';
return `${runtimeDir}${sep}router-task-id-${sessionId}.txt`;
}
/** Загрузить персистнутый task-id (или null). fsImpl инъектируем. */
export function loadTaskId({ sessionId, runtimeDir, fsImpl }) {
try { return String(fsImpl.readFileSync(taskIdPath(runtimeDir, sessionId), 'utf8')).trim() || null; }
catch (e) { if (e && e.code === 'ENOENT') return null; throw e; }
}
/** Персистнуть task-id (идемпотентно — A0 пишет один раз при первом плане).
* F-C1 (sharp-edges): атомарно temp→rename (зеркало plan-lock writeAtomic) — частичная
* запись не оставляет битый якорь на финальном пути. */
export function saveTaskId({ taskId, sessionId, runtimeDir, fsImpl }) {
const path = taskIdPath(runtimeDir, sessionId);
const tmp = `${path}.tmp`;
fsImpl.writeFileSync(tmp, String(taskId || ''));
fsImpl.renameSync(tmp, path);
}