From 1289e685240da72f3dc145bfbb56637036aa6451 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: Tue, 16 Jun 2026 17:11:21 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20round-memory=20=D0=B7=D0=B0=D0=B3=D1=80?= =?UTF-8?q?=D1=83=D0=B7=D0=BA=D0=B0=20roundMemory=20M-side=20=D0=B2=20?= =?UTF-8?q?=D1=85=D1=83=D0=BA=D0=B5=20=D0=BD=D0=B0=D1=81=D1=82=D0=B0=D0=B2?= =?UTF-8?q?=D0=BD=D0=B8=D0=BA=D0=B0=20SP2c-2=20=D0=B8=D0=BD=D0=BA=D1=80?= =?UTF-8?q?=D0=B5=D0=BC=D0=B5=D0=BD=D1=82=202b?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- tools/enforce-mentor-on-plan-write.mjs | 16 +++++++++++++- tools/enforce-mentor-on-plan-write.test.mjs | 23 +++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/tools/enforce-mentor-on-plan-write.mjs b/tools/enforce-mentor-on-plan-write.mjs index 02ba69c..38986e1 100644 --- a/tools/enforce-mentor-on-plan-write.mjs +++ b/tools/enforce-mentor-on-plan-write.mjs @@ -35,6 +35,9 @@ import { bumpMentorNoGo, MENTOR_ESCALATE_AFTER } from './mentor-nogo-counter.mjs import { buildMentorGo, persistMentorGo } from './mentor-go-store.mjs'; import { classifyJudgeOutcome } from './verdict-outcome-line.mjs'; import { pushVerdict } from './verdict-surface-store.mjs'; +// SP2c-2: загрузчик памяти кругов M-side (свои замечания + M-доводы + diff + замечание судьи +// при возврате) — инъектируется в runMentorOnPlanWrite, протягивается до построителя вердикта. +import { buildRoundMemory } from './round-memory-store.mjs'; /** * Волна 7 (§6): сообщение арбитража при 3 NO-GO наставника — дословное замечание + @@ -101,11 +104,13 @@ export function buildLlmCall({ apiKey, model = CLASSIFIER_MODEL, transport = cal * Чистый производитель: inert → {ran:false}; не план-Write → {ran:false}; план без * steps-json → {ran:false, reason} (вердикт НЕ фабрикуется — печать всё равно fail-CLOSE * у судьи [enforce-judge-gate.mjs:79]); иначе onPlanWrite + персист. ВСЕ deps инъектируются. + * roundMemoryImpl (SP2c-2): загрузчик памяти кругов M-side ({stage,content,taskId}→roundMemory); + * null → круг слеп (память пуста, backward-compat). */ export async function runMentorOnPlanWrite(event, { mentorActiveImpl, llmCall, loadJournalImpl, persistJournalImpl, persistVerdictImpl, loadTaskIdImpl, persistTaskIdImpl, journalKey, graphSectionImpl, nowMs = null, - classifyImpl = null, registryImpl = null, + classifyImpl = null, registryImpl = null, roundMemoryImpl = null, } = {}) { if (!mentorActiveImpl()) return { ran: false, reason: 'mentor inert ($0)' }; const tool = event && event.tool_name; @@ -126,6 +131,8 @@ export async function runMentorOnPlanWrite(event, { let graphSectionS = null; try { graphSectionS = graphSectionImpl(); } catch { graphSectionS = null; } const verifiedContextS = parseVerifiedContext(content); + // SP2c-2: память кругов M-side спеки (свои замечания + M-доводы + diff + замечание судьи). + const roundMemoryS = roundMemoryImpl ? roundMemoryImpl({ stage: 'spec', content, taskId: taskIdForPromptS }) : {}; const rs = await onSpecWrite({ specContent: content, specHash, @@ -138,6 +145,7 @@ export async function runMentorOnPlanWrite(event, { verifiedContext: verifiedContextS, negotiationLog: negotiationLogS, graphSection: graphSectionS, + roundMemory: roundMemoryS, }); try { persistVerdictImpl({ ok: rs.ok, wired: rs.wired, reason: rs.reason ?? null, planHash: specHash, verdict: rs.verdict }); } catch { /* best-effort */ } if (rs.journalOk && rs.journal) { try { persistJournalImpl(rs.journal); } catch { /* best-effort (SE10) */ } } @@ -165,6 +173,8 @@ export async function runMentorOnPlanWrite(event, { const planGoal = extractPlanGoal(content); let registry = null; try { registry = typeof registryImpl === 'function' ? registryImpl() : null; } catch { registry = null; } + // SP2c-2: память кругов M-side плана (свои замечания + M-доводы + diff + замечание судьи). + const roundMemoryP = roundMemoryImpl ? roundMemoryImpl({ stage: 'plan', content, taskId: taskIdForPrompt }) : {}; const r = await onPlanWrite({ planSteps: steps, existingTaskId: loadTaskIdImpl(), @@ -180,6 +190,7 @@ export async function runMentorOnPlanWrite(event, { registry, declaredSkills, planGoal, + roundMemory: roundMemoryP, }); const planHash = planId(steps); try { persistVerdictImpl({ ok: r.ok, wired: r.wired, reason: r.reason ?? null, planHash, verdict: r.verdict }); } catch { /* best-effort */ } @@ -211,6 +222,9 @@ async function main() { // ключ/транспорт; сбой ловит onPlanWrite → fail-safe (план без скил-сверки, §5). classifyImpl: async (goal, registry) => classify(goal, registry, { skipPrefilter: true }), registryImpl: () => { try { return loadRegistry({ useCache: false }); } catch { return null; } }, + // SP2c-2: реальный загрузчик памяти кругов M-side из стора (fail-quiet внутри + // buildRoundMemory). baseDir = runtime; side='mentor' → M-дорожка + замечание судьи при возврате. + roundMemoryImpl: ({ stage, content, taskId }) => buildRoundMemory({ taskId, stage, side: 'mentor', currentContent: content, baseDir: dir }), }); if (res && res.ran) { // SP1: громкая видимость вердикта наставника (best-effort, fail-quiet). diff --git a/tools/enforce-mentor-on-plan-write.test.mjs b/tools/enforce-mentor-on-plan-write.test.mjs index 3c4d784..01a8b31 100644 --- a/tools/enforce-mentor-on-plan-write.test.mjs +++ b/tools/enforce-mentor-on-plan-write.test.mjs @@ -116,6 +116,29 @@ describe('runMentorOnPlanWrite (обёртка-производитель W7)', expect(r.ran).toBe(true); expect(classifyCalled).toBe(true); }); + + // SP2c-2: хук грузит память кругов M-side через roundMemoryImpl и протягивает её до + // построителя вердикта наставника (план и спека). stage прокидывается верно. + it('SP2c-2: roundMemoryImpl (план) → память доходит до промпта наставника', async () => { + let capturedUser = null; + const d = deps({ + roundMemoryImpl: ({ stage }) => ({ objections: [`память дорожки ${stage}`] }), + llmCall: async ({ buildPrompt }) => { capturedUser = buildPrompt().user; return GOOD_VERDICT; }, + }); + const r = await runMentorOnPlanWrite(planEvent, d); + expect(r.ran).toBe(true); + expect(capturedUser).toContain('память дорожки plan'); + }); + it('SP2c-2: roundMemoryImpl (спека) → stage=spec в памяти промпта', async () => { + let capturedUser = null; + const d = deps({ + roundMemoryImpl: ({ stage }) => ({ objections: [`память дорожки ${stage}`] }), + llmCall: async ({ buildPrompt }) => { capturedUser = buildPrompt().user; return GOOD_VERDICT; }, + }); + const r = await runMentorOnPlanWrite(specEvent, d); + expect(r.ran).toBe(true); + expect(capturedUser).toContain('память дорожки spec'); + }); }); describe('decideMentorObjection (Фаза 1 — канал замечаний наставника контроллеру)', () => {