diff --git a/hooks/check-agent-misuse.sh b/hooks/check-agent-misuse.sh index ac4b43da..f1492528 100755 --- a/hooks/check-agent-misuse.sh +++ b/hooks/check-agent-misuse.sh @@ -2,8 +2,8 @@ # check-agent-misuse.sh — PreToolUse hook for Agent tool # # Detects when the orchestrator is likely using Agent to execute -# full stories (should use TeamCreate instead). Checks the Agent -# tool_input.prompt for story-level delegation patterns. +# full stories. Checks the Agent tool_input.prompt for story-level +# delegation patterns and points callers to natural-language teammate spawn. # # Exit codes: # 0 = allow (no story-level patterns detected) @@ -17,6 +17,7 @@ HIVE_ROOT="${HIVE_ROOT:-$(dirname "$SCRIPT_DIR")}" . "$HIVE_ROOT/hooks/common.sh" BYPASS_MARKER="__MESSAGES_SESSION_BYPASS__" +SPAWN_GUIDANCE="Use natural-language teammate spawn instead: describe the team and each teammate's tasks in your prompt." # Escape a literal string for safe use inside an ERE alternative. _escape_ere() { @@ -66,9 +67,9 @@ if echo "$prompt" | grep -qiE "$story_regex"; then if [ "$story_count" -ge 1 ]; then # Check for workflow execution signals (not just reading a story for context) if echo "$prompt" | grep -qiE '(execute.*stor|implement.*stor|workflow.*phase|development.*workflow|research.*implement.*test|review.*integrate)'; then - echo "BLOCKED: Agent tool used to execute story-level work. Use TeamCreate for story execution." >&2 + echo "BLOCKED: Agent tool used to execute story-level work. $SPAWN_GUIDANCE" >&2 echo "Detected $story_count story reference(s) with workflow execution patterns." >&2 - echo "The orchestrator must delegate stories via TeamCreate, not Agent." >&2 + echo "The orchestrator must delegate stories via natural-language teammate spawn, not Agent." >&2 exit 2 fi fi @@ -76,8 +77,8 @@ fi # Pattern 2: Agent prompt contains epic execution language if echo "$prompt" | grep -qiE '(execute.*epic|epic.*execution|execute all stories|run the stories)'; then - echo "BLOCKED: Agent tool used for epic-level execution. Use TeamCreate instead." >&2 - echo "The orchestrator delegates epics and stories via TeamCreate, not Agent." >&2 + echo "BLOCKED: Agent tool used for epic-level execution. $SPAWN_GUIDANCE" >&2 + echo "The orchestrator delegates epics and stories via natural-language teammate spawn, not Agent." >&2 exit 2 fi @@ -87,7 +88,7 @@ fi # `Agent` path documented in SKILL.md is "Sequential workflow steps within a # single story" — those descriptions name the step, not the whole story. if echo "$description" | grep -qiE '(story execution|execute (the )?(entire|full|whole) story|run (the )?(entire|full|whole) story|implement (the )?(entire|full|whole) story|execute all steps)'; then - echo "BLOCKED: Agent description indicates whole-story delegation. Use TeamCreate." >&2 + echo "BLOCKED: Agent description indicates whole-story delegation. $SPAWN_GUIDANCE" >&2 exit 2 fi diff --git a/tests/hive-hooks/check-agent-misuse.orchestrator-pattern.behavior.test.js b/tests/hive-hooks/check-agent-misuse.orchestrator-pattern.behavior.test.js index b847e9b4..05124b8b 100644 --- a/tests/hive-hooks/check-agent-misuse.orchestrator-pattern.behavior.test.js +++ b/tests/hive-hooks/check-agent-misuse.orchestrator-pattern.behavior.test.js @@ -6,7 +6,7 @@ * * Three cases: * a) Messages-API spawn with structured bypass marker — ALLOW - * b) TeamCreate spawn — ALLOW + * b) Natural-language teammate spawn emits no tool call, so unrelated tools remain ALLOW * c) Existing orchestrator-pattern catalog match — BLOCK */ @@ -19,6 +19,7 @@ const { spawnSync } = require('node:child_process'); const REPO_ROOT = path.join(__dirname, '..', '..'); const HOOK_PATH = path.join(REPO_ROOT, 'hooks', 'check-agent-misuse.sh'); +const SPAWN_GUIDANCE = /Use natural-language teammate spawn instead: describe the team and each teammate's tasks in your prompt/; function runHook(payload) { const result = spawnSync(HOOK_PATH, { @@ -70,14 +71,14 @@ test('case a — Messages-API bypass requires the structured marker', async (t) }); // --------------------------------------------------------------------------- -// Case (b): TeamCreate fallback path — ALLOW +// Case (b): Natural-language teammate spawn has no positive tool signal. // --------------------------------------------------------------------------- -test('case b — TeamCreate fallback path remains allowed', () => { +test('case b — unrelated tools remain allowed because only Agent calls are checked', () => { const result = runHook({ - tool_name: 'TeamCreate', + tool_name: 'SendMessage', tool_input: { - prompt: 'Execute story from .pHive/epics/cwc-2026-integration/stories/s10-a7-agent-spawn-flow-hook-relax.yaml via team workflow.', - description: 'team create for full story execution fallback', + prompt: 'Ask an existing teammate for status on a story workflow.', + description: 'message existing teammate', }, }); @@ -100,7 +101,7 @@ test('case c — existing orchestrator-pattern catalog still blocks story execut }); assert.equal(result.status, 2, `expected BLOCK, got exit=${result.status} stdout=${result.stdout} stderr=${result.stderr}`); - assert.match(result.stderr, /Use TeamCreate/, 'block reason should direct caller to TeamCreate'); + assert.match(result.stderr, SPAWN_GUIDANCE, 'block reason should direct caller to natural-language teammate spawn'); }); await t.test('description catalog: whole-story delegation phrasing blocks', () => { @@ -114,6 +115,7 @@ test('case c — existing orchestrator-pattern catalog still blocks story execut assert.equal(result.status, 2, `expected BLOCK, got exit=${result.status} stdout=${result.stdout} stderr=${result.stderr}`); assert.match(result.stderr, /whole-story delegation/i); + assert.match(result.stderr, SPAWN_GUIDANCE, 'block reason should use the shared spawn guidance'); }); await t.test('epic execution catalog still blocks', () => { @@ -127,5 +129,6 @@ test('case c — existing orchestrator-pattern catalog still blocks story execut assert.equal(result.status, 2, `expected BLOCK, got exit=${result.status} stdout=${result.stdout} stderr=${result.stderr}`); assert.match(result.stderr, /epic-level execution/i); + assert.match(result.stderr, SPAWN_GUIDANCE, 'block reason should use the shared spawn guidance'); }); });