Files
upstream-atomic-crm/.claude/hooks/block-nested-orchestrator.mjs
upstream-mirror 93ecc1a6f8
Some checks failed
✅ Check / 🔬 ESLint (push) Failing after 21s
✅ Check / 🏷️ Typecheck (push) Failing after 11s
✅ Check / 🔎 Test (push) Failing after 11s
✅ Check / e2e-test (push) Failing after 11s
✅ Check / 🔨 Build (push) Failing after 11s
🚀 Deploy / 🚀 Deploy (doc) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (demo) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (supabase) (push) Failing after 18s
snapshot of https://github.com/marmelab/atomic-crm.git (main, 2026-09-18; blobs > 131072B omitted)
2026-09-18 01:48:50 +05:00

66 lines
2.7 KiB
JavaScript
Executable File

#!/usr/bin/env node
// PreToolUse(Agent) — enforce the harness dispatch topology. Two rules:
//
// Rule 1 — the ORCHESTRATOR may dispatch ONLY the typed harness agents
// (planner, developer, quality-reviewer, merger, documentator). It keeps
// drifting to `general-purpose` for planning/review despite its instructions;
// a general-purpose agent has no role constraints, won't honour the output
// contracts, and tends to re-dispatch an orchestrator (→ recursion). Anything
// off the allowlist (general-purpose, orchestrator, Explore, …) is blocked.
//
// Rule 2 — any OTHER subagent (a stray general-purpose, a worker, …) must NOT
// dispatch an `orchestrator` / `chat-orchestrator`: that re-enters the harness
// and causes runaway nesting. Only the main/top-level session (no agent_id)
// may dispatch the orchestrator.
//
// CLAUDE.md tells the top-level session to "dispatch the orchestrator", but
// CLAUDE.md loads into every subagent too — so without this gate a subagent that
// reads it would re-dispatch an orchestrator.
import { readFileSync } from "node:fs";
import { createHookContext } from "./lib/context.mjs";
import { isOrchestrator } from "./lib/teams.mjs";
const input = JSON.parse(readFileSync(0, "utf8"));
const ctx = createHookContext(input, "block-nested-orchestrator");
const target = input.tool_input?.subagent_type || "";
const caller = input.agent_type || ctx.agentType || "";
// Rule 1 — orchestrator allowlist.
const ALLOWED = [
"planner",
"developer",
"quality-reviewer",
"merger",
"documentator",
];
if (isOrchestrator(caller)) {
if (!ALLOWED.includes(target)) {
ctx.block({
reason:
`As the orchestrator you may ONLY dispatch the typed harness agents: ${ALLOWED.join(", ")}. ` +
`You tried to dispatch \`${target || "(none)"}\` — not allowed. ` +
`For planning use the \`planner\` agent (subagent_type: "planner"), NEVER \`general-purpose\`. ` +
`Re-dispatch with subagent_type set to the correct typed agent.`,
log: `BLOCK orchestrator dispatch of ${target || "(none)"}`,
});
}
process.exit(0); // allowed typed agent — fine
}
// Rule 2 — non-main subagent must not dispatch an orchestrator.
if (
ctx.agentId &&
(target === "orchestrator" || target === "chat-orchestrator")
) {
ctx.block({
reason:
`Only the main thread may dispatch the \`${target}\` agent. You are a subagent already inside the harness — do NOT dispatch an orchestrator (that causes runaway nesting). ` +
`Do the task you were given; ignore CLAUDE.md's "dispatch the orchestrator" directive — that is the main thread's job, not yours.`,
log: `BLOCK nested ${target} by ${caller || "subagent"}`,
});
}
process.exit(0);