From a312cd782fd8d76252a4980f3c12006e7b2facde 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:09:11 +0300 Subject: [PATCH] =?UTF-8?q?fix(enforce):=20hole=202=20=E2=80=94=20Task/Age?= =?UTF-8?q?nt=20count=20as=20mutating=20actions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brain-retro #5 candidate C, hole 2: enforce-classifier-match.mjs's MUTATING_TOOLS set missed Task/Agent, so delegating mutations via Task() bypassed the rule. Added Task and Agent to the set; nodeMatches already handles Task.subagent_type matching. Regression test asserts Task with matching subagent_type does NOT block (keeps the existing nodeMatches Task path intact). Co-Authored-By: Claude Opus 4.7 (1M context) --- tools/enforce-classifier-match.mjs | 2 +- tools/enforce-classifier-match.test.mjs | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/tools/enforce-classifier-match.mjs b/tools/enforce-classifier-match.mjs index 8f1a2af70..d7aaa4e13 100644 --- a/tools/enforce-classifier-match.mjs +++ b/tools/enforce-classifier-match.mjs @@ -28,7 +28,7 @@ import { const RULE_KEY = 'classifier-mismatch'; const CONFIDENCE_THRESHOLD = 0.7; -const MUTATING_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'Bash']); +const MUTATING_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'Bash', 'Task', 'Agent']); /** Normalize a node id: strip "superpowers:" / "skill:" prefix; allow #ID. */ function normalizeNode(s) { diff --git a/tools/enforce-classifier-match.test.mjs b/tools/enforce-classifier-match.test.mjs index 0dea6c4b1..1a6e90af4 100644 --- a/tools/enforce-classifier-match.test.mjs +++ b/tools/enforce-classifier-match.test.mjs @@ -103,4 +103,26 @@ describe('enforce-classifier-match / decide', () => { }); expect(r.block).toBe(false); }); + + it('blocks when Task subagent is spawned without matching recommendation (hole 2)', () => { + const r = decide({ + toolUses: [{ name: 'Task', input: { subagent_type: 'general-purpose', prompt: 'do stuff' } }], + recommendation: 'superpowers:writing-plans', + confidence: 0.9, + assistantText: '', + override: null, + }); + expect(r.block).toBe(true); + }); + + it('does NOT block when Task subagent matches recommendation (regression — Task should count as match when right type)', () => { + const r = decide({ + toolUses: [{ name: 'Task', input: { subagent_type: 'writing-plans', prompt: '...' } }], + recommendation: 'writing-plans', + confidence: 0.9, + assistantText: '', + override: null, + }); + expect(r.block).toBe(false); + }); });