GitHub - hueyexe/opencode-ensemble: Agent teams for OpenCode. Run multiple agents in parallel with messaging, shared tasks, and coordinated execution.

11 min read Original article ↗

OpenCode Ensemble - Parallel agents. One coordinated team.

npm version npm downloads tests TypeScript OpenCode SDK license

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.

Ensemble Dashboard

  • 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 shortcutsj/k navigate agents, Enter opens drawer, Esc closes, ? 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/@hueyexe

Then 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_view to see any teammate's full chat log
  • Status checks via team_status for 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: false for 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, and ENSEMBLE_BRANCH variables
  • 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_merge blocks 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: true to 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:

  1. Explicit model param on team_spawn (lead or user chose it)
  2. modelsByAgent mapping for this agent type
  3. modelAssignment strategy (rotate or random from modelPool)
  4. defaultModel
  5. 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: false for read-only agents (research, review, code analysis).
  • Use plan_approval: true for 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_status in 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