From 522531bbd2940434d2e797dcf0e0c4a8a2dea1eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Fri, 5 Jun 2026 04:21:05 +0300 Subject: [PATCH] =?UTF-8?q?fix(m2):=20=D1=83=D0=BA=D0=B0=D0=B7=D0=B0=D1=82?= =?UTF-8?q?=D0=B5=D0=BB=D1=8C=20=D1=88=D0=B0=D0=B3=D0=B0=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=D0=B2=D1=8F=D0=B7=D0=B0=D0=BD=20=D0=BA=20plan=5Fid=20?= =?UTF-8?q?=E2=80=94=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BF=D0=B5=D1=87=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=20=D1=81=D0=B1=D1=80=D0=B0=D1=81=D1=8B=D0=B2=D0=B0?= =?UTF-8?q?=D0=B5=D1=82=20=D1=83=D0=BA=D0=B0=D0=B7=D0=B0=D1=82=D0=B5=D0=BB?= =?UTF-8?q?=D1=8C=20(R-27/J2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/enforce-supreme-gate.mjs | 17 +++++++++++++++-- tools/enforce-supreme-gate.test.mjs | 18 ++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/tools/enforce-supreme-gate.mjs b/tools/enforce-supreme-gate.mjs index a44fb01c..7e0560c1 100644 --- a/tools/enforce-supreme-gate.mjs +++ b/tools/enforce-supreme-gate.mjs @@ -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 { diff --git a/tools/enforce-supreme-gate.test.mjs b/tools/enforce-supreme-gate.test.mjs index 0d1bbd47..95f4b065 100644 --- a/tools/enforce-supreme-gate.test.mjs +++ b/tools/enforce-supreme-gate.test.mjs @@ -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', () => {