A small multi-agent orchestrator built on the Claude Code CLI. One command takes a user prompt, decomposes it into a task list, dispatches each task to a stateless sub-agent, and synthesizes a final response.
node scripts/run.js "your prompt here"
Works with a Claude Pro / Max subscription or an Anthropic API key — whichever your local claude CLI is authenticated against. No ANTHROPIC_API_KEY env var required.
┌────────────────────────────────────────────┐
user prompt → │ head agent · decompose │ → state/session.json (tasks: pending)
│ head_agent.py OR .claude/agents/ │
│ head-agent.md │
└────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────┐
│ scripts/dispatch.js │ → state/session.json (tasks: completed/failed)
│ spawns `claude --agent sub-agent` per │
│ pending task whose deps are completed │
└────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────┐
│ head_agent.py summarize │ → state/session.json (final_summary)
└────────────────────────────────────────────┘
│
▼
final response
Four components, one shared state file (state/session.json):
head_agent.py— Python module + CLI. Two subcommands:decomposeandsummarize. Calls Claude to plan and to synthesize. Never executes the user's work itself..claude/agents/head-agent.md— Optional Claude-Code-native planner. Interactive variant of the Python decomposer; plans and tracks state across conversation turns. Also never executes.scripts/dispatch.js— Node runner. Readsstate/session.json, spawns a sub-agent per pending task (respectingdependencies), writes results back, marks taskscompletedorfailed..claude/agents/sub-agent.md— Claude Code subagent definition. Stateless single-task executor: gets one task object as its brief, returns a short plain-text result summary, retains nothing.
scripts/run.js wires the headless path together. See Two ways to drive it for when to pick which planner.
- Python 3.10+ (no third-party Python packages required)
- Node.js 18+ (uses only Node built-ins)
- Claude Code CLI (
claude) installed and onPATH. Install: https://docs.claude.com/claude-code
That's it — no pip install, no npm install.
This project runs entirely through the claude CLI, so it inherits whatever auth the CLI is logged into. Either path works:
- Claude Pro / Max subscription — run
claudeonce interactively and complete the browser login flow. The orchestrator then uses your subscription quota for every model call. No env vars needed. - Anthropic API key — run
claude /logininside Claude Code and pick the API-key option (recommended), or exportANTHROPIC_API_KEYin your shell. Billed via console.anthropic.com.
Pick one — you don't need both. To verify, run claude -p "say hello"; if you get a response, the orchestrator will work.
node scripts/run.js "Compare the climate policies of Norway and Sweden and summarize the main differences."run.js will:
- Call
head_agent.py decompose→ writesstate/session.jsonwith a task list. - Call
scripts/dispatch.js→ spawns sub-agents to execute each task in dependency order, writing each task'sresultback intostate/session.json. - Call
head_agent.py summarize→ reads the completed session and prints the final user-facing response to stdout.
state/session.json is left in place after the run so you can inspect what happened.
The repo ships two equivalent planners, sharing one canonical state file at state/session.json:
head_agent.py plans and summarizes; scripts/run.js and scripts/dispatch.js glue it to the sub-agents.
node scripts/run.js "your prompt here"Use this when invoking from a shell, CI job, or another program. It's the fastest path from prompt to final response and the components are non-interactive.
A Claude Code subagent that plans and tracks state across turns. Inside a Claude Code session, ask Claude to use the head-agent subagent:
> use the head-agent to plan: build a CLI tool for fuzzy-finding bookmarks
It will read state/session.json, produce (or update) the task list, write the state back, and end its message with the JSON state block. On follow-up turns it amends the plan, marks tasks done, or records failures — the JSON block is always the contract.
When you're ready to execute the planned tasks, switch to the dispatcher:
node scripts/dispatch.js # uses state/session.json
python3 head_agent.py summarize # final responseBoth planners write the same schema to the same file, so you can mix them freely: plan interactively, execute headlessly, summarize either way.
orchestrator/
├── head_agent.py # plan + summarize (Python, drives `claude -p`)
├── requirements.txt # no third-party deps — comment-only
├── scripts/
│ ├── run.js # main entry point
│ └── dispatch.js # sub-agent runner
├── .claude/
│ └── agents/
│ ├── head-agent.md # Claude-Code-native planner (interactive)
│ └── sub-agent.md # stateless task executor (Claude Code subagent)
└── state/
└── session.json # canonical session state (shared by all four components)
Every component reads and writes this same shape — the markdown head agent emits it inline at the end of every response, the Python head agent writes it on decompose/summarize, and the dispatcher updates per-task status/result/error in place.
You don't have to use run.js — every stage has a standalone CLI.
Decompose only:
python3 head_agent.py decompose "your prompt" --session state/session.json --prettyRun dispatch on an existing session:
node scripts/dispatch.js state/session.jsonSummarize a completed session:
python3 head_agent.py summarize --session state/session.jsonThe head agent class is also importable:
from head_agent import HeadAgent
agent = HeadAgent(
session_path="state/session.json",
model="claude-sonnet-4-6", # optional
claude_bin="claude", # optional — override path to the CLI
timeout_s=300, # optional — per-call timeout
)
state = agent.decompose("your prompt")
# … run dispatch …
final = agent.summarize()Every Claude call shells out to claude -p under the hood, so the importable API has the same auth story as the CLI: subscription or API key, whichever the claude binary is logged into.
Environment variables (all optional):
| Var | Default | Used by | Purpose |
|---|---|---|---|
PYTHON_BIN |
python3 |
run.js | Python interpreter to invoke. |
SESSION_FILE |
./state/session.json |
run.js | Override the shared session-state path. |
CLAUDE_BIN |
claude |
head_agent.py, dispatch.js | Path to the Claude Code CLI. |
HEAD_AGENT_TIMEOUT_S |
300 |
head_agent.py | Per-call timeout for claude -p invocations from the head agent. |
DISPATCH_TIMEOUT_MS |
600000 |
dispatch.js | Per-task wall-clock timeout. |
DISPATCH_PERMISSION |
bypassPermissions |
dispatch.js | Permission mode passed to spawned claude processes. |
No ANTHROPIC_API_KEY is needed — authentication is handled entirely by the claude CLI (see Authentication). The model used by the head agent defaults to claude-sonnet-4-6 and is overridable per command with --model.
The dispatcher spawns claude with --permission-mode bypassPermissions so sub-agents can run unattended. This skips Claude Code's interactive permission prompts — which is what you want for headless orchestration, but it also means a sub-agent will execute its tools (file edits, shell commands, web requests) without asking you first. If you'd rather be in the loop, set DISPATCH_PERMISSION=acceptEdits or default.
Both Claude Code subagents (.claude/agents/head-agent.md and .claude/agents/sub-agent.md) are normal subagent files — edit their frontmatter to change available tools, or edit the body to change the contract.
The dispatcher passes one task object as the user message; everything else flows from sub-agent.md.
- No follow-up questions. The sub-agent must act on the task brief alone — there's no human in the loop during dispatch.
- Final message is plain prose. The dispatcher captures stdout verbatim as
task.result. If you change this contract, also change howhead_agent.py summarizeconsumes results.
The markdown head-agent shares state with the rest of the pipeline via state/session.json.
- Never executes. It plans, updates state, and reports — full stop. Execution belongs to the sub-agents that the dispatcher spawns.
- Schema-compatible output. The JSON block at the end of every response must match the canonical schema shown above. Other components consume it directly.
- Only
state/session.jsonis writable. No other files should be touched by the planner.
claude: command not found during dispatch.
The Claude Code CLI isn't installed or isn't on PATH. Install it, or set CLAUDE_BIN=/full/path/to/claude.
unrecognized arguments: --agent sub-agent or similar.
Older Claude Code CLI versions may use a different flag for selecting an agent. Update Claude Code, or edit the args array near the top of runSubAgent in scripts/dispatch.js.
claude CLI not authenticated, or claude -p hangs / fails immediately.
Run claude once in an interactive terminal and complete the login flow (Pro/Max subscription OR API-key option). Once claude -p "hi" returns a response, the orchestrator will work too. No ANTHROPIC_API_KEY is required if you authenticated via subscription.
Some tasks are stuck pending after dispatch finishes.
Their dependencies failed. Inspect state/session.json for the upstream error field. The summarize step will still run and will acknowledge the failed tasks.
Sub-agent returns an empty summary.
The dispatcher treats this as a failure (error: "sub-agent returned an empty summary"). Re-running typically fixes transient cases; persistent ones usually mean the task description was too ambiguous for the sub-agent to act on.
{ "run_id": "run-20260520T031200Z-ab12cd", "user_prompt": "...", "created_at": "2026-05-20T03:12:00+00:00", "updated_at": "2026-05-20T03:14:55+00:00", "status": "initialized | decomposed | summarizing | finalized | failed", "model": "claude-sonnet-4-6", "tasks": [ { "id": "fetch-data", "description": "Fetch climate policy summaries for both countries.", "agent_type": "research", "dependencies": [], "inputs": {}, "outputs": {}, "status": "pending | in_progress | completed | failed | skipped", "result": "Plain-text summary from the sub-agent.", "error": null } ], "final_summary": "The user-facing response, populated by `summarize`.", "metadata": {} }