fix(enforce): hole 2 — Task/Agent count as mutating actions

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) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-05-26 11:09:11 +03:00
parent ce02d1adad
commit 7e5c297394
2 changed files with 23 additions and 1 deletions
+1 -1
View File
@@ -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) {
+22
View File
@@ -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);
});
});