diff --git a/tools/enforce-judge-gate.mjs b/tools/enforce-judge-gate.mjs index 8d85957..098b62b 100644 --- a/tools/enforce-judge-gate.mjs +++ b/tools/enforce-judge-gate.mjs @@ -118,7 +118,16 @@ export async function runJudgeGate(event, deps = {}) { // «Оба строго» (2026-06-12): СВОЙ ключ судьи ROUTER_JUDGE_LLM_KEY, общий не фолбэк. const apiKey = deps.apiKey !== undefined ? deps.apiKey : resolveJudgeLlmKey(); const requiredLenses = requiredLensesFor(functionName); - const promptArgs = { product: g.product, goal: g.goal, cards: g.cards }; + // SP2c-2: память кругов J-side (свои judge-замечания + J-доводы + diff; судья холодный — + // без замечания-при-возврате). roundMemoryImpl грузит из стора (await — годится sync-тест и + // async-прод); нет инъекции → круг слеп ({}). buildJudgePrompt уже рендерит roundMemory. + const stage = functionName === 'gate1' ? 'spec' : 'plan'; + const rmContent = String((event && event.tool_input && event.tool_input.content) ?? ''); + let roundMemory = {}; + if (typeof deps.roundMemoryImpl === 'function') { + try { roundMemory = (await deps.roundMemoryImpl({ stage, content: rmContent })) || {}; } catch { roundMemory = {}; } + } + const promptArgs = { product: g.product, goal: g.goal, cards: g.cards, roundMemory }; const raw = await callJudgeModel({ functionName, requiredLenses, promptArgs, apiKey, model: deps.model, transport: deps.transport }); if (raw && raw.unavailable) { // M7: причина недоступности протекает в вердикт → лог-WARN + seal-запись её фиксируют. @@ -458,7 +467,17 @@ async function main() { }; let result; try { - result = await runJudgeTurn(event, { mode, nowMs: Date.now(), onWiredSeal: sealTurnProd, mentorApproved }); // inert/shadow/live-block внутри; nowMs → at в seal/verdict/warn (M7) + result = await runJudgeTurn(event, { + mode, nowMs: Date.now(), onWiredSeal: sealTurnProd, mentorApproved, + // SP2c-2: реальный загрузчик памяти кругов J-side из стора (taskId — тот же, что + // сохранил наставник до судьи; side='judge' холодный). Динамический импорт, fail-quiet внутри. + roundMemoryImpl: async ({ stage, content }) => { + let taskId = null; + try { taskId = loadTaskId({ sessionId: (event && event.session_id) || 'unknown', runtimeDir: runtimeDir(), fsImpl: fsDefault }); } catch { taskId = null; } + const { buildRoundMemory } = await import('./round-memory-store.mjs'); + return buildRoundMemory({ taskId, stage, side: 'judge', currentContent: content, baseDir: runtimeDir() }); + }, + }); // inert/shadow/live-block внутри; nowMs → at в seal/verdict/warn (M7) } catch { exitDecision({ block: mode === 'live-block' }); return; } // fail-CLOSE только в live-block // M7 эскалация (round-control C-12): подряд идущие NO-GO судьи. allow → сброс. После 3-го подряд — // сообщение «ЭСКАЛАЦИЯ ВЛАДЕЛЬЦУ» (судья сам выходит на владельца; продавить — escape, который судья diff --git a/tools/enforce-judge-gate.test.mjs b/tools/enforce-judge-gate.test.mjs index 0b7f516..9929b46 100644 --- a/tools/enforce-judge-gate.test.mjs +++ b/tools/enforce-judge-gate.test.mjs @@ -109,6 +109,28 @@ describe('runJudgeGate (async) — рубильник + детект + преф expect(calls).toBe(1); expect(r.wired).toBe(true); }); + + // SP2c-2: судья J-side получает roundMemory (свои judge-замечания + J-доводы + diff) в промпт. + it('SP2c-2: roundMemoryImpl (plan) → J-память в промпте судьи (stage plan)', async () => { + const prompts = []; + await runJudgeGate(planEv(), { + judgeActiveImpl: () => true, apiKey: 'K', mentorApproved: () => true, + transport: async (p) => { prompts.push(p); return okText; }, + roundMemoryImpl: ({ stage }) => ({ objections: [`память судьи ${stage}`] }), + }); + expect(prompts).toHaveLength(1); + expect(prompts[0].user).toContain('память судьи plan'); + }); + it('SP2c-2: roundMemoryImpl (spec) → stage spec в промпте судьи', async () => { + const prompts = []; + const specEv = { tool_name: 'Write', tool_input: { file_path: 'docs/superpowers/specs/x-design.md', content: '## R {#r}\nтекст' } }; + await runJudgeGate(specEv, { + judgeActiveImpl: () => true, apiKey: 'K', mentorApproved: () => true, + transport: async (p) => { prompts.push(p); return okText; }, + roundMemoryImpl: ({ stage }) => ({ objections: [`память судьи ${stage}`] }), + }); + expect(prompts[0].user).toContain('память судьи spec'); + }); it('активен, но не план (Bash) → wired:false, транспорт не зовётся ($0)', async () => { let calls = 0; const deps = { judgeActiveImpl: () => true, apiKey: 'K', transport: async () => { calls++; return okText; } };