fix(m2): указатель шага привязан к plan_id — перепечать сбрасывает указатель (R-27/J2)

This commit is contained in:
Дмитрий
2026-06-05 04:21:05 +03:00
parent 3addf4353b
commit 522531bbd2
2 changed files with 33 additions and 2 deletions
+15 -2
View File
@@ -53,6 +53,18 @@ export function actionOf(toolUse) {
return { op, object };
}
/**
* R-27: указатель шага привязан к plan_id. Чужой план / легаси (голое число) /
* нет plan_id / нет текущего плана → 0 — перепечать плана НЕ отматывает старый
* указатель на новый план (иначе шаги нового плана мис-индексируются).
*/
export function resolveStepPtr(stored, currentPlanId) {
if (!currentPlanId || !stored || typeof stored !== 'object') return 0;
if (stored.plan_id !== currentPlanId) return 0;
const n = Number(stored.ptr);
return Number.isInteger(n) && n >= 0 ? n : 0;
}
/**
* Решение верховного хука. allow только если: семя / зелёный проход ИЛИ действие
* совпадает с текущим шагом валидного замороженного плана, ссылка шага резолвится
@@ -126,11 +138,12 @@ async function main() {
const frozenPlan = loadFrozenPlan({ sessionId: sess, runtimeDir });
const frozenArtifact = loadFrozenArtifact({ sessionId: sess, runtimeDir });
const stepPath = `${runtimeDir}/plan-step-${sess}`;
let stepPtr = 0; try { stepPtr = parseInt(fs.readFileSync(stepPath, 'utf8'), 10) || 0; } catch {}
let stored = null; try { stored = JSON.parse(fs.readFileSync(stepPath, 'utf8')); } catch {}
const stepPtr = resolveStepPtr(stored, frozenPlan?.plan_id); // R-27: привязка к плану
const r = runGate({
event, frozenPlan, frozenArtifact, stepPtr, key,
journal: (entry) => journalAppend({ payload: entry, key, sessionId: sess, runtimeDir }),
saveStep: (n) => fs.writeFileSync(stepPath, String(n)),
saveStep: (n) => fs.writeFileSync(stepPath, JSON.stringify({ plan_id: frozenPlan?.plan_id ?? null, ptr: n })),
});
exitDecision({ block: r.block, message: r.block ? `[supreme-gate] ${r.message}` : undefined });
} catch {
+18
View File
@@ -5,6 +5,24 @@ import { decide, actionOf } from './enforce-supreme-gate.mjs';
import { runGate } from './enforce-supreme-gate.mjs';
import { decideMode } from './enforce-supreme-gate.mjs';
import { isObserveOnly } from './enforce-supreme-gate.mjs';
import { resolveStepPtr } from './enforce-supreme-gate.mjs';
describe('resolveStepPtr — указатель шага привязан к plan_id (R-27)', () => {
it('тот же план → возвращает сохранённый ptr', () => {
expect(resolveStepPtr({ plan_id: 'P1', ptr: 3 }, 'P1')).toBe(3);
});
it('другой план → сброс в 0 (перепечать не отматывает чужой указатель)', () => {
expect(resolveStepPtr({ plan_id: 'P1', ptr: 3 }, 'P2')).toBe(0);
});
it('легаси (голое число) / null / без plan_id → 0', () => {
expect(resolveStepPtr(5, 'P1')).toBe(0);
expect(resolveStepPtr(null, 'P1')).toBe(0);
expect(resolveStepPtr({ ptr: 9 }, 'P1')).toBe(0);
});
it('нет текущего плана → 0', () => {
expect(resolveStepPtr({ plan_id: 'P1', ptr: 3 }, undefined)).toBe(0);
});
});
describe('isSeed (D12/D13 bootstrap exemption)', () => {
it('EnterPlanMode and AskUserQuestion are seeds', () => {