diff --git a/tools/mentor-verdict.mjs b/tools/mentor-verdict.mjs index 179f44b..7e34733 100644 --- a/tools/mentor-verdict.mjs +++ b/tools/mentor-verdict.mjs @@ -74,12 +74,12 @@ export function buildMentorVerdictPrompt({ plan = null, verifiedContext = [], ne * llmCall (инъект; живой транспорт — активация владельца) → parse → validateMentorVerdict → * при валидном wired:true + verdict с plan_hash (binding нах.F4: freeze-gate сверит * verdict.plan_hash === planId(steps) — stale/чужой вердикт не пройдёт). Сбой → wired:false - * (SE-R6-6: не суд). + * (SE-R6-6: не суд). roundMemory (SP2c-2) — память кругов M-side, прокидывается в построитель. */ -export async function runMentorVerdict({ plan = null, verifiedContext = [], negotiationLog = [], graphSection = null, planHash = null, skillContext = null, llmCall }) { +export async function runMentorVerdict({ plan = null, verifiedContext = [], negotiationLog = [], graphSection = null, planHash = null, skillContext = null, roundMemory = {}, llmCall }) { let v; try { - v = await llmCall({ buildPrompt: () => buildMentorVerdictPrompt({ plan, verifiedContext, negotiationLog, graphSection, skillContext }) }); + v = await llmCall({ buildPrompt: () => buildMentorVerdictPrompt({ plan, verifiedContext, negotiationLog, graphSection, skillContext, roundMemory }) }); } catch (e) { // Smoke 2026-06-12: тихий catch не давал отличить 401 (ключ) от сети — деталь // обязана доехать до вердикт-файла/журнала (усечённая; ключ в message не попадает). @@ -119,12 +119,13 @@ export function buildMentorSpecVerdictPrompt({ specContent = '', verifiedContext /** * Фаза 3 (Р6): производитель вердикта наставника по СПЕКЕ. Зеркало runMentorVerdict, но * spec-промпт; binding plan_hash = specHash (хеш артефакта спеки — judgedHashOf(sealableArtifact), - * тот же, чем судья печатает gate1). Сбой → wired:false (SE-R6-6, не суд). + * тот же, чем судья печатает gate1). Сбой → wired:false (SE-R6-6, не суд). roundMemory (SP2c-2) + * — память кругов M-side спеки, прокидывается в построитель. */ -export async function runMentorSpecVerdict({ specContent = '', specHash = null, verifiedContext = [], negotiationLog = [], graphSection = null, llmCall }) { +export async function runMentorSpecVerdict({ specContent = '', specHash = null, verifiedContext = [], negotiationLog = [], graphSection = null, roundMemory = {}, llmCall }) { let v; try { - v = await llmCall({ buildPrompt: () => buildMentorSpecVerdictPrompt({ specContent, verifiedContext, negotiationLog, graphSection }) }); + v = await llmCall({ buildPrompt: () => buildMentorSpecVerdictPrompt({ specContent, verifiedContext, negotiationLog, graphSection, roundMemory }) }); } catch (e) { const detail = String((e && e.message) || e).slice(0, 200); return { ok: false, wired: false, reason: `сбой вызова наставника-вердикта (спека): ${detail}`, verdict: null }; diff --git a/tools/mentor-verdict.test.mjs b/tools/mentor-verdict.test.mjs index cdd2535..49bf75f 100644 --- a/tools/mentor-verdict.test.mjs +++ b/tools/mentor-verdict.test.mjs @@ -177,3 +177,17 @@ describe('runMentorVerdict (§6.1 производитель + binding нах.F4 expect(r.verdict.plan_hash).toBe('PH2'); }); }); + +describe('производители прокидывают roundMemory в построитель (SP2c-2)', () => { + const GOODV = { plan_points_addressed: ['ок'], reasoning: 'r', recommendation: 'rec', confidence: 0.8, decision: 'GO' }; + it('runMentorVerdict → roundMemory доходит до промпта', async () => { + let captured = null; + await runMentorVerdict({ plan: {}, planHash: 'PH', roundMemory: { objections: ['замечание прошлого круга M'] }, llmCall: async ({ buildPrompt }) => { captured = buildPrompt(); return GOODV; } }); + expect(captured.user).toContain('замечание прошлого круга M'); + }); + it('runMentorSpecVerdict → roundMemory доходит до промпта', async () => { + let captured = null; + await runMentorSpecVerdict({ specContent: '# с', specHash: 'SH', roundMemory: { objections: ['замечание прошлого круга спеки'] }, llmCall: async ({ buildPrompt }) => { captured = buildPrompt(); return GOODV; } }); + expect(captured.user).toContain('замечание прошлого круга спеки'); + }); +}); diff --git a/tools/on-plan-write.mjs b/tools/on-plan-write.mjs index 30895aa..b4639ae 100644 --- a/tools/on-plan-write.mjs +++ b/tools/on-plan-write.mjs @@ -29,6 +29,7 @@ export function recommendedChainOf(c) { /** * @returns {{taskId, taskIdPersisted, ok, wired, verdict, reason?, journal, journalOk}} * verdict.plan_hash === planId(planSteps) (нах.F4) — caller отдаёт его freeze-gate. + * roundMemory (SP2c-2) — память кругов M-side, протягивается до построителя вердикта. */ export async function onPlanWrite({ planSteps = [], @@ -47,6 +48,7 @@ export async function onPlanWrite({ registry = null, declaredSkills = [], planGoal = '', + roundMemory = {}, } = {}) { const planHash = planId(planSteps); // ✅O17: существующий task-id побеждает (re-issue не сбрасывает); первый план — якорь. @@ -67,7 +69,7 @@ export async function onPlanWrite({ const skillContext = renderSkillContext({ declared: declaredSkills, recommendedChain, registry }); // Производитель вердикта (C T5b): сбой → ok:false/wired:false (SE-R6-6, не суд). const r = await runMentorVerdictImpl({ - plan: { steps: planSteps }, planHash, verifiedContext, negotiationLog, graphSection, skillContext, llmCall, + plan: { steps: planSteps }, planHash, verifiedContext, negotiationLog, graphSection, skillContext, roundMemory, llmCall, }); // SE10 (A4): журнал — best-effort; throw ловится, круг не падает. Обоснование непустое // всегда (F-C2/ДР-6): из вердикта либо из reason сбоя. @@ -91,7 +93,8 @@ export async function onPlanWrite({ * Фаза 3 (отдельный spec-путь, Р6) — оркестратор записи СПЕКИ. Зеркало onPlanWrite, но * вердикт по спеке (runMentorSpecVerdict, binding specHash — хеш артефакта спеки). task-id * анкорится specHash (спека первая в стэке спека+план → план переиспользует тот же task-id). - * Журнал переговоров — best-effort (SE10). I/O/llmCall инъектируются. + * Журнал переговоров — best-effort (SE10). I/O/llmCall инъектируются. roundMemory (SP2c-2) + * — память кругов M-side спеки, протягивается до построителя вердикта. */ export async function onSpecWrite({ specContent = '', @@ -107,6 +110,7 @@ export async function onSpecWrite({ verifiedContext = [], negotiationLog = [], graphSection = null, + roundMemory = {}, } = {}) { // ✅O17: существующий task-id побеждает; спека анкорит стэк (спека+план) по specHash. const taskId = deriveTaskId({ existingTaskId, firstPlanHash: specHash }); @@ -114,7 +118,7 @@ export async function onSpecWrite({ if (taskId && typeof persistTaskIdImpl === 'function') { try { persistTaskIdImpl(taskId); taskIdPersisted = true; } catch { taskIdPersisted = false; } } - const r = await runMentorSpecVerdictImpl({ specContent, specHash, verifiedContext, negotiationLog, graphSection, llmCall }); + const r = await runMentorSpecVerdictImpl({ specContent, specHash, verifiedContext, negotiationLog, graphSection, roundMemory, llmCall }); let journal = null; let journalOk = false; try { diff --git a/tools/on-plan-write.test.mjs b/tools/on-plan-write.test.mjs index 7122a44..79d889c 100644 --- a/tools/on-plan-write.test.mjs +++ b/tools/on-plan-write.test.mjs @@ -1,6 +1,6 @@ // tools/on-plan-write.test.mjs import { describe, it, expect } from 'vitest'; -import { onPlanWrite } from './on-plan-write.mjs'; +import { onPlanWrite, onSpecWrite } from './on-plan-write.mjs'; import { planId } from './plan-lock.mjs'; const STEPS = [{ n: 1, op: 'Write', object: 'tools/a.mjs' }]; @@ -100,3 +100,26 @@ describe('onPlanWrite — скил-сверка через classify (мерж)', expect(capturedUser).not.toMatch(/#4(?! — )/); // голого #4 без имени нет }); }); + +describe('оркестратор протягивает roundMemory до построителя (SP2c-2)', () => { + it('onPlanWrite → roundMemory доходит до промпта вердикта', async () => { + let capturedUser = null; + await onPlanWrite({ + planSteps: STEPS, + roundMemory: { objections: ['память круга плана'] }, + llmCall: async ({ buildPrompt }) => { capturedUser = buildPrompt().user; return goodVerdict; }, + journalKey: 'k', nowMs: 1, + }); + expect(capturedUser).toContain('память круга плана'); + }); + it('onSpecWrite → roundMemory доходит до промпта вердикта спеки', async () => { + let capturedUser = null; + await onSpecWrite({ + specContent: '# Спека', specHash: 'SH', + roundMemory: { objections: ['память круга спеки'] }, + llmCall: async ({ buildPrompt }) => { capturedUser = buildPrompt().user; return goodVerdict; }, + journalKey: 'k', nowMs: 1, + }); + expect(capturedUser).toContain('память круга спеки'); + }); +});