feat: round-memory owner-seal judge-gate wiring runJudgeTurn sealOnWiredGo sealTurnProd SP3-b4

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-17 03:17:14 +03:00
parent b111ca5ec1
commit 9b2c4f9e86
2 changed files with 68 additions and 10 deletions
+31 -10
View File
@@ -24,7 +24,9 @@ import { CLASSIFIER_MODEL, HEAVY_LLM_TIMEOUT_MS } from './router-config.mjs';
import fsDefault from 'node:fs';
import { join } from 'node:path';
// Task 5 (sealed-plan production): печать на реальном wired GO.
import { sealArtifact, sealPlan, sealablePlan, sealableArtifact, judgedHashOf } from './seal-orchestration.mjs';
import { sealArtifact, sealPlan, sealablePlan, sealableArtifact, judgedHashOf, decideSeal, ownerSealAction } from './seal-orchestration.mjs';
// SP3-b owner-seal: escape-грант владельца над owner-seal:<хеш тела> (читается в sealTurnProd, sync).
import { escapeGrantOpen, loadFloorEscapes, loadConsumed } from './escape-grant.mjs';
import { resolveReceiptKey } from './receipt-key-config.mjs';
import { loadFrozenArtifact, saveFrozenArtifact, saveFrozenPlan, planId } from './plan-lock.mjs';
import { logGuardBlock } from './guard-block-log.mjs';
@@ -299,13 +301,16 @@ export async function runJudgeTurn(event, { mode, logImpl = logVerdictLine, warn
let sealResult = null;
if (verdict && verdict.wired) {
try { logImpl(buildVerdictEntry(verdict, nowMs)); } catch { /* best-effort */ }
// Task 5: печать на реальном wired GO. Best-effort, НЕ влияет на block-решение
// (печать = одобрение, не энфорсмент). Инъектируется в main() — юнит-тесты hermetic.
if (deps.onWiredSeal) { try { sealResult = deps.onWiredSeal(event, verdict, mode); } catch { /* best-effort */ } }
} else if (verdict && verdict.unavailable) {
// M7: причина+тип+время → WARN-лог (diagnose degraded без догадок).
try { warnImpl(event, { cause: verdict.cause, errorType: verdict.errorType, nowMs }); } catch { /* best-effort */ }
}
// SP3-b (ownerseal-wiring-bug): печать пытается встать на КАЖДОЙ записи спеки/плана (judged) —
// НЕ только на wired. sealTurnProd внутри решает (decideSeal): wired-GO обычным путём ЛИБО
// owner-seal перебивает NO-GO/degraded. Раньше вызов сидел под if(verdict.wired) → при NO-GO
// наставника (wired:false) печать пропускалась и owner-seal был мёртвой проводкой. Best-effort,
// НЕ влияет на block-решение (печать = одобрение). onWiredSeal инъектируется в main() — тесты hermetic.
if (judged && deps.onWiredSeal) { try { sealResult = deps.onWiredSeal(event, verdict, mode); } catch { /* best-effort */ } }
// M7 наблюдаемость: исход судьи+печати для записи плана/спеки → seal-attempts.jsonl.
seal({
functionName: verdict && verdict.verdict && verdict.verdict.functionName,
@@ -356,13 +361,15 @@ export async function runJudgeTurn(event, { mode, logImpl = logVerdictLine, warn
* VA-3). judgeMode = режим гейта (shadow/live-block) → запечатан в печать (VA-2). Чистая
* (роутинг по пути), реальные deps впрыснуты — для теста переиспользуется без I/O.
*/
export function sealOnWiredGo({ event, verdict, judgeMode, deps = {} }) {
if (!(verdict && verdict.wired === true && verdict.decision === 'GO')) return { sealed: false };
export function sealOnWiredGo({ event, verdict, judgeMode, ownerSealOpen = false, deps = {} }) {
// SP3-b: печать на обычном GO ИЛИ owner-seal (перевешивает NO-GO/degraded). decideSeal —
// единый «мозг» решения; ни GO, ни owner-seal → нет печати (как раньше при !wired-GO).
if (!decideSeal({ verdict, ownerSealOpen }).seal) return { sealed: false };
const fp = String((event && event.tool_input && event.tool_input.file_path) || '');
const content = String((event && event.tool_input && event.tool_input.content) ?? '');
const key = deps.key !== undefined ? deps.key : (deps.resolveReceiptKey ? deps.resolveReceiptKey() : null);
if (SPEC_PATH_RE.test(fp)) {
const r = deps.sealArtifact({ md: content, verdict, key, judgeMode });
const r = deps.sealArtifact({ md: content, verdict, key, judgeMode, ownerSealOpen });
if (r && r.sealed && deps.persistArtifact) deps.persistArtifact(r.seal);
return { sealed: !!(r && r.sealed), kind: 'artifact' };
}
@@ -370,13 +377,14 @@ export function sealOnWiredGo({ event, verdict, judgeMode, deps = {} }) {
// T6 «зубы» наставника (решение владельца 2026-06-12): freeze-gate ПЕРЕД печатью
// плана. mentorGate инъектируется прод-сборкой ТОЛЬКО при mentorSeamActive() —
// выключен рубильник → undefined → печать как раньше. Бросок гейта → fail-CLOSE.
if (typeof deps.mentorGate === 'function') {
// SP3-b: на owner-seal mentor-gate ПРОПУСКАЕТСЯ (владелец перевешивает И судью, И наставника).
if (!ownerSealOpen && typeof deps.mentorGate === 'function') {
let g;
try { g = deps.mentorGate({ content }); } catch { g = { pass: false, reason: 'mentor freeze-gate бросил (fail-CLOSE)' }; }
if (!g || g.pass !== true) return { sealed: false, kind: 'plan', reason: `mentor freeze-gate: ${(g && g.reason) || 'нет pass'}` };
}
const cur = deps.loadCurrentArtifact ? deps.loadCurrentArtifact() : null;
const r = deps.sealPlan({ md: content, currentArtifact: cur, verdict, key, judgeMode });
const r = deps.sealPlan({ md: content, currentArtifact: cur, verdict, key, judgeMode, ownerSealOpen });
if (r && r.sealed && deps.persistPlan) deps.persistPlan(r.seal);
return { sealed: !!(r && r.sealed), kind: 'plan' };
}
@@ -399,12 +407,25 @@ export function bindingHashForJudge({ content, functionName } = {}) {
function sealTurnProd(event, verdict, mode) {
const sessionId = (event && event.session_id) || 'unknown';
const dir = runtimeDir();
// SP3-b owner-seal: владелец подписал owner-seal:<хеш тела> (escape-грант). Хеш считаем над
// РЕАЛЬНЫМ телом записи (как judged_hash на GO) → работает и на degraded/NO-GO, где вердиктного
// хеша нет. Спека → judgedHashOf(sealableArtifact); план → planId(steps) (как в карточке арбитража).
// Тотально (try): сбой расчёта → ownerSealOpen=false (печать как раньше), баг не кирпичит судью.
let ownerSealOpen = false;
try {
const fp = String((event && event.tool_input && event.tool_input.file_path) || '');
const content = String((event && event.tool_input && event.tool_input.content) ?? '');
const hash = SPEC_PATH_RE.test(fp) ? judgedHashOf(sealableArtifact(content))
: PLAN_PATH_RE.test(fp) ? planId(sealablePlan(content).steps)
: null;
if (hash) ownerSealOpen = escapeGrantOpen(ownerSealAction(hash), loadFloorEscapes(sessionId), loadConsumed(sessionId));
} catch { ownerSealOpen = false; }
// Способ B (Фаза 2): судья — хук ПОСЛЕ наставника, поэтому вердикт наставника уже свежий
// (mentor-GO + персист вердикта для plan_hash). Судья САМ печатает план здесь, через
// sealOnWiredGo + freeze-gate (verity/VA-9). Прежний «фикс дедлока» (судья сохранял judge-GO,
// наставник печатал в Post) снят — порядок теперь правильный.
return sealOnWiredGo({
event, verdict, judgeMode: mode,
event, verdict, judgeMode: mode, ownerSealOpen,
deps: {
resolveReceiptKey: () => resolveReceiptKey(),
sealArtifact, sealPlan,
+37
View File
@@ -500,3 +500,40 @@ describe('M7 наблюдаемость degraded — классификация
expect(degraded.at).toBe(777);
});
});
// SP3-b: owner-seal проводка — оживление печати при NO-GO/degraded + пропуск mentor-gate.
describe('SP3-b owner-seal проводка (sealOnWiredGo + runJudgeTurn)', () => {
it('sealOnWiredGo: NO-GO + ownerSealOpen → sealArtifact зовётся (override перевешивает)', () => {
const event = { tool_name: 'Write', tool_input: { file_path: 'docs/superpowers/specs/x-design.md', content: '## R {#r}\nt' } };
const calls = [];
sealOnWiredGo({ event, verdict: { wired: true, decision: 'NO-GO' }, judgeMode: 'live-block', ownerSealOpen: true,
deps: { sealArtifact: () => { calls.push('artifact'); return { sealed: true }; }, persistArtifact: () => {} } });
expect(calls).toEqual(['artifact']);
});
it('sealOnWiredGo: plan + ownerSealOpen → mentor freeze-gate ПРОПУСКАЕТСЯ, печать встаёт', () => {
const event = { tool_name: 'Write', tool_input: { file_path: 'docs/superpowers/plans/x.md', content: '```steps-json\n[{"op":"Edit","object":"a","ref":"r"}]\n```' } };
let mentorCalled = false;
const r = sealOnWiredGo({ event, verdict: { wired: true, decision: 'NO-GO' }, judgeMode: 'live-block', ownerSealOpen: true,
deps: { loadCurrentArtifact: () => ({ artifact_id: 'AID' }), sealPlan: () => ({ sealed: true, seal: {} }), persistPlan: () => {},
mentorGate: () => { mentorCalled = true; return { pass: false, reason: 'x' }; } } });
expect(mentorCalled).toBe(false);
expect(r.sealed).toBe(true);
});
it('sealOnWiredGo: NO-GO без owner-seal → печати нет (sealArtifact не зовётся)', () => {
const event = { tool_name: 'Write', tool_input: { file_path: 'docs/superpowers/specs/x-design.md', content: 'x' } };
const calls = [];
sealOnWiredGo({ event, verdict: { wired: true, decision: 'NO-GO' }, judgeMode: 'live-block',
deps: { sealArtifact: () => { calls.push('a'); return {}; } } });
expect(calls).toEqual([]);
});
it('runJudgeTurn: wired:false (degraded) + judged → onWiredSeal ВСЁ РАВНО зовётся (оживление проводки)', async () => {
const seen = [];
await runJudgeTurn(planEv(), {
mode: 'shadow', judgeActiveImpl: () => true, apiKey: '',
transport: async () => okText,
logImpl: () => {}, warnImpl: () => {}, sealLogImpl: () => {}, nowMs: 1,
onWiredSeal: (ev, v) => { seen.push(v && v.wired); return { sealed: false }; },
});
expect(seen).toEqual([false]);
});
});