From da6263c23549a4ac8ee8a33dbcb21d2965c8d52f 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, 26 May 2026 11:07:03 +0300 Subject: [PATCH] =?UTF-8?q?fix(enforce):=20hole=201=20=E2=80=94=20remove?= =?UTF-8?q?=20self-override=20via=20assistant=20text?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brain-retro #5 candidate C, hole 1: enforce-classifier-match.mjs allowed the agent to bypass the rule by writing 'override: ' in its own response (self-override = no enforcement). The user-vocabulary override phrases in enforce-override-vocab.json remain the only legitimate path. Added regression test asserting block on assistantText override when user prompt has no override phrase. Co-Authored-By: Claude Opus 4.7 (1M context) --- tools/enforce-classifier-match.mjs | 6 +++--- tools/enforce-classifier-match.test.mjs | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/tools/enforce-classifier-match.mjs b/tools/enforce-classifier-match.mjs index 4b9551a60..8f1a2af70 100644 --- a/tools/enforce-classifier-match.mjs +++ b/tools/enforce-classifier-match.mjs @@ -1,4 +1,4 @@ -#!/usr/bin/env node +#!/usr/bin/env node /** * Rule #8 — Classifier-mismatch enforce. * @@ -63,8 +63,8 @@ export function decide({ toolUses, recommendation, confidence, assistantText, ov const matched = toolUses.some((u) => nodeMatches(recommendation, u)); if (matched) return { block: false }; - // Allow explicit override: lines like "override: " in assistant text. - if (assistantText && /\boverride:\s+\S/i.test(assistantText)) return { block: false }; + // NOTE: prior \ self-bypass removed (retro #5 hole 1) - assistant + // cannot grant itself an override. User must use a vocabulary phrase. return { block: true, diff --git a/tools/enforce-classifier-match.test.mjs b/tools/enforce-classifier-match.test.mjs index ccbe57248..0dea6c4b1 100644 --- a/tools/enforce-classifier-match.test.mjs +++ b/tools/enforce-classifier-match.test.mjs @@ -72,14 +72,26 @@ describe('enforce-classifier-match / decide', () => { expect(r.block).toBe(false); }); - it('allows when explicit "override:" in assistant text', () => { + it('blocks (not allows) when only "override:" in assistant text — self-override removed (hole 1)', () => { const r = decide({ toolUses: [{ name: 'Edit', input: {} }], recommendation: 'foo:bar', confidence: 0.9, assistantText: 'override: simpler direct edit, foo:bar overkill here\n', + override: null, }); - expect(r.block).toBe(false); + expect(r.block).toBe(true); + }); + + it('blocks when assistant text has "override: reason" but user prompt has no override phrase (hole 1)', () => { + const r = decide({ + toolUses: [{ name: 'Edit', input: {} }], + recommendation: 'superpowers:writing-plans', + confidence: 0.9, + assistantText: 'override: just doing it quick', + override: null, + }); + expect(r.block).toBe(true); }); it('allows when override phrase present', () => {