Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions hooks/check-agent-misuse.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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() {
Expand Down Expand Up @@ -66,18 +67,18 @@ 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
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

Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/

Expand All @@ -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, {
Expand Down Expand Up @@ -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',
},
});

Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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');
});
});