contrabass
🎸 A project-level orchestrator for AI coding agents — Go & Charm stack implementation of OpenAI's Symphony
Links
README
From the repo.
Contrabass
A project-level orchestrator for AI coding agents
Go + Charm stack reimplementation of OpenAI's Symphony (openai/symphony) — manage work, not agents

Contrabass is a terminal-first orchestrator for issue-driven agent runs, with an optional local web dashboard for live visibility.
Current scope
Today Contrabass ships with:
- A Cobra CLI with TUI, headless, and optional embedded web dashboard modes
- A
WORKFLOW.mdparser with YAML front matter, Liquid prompt rendering, and$ENV_VARinterpolation - Issue tracker adapters for Linear, GitHub Issues, and a built-in Internal Board (local filesystem, no external service required)
- Agent runners for Codex app-server, OpenCode, oh-my-opencode, OMX (oh-my-codex), and OMC (oh-my-claudecode)
- Git-worktree-based workspace provisioning under
workspaces/<issue-id>with non-git fallback for repositories without git - Teams: multi-agent coordination with a local task board, phased pipeline (plan → exec → verify), live TUI team table, and dual worker modes (tmux-based multi-process or goroutine-based in-process)
- An orchestrator with claim/release, BlockedBy gating, orphan claim recovery, branch advance verification, stall detection, deterministic retry backoff, liveness snapshots with agent stage classification and ETA estimation
- A Charm v2 terminal UI built with Bubble Tea, Bubbles, and Lip Gloss
- Ziikoo — a React dashboard (neo-brutalism theme, shadcn + Tailwind v4) with a three-pane IDE-style layout, live SSE streaming, queue navigation, stage progression pills, completion ETAs, issue detail sheets with Linear metadata and workflow timelines, team/worker tables, agent logs, and zh-CN localization
- Go unit/integration tests, TUI snapshot tests, and dashboard component/hook tests
- A tmux-based multi-process worker mode (default) alongside the in-process goroutine mode, with JSONL event logging, file-based heartbeats, dispatch queue, governance policies, and crash recovery
Requirements
- Go 1.25+
- Bun 1.3+ for the dashboard/landing workspace
- Git (workspace creation uses
git worktree) - tmux (required for the default tmux worker mode in team runs; not needed for goroutine mode)
- A supported agent runtime:
codex app-serveropencode serveoh-my-opencodeomx(oh-my-codex team runtime)omc(oh-my-claudecode team runtime)
- Tracker credentials for the backend you use:
- Linear:
LINEAR_API_KEY - GitHub:
GITHUB_TOKEN
- Linear:
From a fresh clone, run bun install once before using the JS/landing build and test commands.
Installation
Homebrew (macOS/Linux)
brew install junhoyeo/contrabass/contrabass
Nix
# Run without installing
nix --extra-experimental-features 'nix-command flakes' run github:junhoyeo/contrabass -- --help
# Install to profile
nix profile install github:junhoyeo/contrabass
# Enter development shell (provides Go, Bun, etc.)
nix develop github:junhoyeo/contrabass
The Nix package is a headless CLI build: it does not embed the web dashboard.
Use a source build (bun install && make build) when you need the --port
dashboard server.
For NixOS system-wide installation, add to your configuration:
{
inputs.contrabass.url = "github:junhoyeo/contrabass";
# ...
environment.systemPackages = [ inputs.contrabass.packages.${system}.default ];
}
Download from GitHub Releases
Pre-built binaries for macOS and Linux (amd64/arm64) are available on the Releases page.
Build from source
git clone https://github.com/junhoyeo/contrabass.git
cd contrabass
bun install
make build
make build first builds packages/dashboard/dist/ and then embeds it into the Go binary.
Note:
go install github.com/junhoyeo/contrabass/cmd/contrabass@latestworks for the CLI and TUI, but the embedded web dashboard (--port) will be empty becausego installdoes not run the JS build step.
Quick start
Run with the demo workflow
LINEAR_API_KEY=your-linear-token \
./contrabass --config testdata/workflow.demo.md
Run with the embedded web dashboard
LINEAR_API_KEY=your-linear-token \
./contrabass --config testdata/workflow.demo.md --port 8080
Then open http://localhost:8080.
Run headless
LINEAR_API_KEY=your-linear-token \
./contrabass --config testdata/workflow.demo.md --no-tui
CLI flags
--config string path to WORKFLOW.md file (required)
--dry-run exit after first poll cycle
--log-file string log output path (default "contrabass.log")
--log-level string log level (debug/info/warn/error) (default "info")
--no-tui headless mode — skip TUI, log events to stdout
--port int web dashboard port (0 = disabled)
Team subcommand flags
contrabass team run --config workflow.md [flags]
--worker-mode string override worker mode (goroutine|tmux, default from config)
How Contrabass works
- Poll the configured tracker for candidate issues.
- Skip issues with unresolved
BlockedBydependencies (BlockedBy gating). - Claim an eligible issue, recording the workspace HEAD SHA at claim time.
- Create or reuse a git worktree in
workspaces/<issue-id>(falls back to plain directory when git is unavailable). - Render the prompt body from
WORKFLOW.mdusing issue data. - Launch the configured agent runner.
- Stream agent events, classify agent stage (Exploration → Editing → Testing → Reviewing → Wrapping), track token consumption, and estimate completion ETAs.
- On completion, verify the workspace branch advanced beyond the claim HEAD before marking success.
- On failure, retry with deterministic exponential backoff + FNV-hash jitter.
- Recover orphaned claims on restart — issues marked Claimed but not actively running are reset to Unclaimed.
- Mirror state into the TUI, the Ziikoo dashboard (via SSE), and the JSON snapshot API.
Orchestrator features
| Feature | Description |
|---|---|
| BlockedBy gating | Issues with unresolved blockers are deferred from dispatch |
| Orphan claim recovery | Claimed-but-not-running issues are reclaimed on restart |
| Branch advance verification | Verifies agents made commits before marking success |
| Agent stage classification | Monotonic 5-stage progression based on diff velocity and token patterns |
| Completion ETA | Confidence-banded estimates (requires 3+ min elapsed, stage ≥ 3 for high confidence) |
| Liveness snapshots | Per-agent heartbeat age, activity timestamps, diff stats, iteration progress |
| Stall detection | Flags runs lacking recent events beyond stall_timeout_ms |
| Deterministic backoff | Exponential growth with FNV-hash jitter (reproducible across restarts) |
| Graceful shutdown | Drains running agents before process exit |
Runtime notes
WORKFLOW.mdis watched withfsnotify; on parse errors, Contrabass keeps the last known good config.- Configuration fields are strictly validated; inspect resolved values and reload lifecycles with
contrabass config effective --config WORKFLOW.md. - The Codex runner speaks newline-delimited JSON (
JSONL) tocodex app-serverrather thanContent-Lengthframed messages. Seedocs/codex-protocol.md. - The Codex runner handles
-32001server overload errors with exponential backoff retry (up to 5 attempts) and detects stalled streams via configurable read timeouts. - The workflow parser already accepts more Symphony-shaped fields than the runtime fully consumes today. For example,
workspace,hooks, and somecodexsettings are parsed, but the current runtime mainly uses tracker selection, timeouts, retry settings, binary paths, and prompt/template fields.
Team worker modes
Teams support two worker modes, configured via team.worker_mode in the workflow file or the --worker-mode CLI flag:
| Mode | Description | Default |
|---|---|---|
tmux | Each worker runs in a separate tmux pane with process isolation, cross-process IPC via JSONL events, and file-based heartbeats | Yes |
goroutine | Workers run as goroutines within the contrabass process — lighter weight, no tmux dependency |
tmux mode (default) provides:
- Process isolation — each agent CLI runs in its own tmux pane
- JSONL event log for cross-process event streaming
- File-based heartbeat monitoring with stale detection
- Dispatch queue with ack tracking and timeout redelivery
- Governance policies with role routing heuristics
- Crash recovery with state diagnosis and automatic cleanup
- Advisory file locking via
flock(2)for safe concurrent access
goroutine mode runs all workers in-process using Go's errgroup and sync.Mutex. It requires no external dependencies but shares the process address space.
Team state is persisted as JSON files under .contrabass/state/team/{teamName}/.
Workflow file format
Contrabass reads a Markdown workflow file with YAML front matter followed by the prompt template body.
---
max_concurrency: 3
poll_interval_ms: 2000
max_retry_backoff_ms: 240000
model: openai/gpt-5-codex
project_url: https://linear.app/acme/project/example
agent_timeout_ms: 900000
stall_timeout_ms: 60000
tracker:
type: linear
linear:
issue_details:
enabled: true
sync_comments:
enabled: false
mode: reply_thread
agent:
type: codex
startup_timeout_ms: 30000
codex:
binary_path: codex app-server
orchestrator:
event_buffer_size: 256
run_signal_buffer_size: 256
issue_cache_size: 1000
run_shutdown_timeout_ms: 5000
stop_grace_timeout_ms: 5000
git_command_timeout_ms: 2000
shutdown:
drain_timeout_ms: 30000
cleanup_timeout_ms: 10000
poll_interval_ms: 10
backoff:
continuation_ms: 1000
failure_base_ms: 10000
multiplier: 2
jitter_percent: 10
snapshot:
diff_timeout_ms: 1000
---
# Workflow Prompt
Issue title: {{ issue.title }}
Issue description: {{ issue.description }}
Issue URL: {{ issue.url }}
Produce code and tests that satisfy the issue requirements.
The complete orchestrator policy, including snapshot stage and ETA heuristics,
is documented in docs/orchestrator-configuration.md.
The complete parsing, validation, environment-reference, and effective-config
behavior is documented in docs/configuration.md.
Linear detail and timeline sync settings
When tracker.type: linear is used, the dashboard can load richer issue
metadata through the Contrabass backend without exposing Linear credentials to
browser code.
linear:
issue_details:
enabled: true
sync_comments:
enabled: false
mode: reply_thread # reply_thread by default; top_level is the fallback-safe mode
linear.issue_details.enabledcontrols backend issue detail reads used by the issue detail sheet. Candidate polling remains lean.linear.sync_comments.enabledis opt-in and defaults tofalse; when enabled, durable workflow timeline nodes are projected to Linear comments.- Comment sync is best-effort and asynchronous. It records retry/sync status in local timeline state and does not block issue completion, retry queueing, or dashboard rendering.
- Disable
linear.sync_comments.enabledto preserve legacy direct completion comments and avoid any Linear comment projection.
Template bindings
The current prompt renderer exposes:
issue.titleissue.descriptionissue.url
Environment-variable interpolation
String values in YAML front matter can reference environment variables using $NAME syntax.
Examples:
tracker.token: $GITHUB_TOKENopencode.password: $OPENCODE_SERVER_PASSWORDomx.binary_path: $OMX_BINARYomc.binary_path: $OMC_BINARY
Linear issue details and workflow timeline
For Linear trackers, Contrabass can load richer issue metadata for the dashboard and maintain a local workflow timeline that is projected back to Linear comments only when explicitly enabled.
tracker:
type: linear
linear:
issue_details:
enabled: true
sync_comments:
enabled: false
mode: reply_thread # or top_level
linear.issue_details.enableddefaults to enabled for Linear trackers and is ignored for non-Linear trackers.linear.sync_comments.enableddefaults tofalse; comment sync is best-effort and opt-in.linear.sync_comments.modedefaults toreply_thread; usetop_levelwhen threaded replies are unsupported or undesired.- Workflow timeline files are local Contrabass state and remain the source of truth even when Linear sync is disabled or temporarily fails.
OMC / OMX workflow sections
For team-runtime-backed runners, set agent.type to omx or omc and configure the corresponding section.
agent:
type: omx
omx:
binary_path: omx
team_spec: 2:executor
poll_interval_ms: 1500
startup_timeout_ms: 22000
ralph: true
agent:
type: omc
omc:
binary_path: omc
team_spec: 2:claude
poll_interval_ms: 1200
startup_timeout_ms: 21000
Notes:
binary_pathcan point to the installed CLI wrapper, for exampleomxoromc.team_specis passed directly to the team runtime, such as1:executor,2:executor, or2:claude.- Contrabass writes the rendered task prompt into
.contrabass/runner/<runner>/...inside the workspace and instructs the team runtime to execute from that file. - OMC/OMX team runners generally require the underlying toolchain prerequisites those CLIs expect, especially tmux-based team support.
Team configuration
The team section configures multi-agent coordination:
team:
max_workers: 5
max_fix_loops: 3
claim_lease_seconds: 300
state_dir: .contrabass/state/team
execution_mode: team # team | single | auto
worker_mode: tmux # tmux (default) | goroutine
worker_mode: Controls how agent workers are spawned.tmux(default) uses separate tmux panes with process isolation.goroutineruns workers in-process.execution_mode: Controls coordination strategy.teamuses the full phased pipeline,singleruns one agent at a time,autoselects based on task count.
Example workflow files
testdata/workflow.demo.md— demo Linear + Codex workflowtestdata/workflow.github.md— GitHub + OpenCode workflowtestdata/workflow.ohmyopencode.md— oh-my-opencode workflowtestdata/workflow.omx.md— OMX workflowtestdata/workflow.omc.md— OMC workflowtestdata/workflow.md— realistic Linear fixture
Supported integrations
| Surface | Current support |
|---|---|
| Trackers | Linear, GitHub Issues, Internal Board |
| Agent runners | Codex app-server, OpenCode, oh-my-opencode, OMX, OMC |
| Operator surfaces | Charm TUI, Ziikoo web dashboard, headless mode |
| Live config reload | Yes (WORKFLOW.md via fsnotify) |
| State streaming | JSON snapshot API + SSE (orchestrator, team, board, agent log events) |
Trackers
- Linear
- GraphQL-based issue fetch, claim, release, state update, and comment posting
- Can auto-resolve the assignee from the API token when
tracker.assignee_idis omitted
- GitHub Issues
- REST-based issue fetch, assign/unassign, comment, and close-on-release behavior
- Pull requests are skipped when fetching issues
- Internal Board
- File-based local issue tracking under
.contrabass/board/— no external service required - Supports team-scoped boards for multi-agent coordination
- See
docs/local-board.mdfor format details
- File-based local issue tracking under
Agent runners
- Codex
- Launches
codex app-serverwith JSONL protocol (newline-delimited JSON, not Content-Length framed) - Performs
initialize→initialized→thread/start→turn/start - Streams notifications and token usage updates in real time
- Handles
-32001server overload with exponential backoff retry (up to 5 attempts) - Detects stalled streams via configurable read timeout (
WithStreamReadTimeout) - Closes stdin on terminal events (
turn/completed,turn/failed,turn/cancelled) for clean exit - Supports Codex 0.128+
thread/tokenUsageshape - Forwards workflow-level
codexconfig as-c key=valueoverrides (model, approval policy, sandbox)
- Launches
- OpenCode
- Starts or reuses an
opencode serveprocess - Creates sessions over HTTP and streams events over SSE
- Starts or reuses an
- oh-my-opencode
- Wraps the
oh-my-opencodeagent binary - HTTP session creation with SSE event streaming
- Wraps the
- OMX (oh-my-codex)
- Launches
omx team ...with a workspace-scoped task file - Polls
omx team api get-summaryandomx team api list-tasksfor status and results - Tracks per-session token usage (input/output/total) and rate limit proximity (5-hour, weekly)
- Monitors worker liveness via file-based heartbeats with stale detection
- Shuts down the team with
omx team shutdown ... --force(and--ralphwhen configured) - Supports OMX v0.16+ native worker supervisor protocol
- Launches
- OMC (oh-my-claudecode)
- Launches
omc team ...with a workspace-scoped task file - Polls
omc team api get-summaryandomc team api list-tasksfor status and results - Same token/heartbeat monitoring as OMX
- Shuts down the team with
omc team shutdown ... --force
- Launches
Web dashboard (Ziikoo) and HTTP API
When --port is set, Contrabass serves Ziikoo — a React dashboard embedded in the Go binary — alongside a JSON/SSE API for programmatic access.
Dashboard features
Ziikoo uses a three-pane IDE-style layout:
- Left sidebar — queue navigation (running, backoff, todo, backlog, recently done, canceled) with live counts
- Main content — responsive data tables with aggregate metric cards
- Right detail sheet — slide-out panel with issue metadata, workflow timeline, and agent controls
Key capabilities:
- 5-step agent stage pill showing progression: Exploration → Editing → Testing → Reviewing → Wrapping
- Completion ETA with confidence bands (low/medium/high)
- Activity indicators with freshness coloring (fresh/warm/stale based on heartbeat age)
- Live metrics — running load, queued count, archived count, token consumption (in/out)
- Issue detail sheets — Linear metadata (assignee, creator, team, project, cycle, estimate, due date, relations), workflow timeline with sync status badges, debug info (PID, session ID, workspace path)
- Blocked queue panel — issues deferred by BlockedBy with blocker identifiers
- Retry queue — backoff entries with live countdown timers
- Team table — team phase, worker counts, task counts, fix loop progress
- Worker table — per-worker status (busy/idle/stopped), current task, PID
- Agent logs — streaming stdout/stderr with worker filter dropdown
- Board view — CRUD interface for the internal board tracker (create/edit issues, change state)
- Stop agent button — terminate running agents directly from the detail sheet
- zh-CN localization — full Simplified Chinese interface
HTTP API
| Method | Path | Description |
|---|---|---|
GET | /api/v1/state | Full orchestrator snapshot (stats, running entries, backoff queue, issues, build info) |
GET | /api/v1/issues/{issue_id}/details | Issue with Linear metadata when available |
GET | /api/v1/issues/{issue_id}/timeline | Workflow timeline snapshot |
GET | /api/v1/{identifier} | Single issue lookup from snapshot |
GET | /api/v1/board/issues | List all internal board issues |
GET | /api/v1/board/issues/{identifier} | Get single board issue |
POST | /api/v1/board/issues | Create board issue |
PATCH | /api/v1/board/issues/{identifier} | Update board issue (title, description, state, assignee) |
POST | /api/v1/running/{issue_id}/stop | Terminate running agent and release issue |
POST | /api/v1/refresh | Trigger refresh (202 Accepted) |
GET | /api/v1/mcp/config | Copyable MCP Agent config metadata |
POST | /api/v1/mcp/token | Generate an in-memory 24h MCP bearer token and config JSON |
POST | /api/v1/mcp/stream | Token-protected Streamable HTTP JSON-RPC endpoint for external MCP Agents |
GET | /api/v1/mcp/stream | Token-protected server-to-client message stream for external MCP Agents |
POST | /api/v1/stream | Streamable HTTP JSON-RPC endpoint (dashboard.subscribe, dashboard.snapshot, dashboard.ping) |
GET | /api/v1/stream | Optional Streamable HTTP server-to-client message stream |
GET | /api/v1/events | Legacy SSE event stream kept for compatibility |
The Web dashboard includes an MCP 配置 page in the sidebar. Use it to generate a short-lived token and copy a ready-to-send Agent config:
{
"mcpServers": {
"contrabass": {
"type": "streamable_http",
"url": "http://localhost:8080/api/v1/mcp/stream",
"headers": {
"Authorization": "Bearer mcp_..."
}
}
}
}
Generated MCP tokens are held in the running Contrabass Web process and expire
after 24 hours. Restarting the process clears them. The dashboard's existing
/api/v1/stream endpoint remains tokenless for backward-compatible browser use;
external Agents should use /api/v1/mcp/stream.
Streamable HTTP event stream
The dashboard connects to /api/v1/stream with a JSON-RPC POST request:
{"jsonrpc":"2.0","id":"dashboard-subscribe","method":"dashboard.subscribe"}
Use Accept: application/json, text/event-stream and Content-Type: application/json.
The response is text/event-stream with event: message frames containing JSON-RPC messages:
dashboard.snapshot— full orchestrator snapshotdashboard.event— incrementalWebEvent
The legacy /api/v1/events SSE endpoint remains available for compatibility. It sends a full snapshot event first, followed by incremental events:
| Kind | Events |
|---|---|
orchestrator | StatusUpdate, AgentStarted, AgentFinished, BackoffEnqueued, IssueReleased |
team | tool_call, team/stalled, team/all_idle, team/missing, team/event |
board | board_issue_created, board_issue_updated, board_issue_moved |
agent_log | Streaming worker stdout/stderr |
queue | Dispatch blocked by unresolved dependencies |
Heartbeat events are filtered server-side and never reach clients. Keep-alive comments are sent every 15 seconds.
Development
Build and test
make build # build dashboard, then build ./contrabass
make build-dashboard # build packages/dashboard/dist only
make build-landing # build packages/landing/dist only
make test # go test ./... -count=1
make test-dashboard # bun test in packages/dashboard
make test-landing # astro check in packages/landing
make test-quick # recommended local validation path
make test-all # Go + dashboard tests + landing checks
make ci # lint + test-quick + binary/dashboard build + landing build
make lint # go vet ./...
make clean # remove built artifacts
make release-dry # dry-run GoReleaser locally (skips publish)
For day-to-day local validation, use make test-quick.
For a fuller pre-push or CI-style pass, use make ci.
Dashboard development
make dev-dashboard
make dev-landing
The repository is a root Bun workspace with packages/dashboard and packages/landing.
The Astro landing site renders README.md, so this file is both repo documentation and site content.
Running from source
go run ./cmd/contrabass --config testdata/workflow.demo.md --port 8080
Docs and fixtures
docs/codex-protocol.md— notes on the Codex app-server framing and lifecycle used heredocs/local-board.md— internal board tracker file format and schemadocs/test-plan.md— ported test-plan notes from the Elixir codebasetestdata/snapshots/— golden snapshots for the TUI renderer
Charm stack
Direct dependencies from the Charm v2 ecosystem:
| Logo | Library | Import Path | Purpose |
|---|---|---|---|
![]() | Bubble Tea | charm.land/bubbletea/v2 | TUI framework (Elm architecture) |
![]() | Lip Gloss | charm.land/lipgloss/v2 | Styling & layout |
![]() | Bubbles | charm.land/bubbles/v2 | Reusable TUI components |
![]() | Log | github.com/charmbracelet/log | Structured logging |
![]() | x | github.com/charmbracelet/x | x/mosaic for terminal image rendering |
Plus:
github.com/charmbracelet/logfor structured logginggithub.com/fsnotify/fsnotifyfor config watchinggithub.com/osteele/liquidfor prompt templatinggithub.com/stretchr/testifyfor Go test assertions
Releasing
CI and release workflows run automatically via GitHub Actions:
- CI (
.github/workflows/ci.yml) — runs on every push and PR: lint, test, build - Release (
.github/workflows/release.yml) — triggered by pushing a version tag
To ship a new release:
git tag v0.4.1
git push origin v0.4.1
This builds cross-platform binaries (macOS/Linux, amd64/arm64) via GoReleaser, publishes a GitHub Release with grouped changelogs, and updates the Homebrew tap.
After GoReleaser publishes the release, scripts/generate-release-notes.ts
appends contributor attribution — each change is tagged with the author's @username and linked PR,
and first-time contributors get a dedicated shout-out section.
Notes for contributors
For detailed contribution guidelines, see CONTRIBUTING.md.
- The dashboard assets must exist before the Go binary is built because the binary embeds
packages/dashboard/dist. packages/landingrendersREADME.md, so README changes also affect the landing site.- If workspace package resolution looks broken in
packages/dashboardorpackages/landing, rerunbun installat the repository root to refresh workspace links. - TUI snapshots live in
testdata/snapshots/and are exercised byinternal/tuitests.
Collected info
- ★ 222 stars
- ⎇ 25 forks
- Language: Go
- Source updated: 9/17/2026
Config for your environment
Replace {MCP_ENDPOINT_URL} with this MCP’s endpoint URL (from its repo or docs above). No API key — you connect directly.
Tool
OS
Config file: ~/.cursor/mcp.json
{
"mcpServers": {
"mcp-server": {
"url": "{MCP_ENDPOINT_URL}"
}
}
}Paste into mcpServers in the config file. Restart Cursor after saving.
If this MCP is also published on mcpchannel.ai, you can subscribe from Browse and use the gateway config there instead.




