From a846eed9dcf57e38463fcb8c3e07d4a6b8f9ed84 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:11:29 +0300 Subject: [PATCH] =?UTF-8?q?fix(enforce):=20hole=205=20=E2=80=94=20tighten?= =?UTF-8?q?=20nodeMatches=20to=20exact/segment=20match?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brain-retro #5 candidate C, hole 5: nodeMatches() used free-form substring matching (s.includes(rec) || rec.includes(s)), which matched 'meta-planning' to a 'planning' recommendation. Tightened to exact match OR matching last segment after ':' / '#' (skill ns / registry id). Regression tests preserve: superpowers:writing-plans matches writing-plans, exact-name matches keep working. --- tools/enforce-classifier-match.mjs | 19 +++++++++++---- tools/enforce-classifier-match.test.mjs | 31 +++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/tools/enforce-classifier-match.mjs b/tools/enforce-classifier-match.mjs index d7aaa4e1..f782e6ad 100644 --- a/tools/enforce-classifier-match.mjs +++ b/tools/enforce-classifier-match.mjs @@ -40,13 +40,22 @@ function nodeMatches(recommendation, toolUse) { if (!recommendation || !toolUse) return false; const rec = normalizeNode(recommendation); if (!rec) return false; + // Hole 5 fix: exact match OR matching last segment after ':' / '#'. + // No generic substring (would match meta-planning to planning). + const matches = (candidate) => { + if (!candidate) return false; + if (candidate === rec) return true; + const recSegs = rec.split(/[:#]/); + const canSegs = candidate.split(/[:#]/); + const recLast = recSegs[recSegs.length - 1]; + const canLast = canSegs[canSegs.length - 1]; + return recLast === canLast; + }; if (toolUse.name === 'Skill') { - const s = normalizeNode(String(toolUse.input && toolUse.input.skill || '')); - if (s && (s === rec || s.includes(rec) || rec.includes(s))) return true; + return matches(normalizeNode(String(toolUse.input && toolUse.input.skill || ''))); } - if (toolUse.name === 'Task') { - const sub = String(toolUse.input && toolUse.input.subagent_type || '').toLowerCase(); - if (sub && rec.includes(sub)) return true; + if (toolUse.name === 'Task' || toolUse.name === 'Agent') { + return matches(String(toolUse.input && toolUse.input.subagent_type || '').toLowerCase()); } return false; } diff --git a/tools/enforce-classifier-match.test.mjs b/tools/enforce-classifier-match.test.mjs index 1a6e90af..fb076224 100644 --- a/tools/enforce-classifier-match.test.mjs +++ b/tools/enforce-classifier-match.test.mjs @@ -125,4 +125,35 @@ describe('enforce-classifier-match / decide', () => { }); expect(r.block).toBe(false); }); + + it('does not match meta-planning to planning recommendation (hole 5)', () => { + const r = decide({ + toolUses: [{ name: 'Skill', input: { skill: 'meta-planning' } }, { name: 'Edit', input: {} }], + recommendation: 'planning', + confidence: 0.9, + assistantText: '', + override: null, + }); + expect(r.block).toBe(true); + }); + + it('matches superpowers:writing-plans to writing-plans recommendation (regression — keep working)', () => { + expect(decide({ + toolUses: [{ name: 'Skill', input: { skill: 'superpowers:writing-plans' } }, { name: 'Edit', input: {} }], + recommendation: 'writing-plans', + confidence: 0.9, + assistantText: '', + override: null, + }).block).toBe(false); + }); + + it('matches exact-name skill regression — keep working', () => { + expect(decide({ + toolUses: [{ name: 'Skill', input: { skill: 'brainstorming' } }, { name: 'Edit', input: {} }], + recommendation: 'brainstorming', + confidence: 0.9, + assistantText: '', + override: null, + }).block).toBe(false); + }); });