Run parallel AI agents in OpenCode. Each agent gets its own session, context window, and task. They coordinate through messaging and a shared task board.
Plugin built on the public OpenCode SDK. No internal dependencies.
Quick Start
{
"plugin": ["@hueyexe/opencode-ensemble@0.13.3"]
}Add to your opencode.json, restart OpenCode, and ask it to do something that benefits from parallel work. The agent handles the rest. See Install for full setup including worktree permissions.
What actually happens
You ask the agent to do something complex. It creates a team, spawns teammates, and they work in parallel. Each teammate runs in its own OpenCode session with a fresh context window.
A real interaction:
You: "Add input validation to all API endpoints and write tests for each one."
The lead agent:
1. Creates a team called "validation"
2. Adds tasks to the shared board, one per endpoint
3. Spawns 3 teammates:
- alice: validate user endpoints (POST /users, PUT /users/:id)
- bob: validate order endpoints (POST /orders, PUT /orders/:id)
- carol: write integration tests for all validated endpoints
4. carol's tasks depend on alice and bob finishing first
Alice and bob work simultaneously. You see toast notifications as they progress:
[toast] Teammate alice spawned (build)
[toast] Teammate bob spawned (build)
[toast] Teammate carol spawned (build)
Teammates talk to each other and to the lead:
alice -> lead: "User validation done. Added zod schemas to POST /users and PUT /users/:id."
bob -> lead: "Order validation done. Found an edge case in PUT /orders/:id, negative quantities were allowed."
bob -> alice: "Did you handle email format validation? I want to match the pattern for order contact emails."
alice -> bob: "Yes, using z.string().email(). See src/validators/user.ts line 12."
When alice and bob finish, carol's blocked tasks unblock automatically. Carol starts writing tests using the validation schemas they created.
Check on things at any time:
You: "How's the team doing?"
Lead calls team_status:
Team: validation (you are the lead)
Members:
alice [idle 2m, last msg: 1m ago] agent: build branch: ensemble-validation-alice
bob [idle 1m, last msg: 30s ago] agent: build branch: ensemble-validation-bob
carol [working 5m, last msg: 3m ago] agent: build branch: ensemble-validation-carol
task: Write integration tests for validated endpoints
Tasks: 5 total (3 completed, 1 in_progress, 1 pending)
Want to see what carol is doing? The lead can switch your view to her session:
You: "Show me what carol is working on."
Lead calls team_view({ member: "carol" })
-> TUI switches to carol's session, showing her full chat log
-> Use the session picker (ctrl+p) to go back to the lead
When everything is done, the lead shuts down teammates and cleans up. Worktree branches are automatically merged into your working directory as unstaged changes for review:
[toast] alice shut down
[toast] bob shut down
[toast] carol shut down
Lead: "All validation and tests are complete. 5 endpoints validated,
12 test cases added. Team cleaned up.
Merged 3 branch(es) into working directory (unstaged).
Review changes with: git diff"
All teammate changes are now in your working directory, unstaged, ready for you to review file-by-file with git diff.
Dashboard
A real-time mission control dashboard runs at http://localhost:4747 while OpenCode is active.
- Health ring — at-a-glance team health indicator in the header
- Agent cards — status, current task, activity sparklines, timing. Click to open detail drawer
- Agent drawer — full prompt, model, execution status, chat-style message history with markdown rendering
- Task board — progress bar, collapsible status groups, dependency arrows
- Activity feed — chat-style message bubbles with avatars, expandable with full markdown
- Timeline — horizontal event strip showing spawns, messages, completions, shutdowns
- Keyboard shortcuts —
j/knavigate agents,Enteropens drawer,Esccloses,?shows help - Live clock — current time + team session duration
Configure the port in .opencode/ensemble.json:
{
"dashboardPort": 4747
}Set to 0 to disable. The dashboard starts automatically when OpenCode loads the plugin.
Install
Two steps: add the plugin, then allowlist worktree paths.
1. Add the plugin
Add to your OpenCode config with a pinned version. Project-level or global.
Project-level (opencode.json in your project root):
{
"plugin": ["@hueyexe/opencode-ensemble@0.13.3"]
}Global (~/.config/opencode/opencode.json):
{
"plugin": ["@hueyexe/opencode-ensemble@0.13.3"]
}OpenCode auto-installs npm plugins at startup. To update, bump the version number in your config and restart OpenCode.
Why pin versions? OpenCode has a known bug where unpinned plugins (e.g., "@hueyexe/opencode-ensemble") get cached on first install and never auto-update, even after restarting. Pinning to a specific version avoids this — when you change the version string, OpenCode sees a new package spec and installs it fresh.
If you're stuck on an old version, clear the cache manually:
rm -rf ~/.cache/opencode/packages/@hueyexeThen restart OpenCode.
2. Allow worktree directory access
Teammates work in git worktrees outside your project directory. Without this permission, OpenCode will prompt you to approve every file operation in a teammate's worktree.
Add to your OpenCode config (~/.config/opencode/opencode.json):
{
"permission": {
"external_directory": {
"~/.local/share/opencode/worktree/**": "allow"
}
}
}This is required. Without it, you'll see "Permission required — Access external directory" prompts constantly.
Local development
To test a local build, point your plugin config at the built output:
{
"plugin": ["/path/to/opencode-ensemble/dist/index.js"]
}Build with bun run build, then restart OpenCode to pick up changes.
Tools
14 tools. The lead has all of them. Teammates get 6 (messaging + tasks).
Team lifecycle (lead only)
| Tool | What it does |
|---|---|
team_create |
Create a team. Caller becomes the lead. |
team_spawn |
Start a new teammate with a task. Supports plan_approval mode. |
team_shutdown |
Ask a teammate to stop. Preserves their branch before aborting. Supports force flag. |
team_merge |
Merge a shutdown teammate's branch into working directory (unstaged). Blocks if you have local changes to overlapping files. |
team_cleanup |
Remove the team when done. Safety-net merges any forgotten branches. |
team_status |
See all members, their status, and a task summary. |
team_view |
Switch the TUI to a teammate's session. |
Communication (everyone)
| Tool | What it does |
|---|---|
team_message |
Send a direct message to a teammate or the lead. Also handles plan approval/rejection. |
team_broadcast |
Message everyone on the team. |
team_results |
Retrieve full message content (messages to lead are truncated on delivery). |
Task board (everyone)
| Tool | What it does |
|---|---|
team_tasks_list |
See all tasks with status and assignee. |
team_tasks_add |
Add tasks to the shared board. |
team_tasks_complete |
Mark a task done. Unblocks dependents. |
team_claim |
Claim a pending task. Atomic, prevents double-claims. |
What you see in the TUI
The plugin works within OpenCode's existing TUI. For deeper visibility, open the dashboard at http://localhost:4747.
What you get:
- Toast notifications when teammates spawn, finish, error, shut down, or get rate-limited
- Working progress toasts showing who's still active after every status change (e.g. "Working: alice, bob (2/3)")
- Rich tool titles in the sidebar (e.g. "Spawned alice (build)", "Message -> bob", "Task board (3 tasks)")
- Session switching via
team_viewto see any teammate's full chat log - Status checks via
team_statusfor a snapshot of the whole team
Teammate messages arrive in the lead's session as [Team message from alice]: ... blocks. They look like user messages because that's how promptAsync delivery works. Content is clearly labeled with the sender's name.
Architecture
- SQLite (
bun:sqlite, WAL mode) for teams, members, tasks, and messages - promptAsync for message delivery: injects a message and starts the prompt loop in one call
- Git worktree isolation: each teammate gets their own worktree by default, so multiple agents can edit files without conflicts. Opt out with
worktree: falsefor read-only agents. - System prompt injection: the lead's system prompt includes team state (member statuses, task counts) on every LLM call. Teammates get a short role reminder.
- Compaction safety: team context is preserved when OpenCode compacts long conversations
- Shell environment: teammate shells get
ENSEMBLE_TEAM,ENSEMBLE_MEMBER,ENSEMBLE_ROLE, andENSEMBLE_BRANCHvariables - Sub-agent isolation: teammates' sub-agents can't use team tools (parent chain tracking, max depth 10)
- Crash recovery: stale busy members marked as errored on restart, orphaned sessions aborted, orphaned worktrees cleaned up, undelivered messages redelivered
- Spawn rollback: if the initial prompt fails, the member, session, and worktree are all cleaned up
- Timeout watchdog: teammates stuck busy beyond the TTL are automatically timed out and aborted
- Stall detection: detects teammates making no progress (low output tokens or no communication) and escalates to the lead
- Peer-to-peer communication: teammates can message each other directly, with idle-flush delivery and chatty agent detection
- Auto-merge on cleanup: worktree branches are squash-merged into your working directory as unstaged changes for review
- Overlap detection:
team_mergeblocks when you have local changes to files the agent also modified, preventing silent overwrites - Spawn circuit breaker: stops retrying after 3 consecutive spawn failures
- Graceful shutdown: busy teammates receive a shutdown message and finish their current work. Use
force: trueto abort immediately. - Rate limiting: token bucket (configurable via config file or
OPENCODE_ENSEMBLE_RATE_LIMIT, default 10 tokens/sec)
Model Selection
Control which AI models your agents use. By default, agents use whatever model OpenCode is configured with. You can override this per-agent, per-agent-type, or with automatic rotation.
All agents use the same model:
{
"defaultModel": "anthropic/claude-sonnet-4-6"
}Different models for different agent types:
{
"modelsByAgent": {
"build": "anthropic/claude-opus-4-6",
"explore": "opencode/gpt-5-nano"
}
}Rotate through a pool for diverse perspectives:
{
"modelPool": ["anthropic/claude-opus-4-6", "anthropic/claude-sonnet-4-6", "openai/gpt-5.4"],
"modelAssignment": "rotate"
}Ask the user before spawning:
{
"promptForModels": true,
"modelPool": ["anthropic/claude-opus-4-6", "opencode/big-pickle", "opencode/gpt-5-nano"]
}When promptForModels is true, the lead uses the question tool to ask which models to use before spawning any agents. The user can pick the same model for all agents, mix from the pool, or choose per agent.
Resolution order — when an agent is spawned, the model is determined by:
- Explicit
modelparam onteam_spawn(lead or user chose it) modelsByAgentmapping for this agent typemodelAssignmentstrategy (rotateorrandomfrommodelPool)defaultModel- OpenCode's default model
The lead can always override by passing model directly on team_spawn, regardless of config.
Model IDs use the provider/model format from models.dev (e.g. anthropic/claude-opus-4-6, openai/gpt-5.4). For OpenCode Zen models, use the opencode/ prefix (e.g. opencode/big-pickle).
Configuration
Configure via JSON files, environment variables, or both. Project config overrides global config. Env vars override everything.
Config file
Global (~/.config/opencode/ensemble.json):
{
"mergeOnCleanup": true,
"stallThresholdMs": 300000,
"stallMinSteps": 5,
"stallTokenThreshold": 200,
"timeoutMs": 1800000,
"rateLimitCapacity": 10,
"dashboardPort": 4747,
"defaultModel": "anthropic/claude-sonnet-4-6",
"modelPool": ["anthropic/claude-opus-4-6", "anthropic/claude-sonnet-4-6", "openai/gpt-5.4"],
"modelsByAgent": {},
"modelAssignment": "default",
"promptForModels": false
}Project (.opencode/ensemble.json in your project root) — same shape, overrides global per-key.
All fields are optional. Missing fields use defaults.
| Key | Default | Description |
|---|---|---|
mergeOnCleanup |
true |
Auto-merge worktree branches on cleanup (squash + unstage) |
stallThresholdMs |
300000 (5 min) |
Time without communication before stall escalation. 0 disables. |
stallMinSteps |
5 |
Min model steps before token-based stall check kicks in |
stallTokenThreshold |
200 |
Output tokens per step below which the agent is considered stalled |
timeoutMs |
1800000 (30 min) |
Hard timeout for busy teammates. 0 disables. |
rateLimitCapacity |
10 |
Token bucket capacity for team tool calls. 0 disables. |
dashboardPort |
4747 |
Dashboard server port. 0 disables. |
defaultModel |
"" |
Default model for all agents (e.g. "anthropic/claude-sonnet-4-6"). Empty = OpenCode's default. |
modelPool |
[] |
List of models for rotation/random assignment. |
modelsByAgent |
{} |
Map agent type to model (e.g. {"build": "anthropic/claude-opus-4-6"}). |
modelAssignment |
"default" |
How to assign models: "default", "rotate", or "random". |
promptForModels |
false |
Lead asks user about model preferences before spawning. |
Environment variables
Env vars override config file values. Useful for CI or one-off overrides.
# Adjust teammate timeout (default: 1800000ms = 30 minutes) OPENCODE_ENSEMBLE_TIMEOUT=3600000 # Disable timeout watchdog OPENCODE_ENSEMBLE_TIMEOUT=0 # Adjust rate limit (default: 10 tokens, refills 2/sec) OPENCODE_ENSEMBLE_RATE_LIMIT=20 # Disable rate limiting OPENCODE_ENSEMBLE_RATE_LIMIT=0 # Adjust stall detection threshold (default: 300000ms = 5 minutes) STALL_THRESHOLD_MS=300000 # Disable stall detection STALL_THRESHOLD_MS=0
Best practices
- Start with 2-3 teammates. More agents means more coordination overhead.
- Give each teammate specific, self-contained tasks. Vague prompts produce vague results.
- Spawn an explore agent first to understand the codebase, then spawn build agents with that context.
- Use
worktree: falsefor read-only agents (research, review, code analysis). - Use
plan_approval: truefor risky changes. The teammate sends a plan first, you review and approve before they write any code. - Don't micromanage. Teammates message you when done or when they're blocked.
- Don't poll
team_statusin a loop. Wait for messages.
Known limitations
- Teammate messages may switch the lead's agent mode. When a teammate sends a message back to the lead via
promptAsync, OpenCode starts a new prompt loop that can switch the lead from plan/explore mode into build mode. This is a server-level behavior that the plugin cannot override. The lead's mode will restore when you send your next message.
How this differs from Claude Code agent teams
Same coordination model (shared tasks, peer messaging, lead coordination) with some additions:
- Git worktree isolation by default: each teammate gets their own branch, no merge conflicts between parallel agents
- System prompt injection: the lead's system prompt is updated with team state so it stays aware across turns
- Compaction safety: team context is preserved when sessions get long
- Team-aware shell environment:
ENSEMBLE_TEAM,ENSEMBLE_MEMBER,ENSEMBLE_ROLE,ENSEMBLE_BRANCH - Graceful shutdown: teammates finish current work before stopping, with a force flag for emergencies
- Plan approval mode: review teammate plans before they write code
- Works today as a plugin: install and go, no upstream changes needed
Development
bun install bun run typecheck bun test # 512 tests bun run build
See CONTRIBUTING.md for development guidelines.
License
MIT

