Orchestrate teams of pi coding agents with DAG-aware dependency resolution, git worktree isolation, automatic retry, and structured JSON output.
- What Is This?
- When to Use
- Installation
- Configuration
- How Your Agent Uses It
- How It Works
- Dispatch JSON Reference
- CLI Reference
- Error Handling & Recovery
- Architecture
- License
pi-dispatch is a skill for agentic coding apps — primarily Claude Code, but also compatible with any tool that supports the ~/.agents/skills/ convention.
It addresses a problem: Claude Code subagents are becoming prohibitively expensive. So it learns Claude to effectively delegate work outside Anthropic's ecosystem.
You install the skill and configure which models to use. When you give your coding agent a task, it reads the skill instructions and automatically:
- Decomposes your task into a dependency graph of subtasks
- Writes a dispatch plan (JSON) describing which agents to spawn, in what order, with what prompts
- Runs the orchestrator, which executes agents in parallel waves inside isolated git worktrees
- Merges the results back into your branch — or reverts cleanly if anything fails
You never write dispatch JSON by hand. Your agent does.
Use pi-dispatch when:
- A coding task can be decomposed into subtasks (e.g., research → implement → test → document)
- Subtasks have ordering constraints (A must finish before B starts)
- You want parallel execution of independent subtasks
- You need isolation so agents don't interfere with each other's files
- You want automatic rollback if anything fails
Use the native subagents instead when:
- You're spawning Claude subagents (sonnet/opus/haiku)
- The task is simple enough for a single agent
- Python 3.12+
- Git (for worktree support)
- pi CLI installed and configured
- An OpenRouter API key (or another provider configured in
config.json)
The included install.sh detects which agentic apps you have installed and lets you choose where to install the skill:
git clone <this-repo-url>
cd pi-dispatch-skill
chmod +x install.sh
./install.shThe script will:
- Detect installed apps — checks for pi, Claude Code, Codex, and the universal
~/.agents/skills/directory - Let you select targets — choose one or more apps (e.g.,
"1 2"or"all") - Choose install method:
- Copy — snapshot the skill into each app's skills directory; re-run to upgrade
- Symlink — create a live link back to this repo; changes in the repo are reflected immediately (repo path must remain stable)
| App | Target Path |
|---|---|
| Universal | ~/.agents/skills/pi-dispatch/ |
| Pi | ~/.pi/agent/skills/pi-dispatch/ |
| Claude Code | ~/.claude/skills/pi-dispatch/ |
| Codex | ~/.agents/skills/pi-dispatch/ |
If you prefer manual installation, simply copy the pi_dispatch/ directory to the appropriate skills path for your app:
cp -r pi_dispatch/ ~/.pi/agent/skills/pi-dispatch/Edit pi_dispatch/config.json to customize model routing and defaults:
{
"provider": "openrouter",
"default_timeout_minutes": 5,
"task_routing": {
"hard": { "pi_tier": "hard" },
"moderate": { "pi_tier": "hard" },
"easy": { "pi_tier": "easy" },
"mechanical": { "pi_tier": "easy" },
"verification": { "pi_tier": "easy" }
},
"pi_models": {
"hard": ["xiaomi/mimo-v2.5-pro", "z-ai/glm-5.1"],
"easy": [
"qwen/qwen3.6-plus",
"deepseek/deepseek-v4-pro",
"z-ai/glm-5-turbo"
]
}
}| Field | Description |
|---|---|
provider |
Default model provider (e.g., openrouter) |
default_timeout_minutes |
Default timeout per agent if not specified in dispatch JSON |
task_routing |
Maps task type names to model tiers. Keys become valid task_type values in dispatch JSON. |
pi_models |
Model pools keyed by tier name. Keys must match the pi_tier values referenced in task_routing. |
Both task_routing keys and pi_models keys are arbitrary strings — not built-in constants. You can name them whatever you want, as long as they're consistent:
- Every key in
task_routingbecomes a validtask_typefor your agent to use in dispatch JSON - Every
pi_tiervalue intask_routingmust have a matching key inpi_models
For example, this is equally valid:
{
"task_routing": {
"complex": { "pi_tier": "premium" },
"simple": { "pi_tier": "budget" },
"review": { "pi_tier": "budget" }
},
"pi_models": {
"premium": ["anthropic/claude-sonnet-4", "openai/o3"],
"budget": ["qwen/qwen3.6-plus", "deepseek/deepseek-v4-pro"]
}
}The names below are just a suggested convention from the default config — not required:
| Type | Tier | Use Case |
|---|---|---|
hard |
hard | Complex reasoning, architecture, refactoring |
moderate |
hard | Non-trivial implementation tasks |
easy |
easy | Straightforward tasks, research, testing |
mechanical |
easy | Repetitive tasks, documentation, formatting |
verification |
easy | Review, validation, test running |
The tier determines which model pool is used. Within a pool, models are selected via round-robin across parallel agents, and the next model is tried on retry.
When you give your coding agent a task, here's what happens behind the scenes:
The agent's context includes SKILL.md — the skill instructions that teach it how to use pi-dispatch. It learns to decompose tasks, write dispatch JSON, and run the orchestrator.
The agent analyzes your request and breaks it into subtasks with dependency ordering. For example, given "Add dark mode toggle to settings page", it might plan:
Wave 0: [research] — analyze existing code and theming system
Wave 1: [implement] — build the toggle (depends on research)
Wave 2: [tests, docs] — write tests and update docs (both depend on implement)
The agent writes a JSON file to .pi_agent_dispatch/<timestamp>.json:
{
"task": "Add dark mode toggle to settings page",
"agents": [
{
"id": "research",
"task_type": "easy",
"depends": [],
"prompt": "Analyze the current settings page and theming system. Identify which files need changes. Output a concise summary."
},
{
"id": "implement",
"task_type": "hard",
"depends": ["research"],
"prompt": "Implement a dark mode toggle in the settings page. Add a toggle component, wire it to the theme system, persist the preference. Output a concise summary."
},
{
"id": "tests",
"task_type": "easy",
"depends": ["implement"],
"prompt": "Write tests for the dark mode toggle. Test toggle state, persistence, and theme application. Output a concise summary."
},
{
"id": "docs",
"task_type": "mechanical",
"depends": ["implement"],
"prompt": "Update docs to describe the dark mode feature. Output a concise summary."
}
]
}python pi_dispatch/orchestrate.py .pi_agent_dispatch/20260511T120000.jsonThe orchestrator prints structured JSON. Your agent reads it and reports back to you:
- Success — tells you what each subtask accomplished, shows the merged diff
- Conflict — explains the conflict and suggests a fix (e.g., adjusting dependencies)
- Failure — explains what went wrong; your branch is reverted to its original state
You just give your agent a task. It handles the decomposition, parallel execution, and merging. You get back a summary of what happened and a clean git diff to review.
┌─────────────────────────────────────────────────────────────────┐
│ 1. PARSE dispatch JSON │
│ • Validate agent definitions │
│ • Detect dependency cycles │
│ • Assign agents to waves (topological sort) │
├─────────────────────────────────────────────────────────────────┤
│ 2. CHECKPOINT current branch │
│ • Commit any uncommitted changes │
│ • Record HEAD SHA for potential revert │
├─────────────────────────────────────────────────────────────────┤
│ 3. CREATE integration branch + worktree │
│ • Branched from checkpoint │
│ • All agent work merges here first │
├─────────────────────────────────────────────────────────────────┤
│ 4. FOR EACH WAVE: │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ a. Create branch + worktree per agent (from integ.) │ │
│ │ b. Spawn pi CLI in each worktree (parallel) │ │
│ │ c. Wait for all agents in wave │ │
│ │ d. Retry failed agents with next model │ │
│ │ e. Merge successful branches → integration branch │ │
│ │ f. On merge conflict → pause (--continue to resume) │ │
│ │ g. On total wave failure → abort + revert │ │
│ └───────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ 5. MERGE integration branch → working branch │
│ • All agent work is now in your branch │
├─────────────────────────────────────────────────────────────────┤
│ 6. CLEANUP │
│ • Remove all worktrees │
│ • Delete all temporary branches │
│ • Print structured JSON output │
└─────────────────────────────────────────────────────────────────┘
Agents are organized into a Directed Acyclic Graph (DAG) based on their depends fields. The orchestrator performs a topological sort to determine execution order:
- Wave 0: Agents with no dependencies (run first)
- Wave 1: Agents that depend only on wave-0 agents
- Wave N: Agents that depend on agents in waves 0..N-1
Agents within the same wave run in parallel — but only if they touch disjoint files. If two agents modify the same file, they must be sequenced via depends.
Wave 0: [research]
│
Wave 1: [implement]
╱ ╲
Wave 2: [tests] [docs]
╲ ╱
Wave 3: [review] ← depends on both tests and docs
The orchestrator detects:
- Cycles →
CycleDetectedError(e.g., A depends on B, B depends on A) - Unknown dependencies →
UnknownDependencyError(referencing a non-existent agent id)
Each agent runs in its own git worktree — a separate working directory linked to its own branch. This means:
- Agents can't see or modify each other's files during execution
- Each agent's changes are captured on a dedicated branch
- The orchestrator controls when and how changes are merged
/tmp/pi_dispatch/<timestamp>/
├── research/ ← worktree for 'research' agent
├── implement/ ← worktree for 'implement' agent
├── implement-retry/ ← worktree for retry attempt
├── tests/ ← worktree for 'tests' agent
├── docs/ ← worktree for 'docs' agent
└── pi_integration/ ← integration worktree (merges land here)
After each wave completes:
- Agent worktrees are committed (
git add -A && git commit) - Each agent branch is merged into the integration branch (
git merge --no-ff) - If the merge succeeds, the agent's worktree and branch are cleaned up
- If the merge conflicts, the orchestrator:
- Aborts the merge
- Cleans up all wave worktrees
- Preserves the integration branch
- Prints a
conflict_pausedstate with instructions to--continue
After all waves succeed, the integration branch is merged into your working branch.
Each agent gets up to 2 attempts:
- First attempt: Uses the model selected by round-robin from the tier pool
- Retry attempt: Uses the next model in the same tier pool
Model selection:
- Agents with
task_typeget a model from the corresponding tier via round-robin - Agents with an explicit
pi_modeluse that exact model (no retry — deterministic choice)
Example with a 3-model pool:
Wave position 0 → qwen/qwen3.6-plus (first attempt)
deepseek/deepseek-v4-pro (retry)
Wave position 1 → deepseek/deepseek-v4-pro (first attempt)
z-ai/glm-5-turbo (retry)
Wave position 2 → z-ai/glm-5-turbo (first attempt)
qwen/qwen3.6-plus (retry)
Your coding agent creates this automatically. The reference below documents the format for understanding agent output and debugging.
{
"task": "One-line description of the overall task",
"agents": [ ... ]
}| Field | Required | Description |
|---|---|---|
id |
Yes | Unique identifier within this dispatch file |
task_type |
Yes, unless pi_model is set |
Key from config.json:task_routing (e.g., hard, easy, mechanical) |
pi_model |
No | Explicit model override; bypasses tier routing; no retry on failure |
timeout_minutes |
No | Per-agent timeout; defaults to config.default_timeout_minutes |
provider |
No | Per-agent provider override; defaults to config.provider |
depends |
No | List of agent ids that must complete before this one starts; defaults to [] |
prompt |
Yes | Self-contained prompt; must end with a summary instruction |
These are the rules your agent follows when creating dispatch plans:
- Parallel agents must touch disjoint files. If two agents modify the same file, they must be sequenced with
depends. - Every prompt ends with an explicit summary instruction (e.g., "Output a concise summary of changes made."). This summary becomes the
outputfield in results. - Prompts use relative paths only — pi runs inside the worktree, not the project root.
- Prompts are self-contained — agents don't share context. Each prompt includes all necessary information.
Parallel agents each run in their own worktree. Any tool that writes files into the repo directory — IDE indexes, language-server caches, linter caches, coverage outputs, or any cache keyed on file paths or timestamps — will produce a different file per agent. When the orchestrator merges those branches, the differing files cause conflicts that look like depends ordering errors but have nothing to do with your actual code changes.
The fix is always .gitignore, not depends. Before running a dispatch, make sure your .gitignore covers any files your tools generate automatically. Common offenders:
| Tool / ecosystem | Files to ignore |
|---|---|
| JetBrains IDEs | .idea/ |
| VS Code | .vscode/ |
| Python | **/__pycache__/, *.pyc, .pytest_cache/ |
| Coverage | .coverage, htmlcov/ |
| Any tool with a repo cache | .pi-lens/, .mypy_cache/, .ruff_cache/ |
To diagnose: after a conflict_paused failure, check git status inside any surviving worktree to see which unexpected files were created.
Your coding agent runs these commands. The reference below is for understanding what happens and for manual intervention.
python pi_dispatch/orchestrate.py <dispatch-file.json>When a merge conflict occurs, the orchestrator pauses and prints instructions. Your agent handles this automatically, but you can also resume manually:
# Resume with the same dispatch file
python pi_dispatch/orchestrate.py --continue .pi_agent_dispatch/<ts>-state.json
# Resume with a fixed dispatch file (e.g., after fixing depends)
python pi_dispatch/orchestrate.py --continue .pi_agent_dispatch/<ts>-state.json \
--json-override .pi_agent_dispatch/<fixed>.jsonAlready-merged agents are skipped — only failed/pending agents re-run from the preserved integration branch.
If the orchestrator crashes or is interrupted:
python pi_dispatch/orchestrate.py --cleanup .pi_agent_dispatch/<ts>-state.jsonThis removes all worktrees, deletes agent branches, and reverts the integration branch.
| Variable | Description | Default |
|---|---|---|
PI_CMD |
Override the pi CLI command (for testing) | pi |
| Code | Meaning |
|---|---|
| 0 | Success — all agents completed and merged |
| 1 | Dispatch error (validation, config, or unexpected failure) |
| 2 | Agent failure or merge conflict — check JSON output for details |
All agents succeeded. Changes are merged into your working branch.
{
"status": "complete",
"agents": {
"research": { "status": "merged", "output": "..." },
"implement": { "status": "merged", "output": "..." }
}
}A merge conflict occurred. Your working branch is untouched. The integration branch is preserved.
{
"status": "conflict_paused",
"failure_reason": "merge conflict: agent implement",
"resume_with": "python pi_dispatch/orchestrate.py --continue .pi_agent_dispatch/<ts>-state.json"
}Your agent reads this output, adjusts the dependency ordering if needed, and runs --continue.
All agents in a wave failed after retries. The orchestrator reverts everything — your working branch is reset to the checkpoint.
{
"status": "failed",
"reverted_to": "abc123...",
"failed_wave": 1,
"failure_reason": "non-zero exit",
"failed_agents": ["implement"]
}Your agent may show you these, or you can run them yourself:
# Everything from this dispatch
git diff <checkpoint_sha>..HEAD
# One specific agent's changes
git show <merge_sha>
# What one agent added vs. the previous merge
git diff <prev_merge_sha>..<this_merge_sha>pi_dispatch/
├── __init__.py # Package marker
├── config.json # Model routing and timeout configuration (edit this)
├── config.py # Config loading, dispatch parsing, validation
├── dag.py # DAG resolution: cycle detection, topological sort
├── exceptions.py # Domain exceptions (DispatchError hierarchy)
├── git_ops.py # Thin wrappers around git subprocess calls
├── model_selector.py # Round-robin and retry-next model selection
├── orchestrate.py # CLI entry point (only file touching sys.path)
├── orchestrator.py # Core Orchestrator class and cleanup utility
├── paths.py # Worktree/branch path computation (single source of truth)
├── process.py # Parallel subprocess runner for agent execution
├── SKILL.md # Skill instructions for your coding agent
└── state.py # State file for crash recovery and --continue
| Module | Responsibility |
|---|---|
SKILL.md |
Instructions injected into your agent's context; teaches it how to use pi-dispatch |
config.json |
Your file to edit — model pools, provider, timeouts |
orchestrate.py |
CLI entry point; adds parent dir to sys.path; delegates to orchestrator.main() |
orchestrator.py |
Orchestrator class: wave execution, merge, retry, abort, cleanup |
dag.py |
Topological sort, cycle detection, unknown dependency detection |
git_ops.py |
Git operations: worktree, branch, merge, checkpoint |
process.py |
Parallel subprocess execution with timeout support |
model_selector.py |
Model selection: round-robin by tier, next-on-retry |
config.py |
Config/dispatch file loading and validation |
paths.py |
Centralized path computation for worktrees and branches |
state.py |
State file persistence for crash recovery and --continue |
exceptions.py |
Exception hierarchy: DispatchError → specific error types |
-
Git worktrees for isolation — Each agent gets its own working directory and branch. No filesystem conflicts, no race conditions.
-
Integration branch as staging area — Agent branches merge into an integration branch first. Only when all agents succeed does the integration branch merge into your working branch.
-
Checkpoint + revert — Before starting, the orchestrator commits any uncommitted changes and records the SHA. On failure,
git reset --hardrestores the exact state. -
Automatic retry with model rotation — Failed agents retry with the next model in the tier pool. Explicit
pi_modelagents don't retry (deterministic choice). -
Conflict-aware resumption — Merge conflicts don't crash the system. The orchestrator pauses, preserves state, and lets the agent fix dependencies and
--continue.
MIT License — see LICENSE for details.