From edea1ea40c187914de58a03a2d40df1dcd4bd839 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 16:41:05 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20round-memory=20=D0=B2=D1=80=D0=B5=D0=B7?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=B7=D0=B0=D0=BF=D0=B8=D1=81=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=B0=D0=BC=D1=8F=D1=82=D0=B8=20=D0=BA=D1=80=D1=83=D0=B3=D0=BE?= =?UTF-8?q?=D0=B2=20=D0=B2=20=D0=B6=D0=B8=D0=B2=D1=83=D1=8E=20=D0=BE=D1=80?= =?UTF-8?q?=D0=BA=D0=B5=D1=81=D1=82=D1=80=D0=B0=D1=86=D0=B8=D1=8E=20SP2c-1?= =?UTF-8?q?=20=D1=872?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- tools/enforce-judge-gate.mjs | 11 +++++++++++ tools/enforce-mentor-on-plan-write.mjs | 11 +++++++++++ tools/round-memory-record.mjs | 14 ++++++++++++-- tools/round-memory-record.test.mjs | 21 +++++++++++++++++++-- 4 files changed, 53 insertions(+), 4 deletions(-) diff --git a/tools/enforce-judge-gate.mjs b/tools/enforce-judge-gate.mjs index c45f524..8d85957 100644 --- a/tools/enforce-judge-gate.mjs +++ b/tools/enforce-judge-gate.mjs @@ -472,6 +472,17 @@ async function main() { let taskId = null; try { taskId = loadTaskId({ sessionId: (event && event.session_id) || 'unknown', runtimeDir: runtimeDir(), fsImpl: fsDefault }); } catch { taskId = null; } const n = bumpJudgeNoGo({ taskId, sessionId: (event && event.session_id) || 'unknown', blocked: isNoGo }); + // SP2c-1: дословное замечание судьи на NO-GO в дорожку judge. Best-effort, fail-quiet. + if (isNoGo) { + try { + const fp = String((event && event.tool_input && event.tool_input.file_path) || ''); + const stage = SPEC_PATH_RE.test(fp) ? 'spec' : (PLAN_PATH_RE.test(fp) ? 'plan' : null); + if (stage) { + const rm = await import('./round-memory-record.mjs'); + rm.recordSideObjection(taskId, stage, 'judge', formatJudgeObjection(result.verdict)); + } + } catch { /* fail-quiet */ } + } if (isNoGo && n >= JUDGE_ESCALATE_AFTER) { const planContent = String((event && event.tool_input && event.tool_input.content) ?? ''); result = { ...result, message: buildJudgeArbitrationMessage(result.verdict, planContent, n) }; diff --git a/tools/enforce-mentor-on-plan-write.mjs b/tools/enforce-mentor-on-plan-write.mjs index 1a552ee..02ba69c 100644 --- a/tools/enforce-mentor-on-plan-write.mjs +++ b/tools/enforce-mentor-on-plan-write.mjs @@ -232,6 +232,17 @@ async function main() { // Фаза 1 (Р2): на NO-GO/degraded — ПОЛНЫЙ текст доходит до контроллера через рабочий // exit-2 канал (подтверждён Фазой 0). На 3-м NO-GO — карточка арбитража. const planContent = String((event.tool_input && event.tool_input.content) ?? ''); + // SP2c-1: память кругов — снимок версии (наставник раньше судьи в цепочке) + + // дословное замечание наставника на NO-GO. Best-effort, fail-quiet. + try { + const fp = String((event.tool_input && event.tool_input.file_path) || ''); + const stage = SPEC_PATH_RE.test(fp) ? 'spec' : (PLAN_PATH_RE.test(fp) ? 'plan' : null); + if (stage) { + const rm = await import('./round-memory-record.mjs'); + rm.recordArtifact(res.taskId, stage, planContent); + if (blocked) rm.recordSideObjection(res.taskId, stage, 'mentor', formatMentorObjection(res)); + } + } catch { /* fail-quiet */ } const decision = decideMentorObjection({ res, planContent, n }); // Способ B (Task 2.2): наставник НЕ печатает. На GO — записывает подписанное одобрение // (mentor-GO, binding plan_hash); печать сделает судья (хук ПОСЛЕ) при валидном mentor-GO. diff --git a/tools/round-memory-record.mjs b/tools/round-memory-record.mjs index 97f63c0..9c482d0 100644 --- a/tools/round-memory-record.mjs +++ b/tools/round-memory-record.mjs @@ -2,8 +2,8 @@ /** round-memory-record — orchestration-помощник записи памяти кругов (SP2c-1). * Цель — минимизировать врезки в discipline-source: одна точка пишет снимок версии артефакта * и доводы контроллера последнего круга «## Переговоры» по дорожкам M/J. Объекция стороны - * (NO-GO) пишется отдельным вызовом recordObjection прямо из стора. Fail-quiet. */ -import { recordVersion, recordArg } from './round-memory-store.mjs'; + * (NO-GO) пишется через recordSideObjection. Fail-quiet. */ +import { recordVersion, recordArg, recordObjection } from './round-memory-store.mjs'; import { parseNegotiationSection } from './negotiation-section.mjs'; /** На запись артефакта: снимок версии + доводы ПОСЛЕДНЕГО круга по дорожкам M/J. @@ -20,3 +20,13 @@ export function recordArtifact(taskId, stage, content, baseDir) { return true; } catch { return false; } } + +/** На объекцию стороны (NO-GO): дословное замечание в дорожку side∈{mentor,judge}. + * Пустой текст не пишется. Любая ошибка → no-op (false), не кидает. */ +export function recordSideObjection(taskId, stage, side, text, baseDir) { + try { + const t = String(text == null ? '' : text); + if (!t) return false; + return recordObjection(taskId, stage, side, t, baseDir); + } catch { return false; } +} diff --git a/tools/round-memory-record.test.mjs b/tools/round-memory-record.test.mjs index a014344..f0c23c1 100644 --- a/tools/round-memory-record.test.mjs +++ b/tools/round-memory-record.test.mjs @@ -2,8 +2,8 @@ import { describe, it, expect } from 'vitest'; import { mkdtempSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { recordArtifact } from './round-memory-record.mjs'; -import { getVersions, getArgs } from './round-memory-store.mjs'; +import { recordArtifact, recordSideObjection } from './round-memory-record.mjs'; +import { getVersions, getArgs, getObjections } from './round-memory-store.mjs'; const dir = () => mkdtempSync(join(tmpdir(), 'rmrec-')); @@ -35,3 +35,20 @@ describe('recordArtifact', () => { expect(() => recordArtifact('t1', 'plan', null, '\0bad')).not.toThrow(); }); }); + +describe('recordSideObjection', () => { + it('пишет дословное замечание стороны в дорожку', () => { + const d = dir(); + recordSideObjection('t1', 'plan', 'judge', 'замечание судьи', d); + expect(getObjections('t1', 'plan', 'judge', d)).toEqual(['замечание судьи']); + }); + it('пустой текст не пишется', () => { + const d = dir(); + recordSideObjection('t1', 'plan', 'mentor', '', d); + recordSideObjection('t1', 'plan', 'mentor', null, d); + expect(getObjections('t1', 'plan', 'mentor', d)).toEqual([]); + }); + it('fail-quiet на мусоре не кидает', () => { + expect(() => recordSideObjection('t1', 'plan', 'judge', 'x', '\0bad')).not.toThrow(); + }); +});