feat: round-memory протяжка roundMemory через производителя и оркестратор наставника SP2c-2 инкремент 2a

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-16 17:03:12 +03:00
parent 3fb98fa51c
commit 2ba6d3c405
4 changed files with 52 additions and 10 deletions
+7 -6
View File
@@ -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 };
+14
View File
@@ -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('замечание прошлого круга спеки');
});
});
+7 -3
View File
@@ -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 {
+24 -1
View File
@@ -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('память круга спеки');
});
});