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:
@@ -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,
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user