Arbiter is an AI-driven issue tracker and autonomous coding agent harness. It coordinates work across your projects through a CLI (arb), MCP tools for a coordinator agent, and a LiveView dashboard. Tasks are tracked as issues, dispatched to worker agents for autonomous execution in isolated git worktrees, and merged through a ReviewGate and merge queue for code quality and safety.
-
Elixir 1.19+ / Erlang 28+ — mise is the recommended way to install. With mise installed:
mise install(The
.tool-versionsfile in this repo pins the exact versions.)
Arbiter's datastore is SQLite — no database server or Docker is required.
# 1. Clone
git clone <repo-url> arbiter
cd arbiter
# 2. Install dependencies, run migrations, seed the default workspace
mix setup
# 3. Build and install the arb CLI onto your PATH
arb install cliarb install cli builds the CLI escript from apps/arbiter_cli and installs
it to ~/.local/bin/arb. If ~/.local/bin isn't in your PATH yet, add this
to your shell profile (.bashrc, .zshrc, etc.) and restart your terminal:
export PATH="$HOME/.local/bin:$PATH"Since arb install cli itself depends on arb already being on your PATH the
very first time, bootstrap it once by hand:
cd apps/arbiter_cli && mix escript.build && cd ../..
mkdir -p ~/.local/bin && cp apps/arbiter_cli/arb ~/.local/bin/Re-run arb install cli any time you pull changes to apps/arbiter_cli.
Workspaces — isolated coordination scopes. A workspace holds a set of issues, dispatch policies, and tooling configuration. An installation can run multiple workspaces side by side, each with its own repos, tracker, and merge settings.
Repos — registered git repositories. Workers check out code on repos to work on issues.
Issues — tasks to be worked. Can be tracked in an external system (Jira, GitHub, Linear) or managed locally. Status flows from creation through ready → in_progress → done.
Workers — autonomous agents spawned via Claude Code (or future adapters) to work an issue. Each worker receives an issue, works it in an isolated git worktree, and reports completion with a PR or notes. Their full transcript is retained for audit and learning.
ReviewGate — optional quality checkpoint. A second Claude agent reviews the worker's work before merging. Can be disabled per-issue.
Merge queue — batches approved changes and applies them to the repo in sequence. Handles merge conflicts, CI status checks, and rollback on failure.
PRPatrol — polls open PRs per repo and dispatches follow-up workers when a review needs a response (changes requested, unresolved review threads, or failing required checks).
Watchdog — polls a merge request's fate for a worker parked at :awaiting_review and drives the worker to its terminal state (merged, closed/rejected, or auto-merged).
Escalation mailbox — the coordinator's inbox for anything that needs a human or coordinator ruling (e.g. a ReviewGate escalation, a worker failure). Read it via arb message inbox or the inbox_check / notify_list MCP tools.
/events — a server-push event stream (GET /events) for the coordinator: newline-delimited JSON, one event per line, for topics like inbox, review_gate, worker_failed, worker_done, and (opt-in) task_state / external_review. Every event carries a "cursor"; pass since=<cursor|timestamp> to replay what was missed since a disconnect before rejoining the live stream (see ArbiterWeb.Api.EventController moduledoc).
The primary integration path for a coordinator agent (e.g. a dedicated Claude Code session) is the arbiter MCP server, which exposes tools like task_show, task_create, task_list, worker_dispatch, worker_resume, worker_review, worker_list, worker_log, inbox_check, message_send, notify_list, workspace_show, workspace_config_get/set, quota_get, run_log_list, transcript_capture_stats, and usage_summarize, plus whole tool categories beyond one-off issue dispatch:
- Skills —
skill_list/skill_get/skill_create/skill_update/skill_deletefor managing reusable skill content. - Dependencies + scheduler —
dep_add/dep_remove/dep_listto wire issues together withdepends_on/blocks/conflicts_withedges, andscheduler_pause/scheduler_resume/scheduler_statusto control the board scheduler (Autopilot) that auto-dispatches Ready cards in edge order. Chains of issues run by declaring the edges, not by building a separate graph object. - ExternalReview —
external_review_list,external_review_show,external_review_transcript,review_greenlightfor inspecting and unblocking worktree-backed external code review.external_review_transcriptisworker_log's counterpart for a review: the prompt it was given, the raw transcript its reviewer emitted, and every tool call paired with its result — keyed on the review record id, since an external review is not task-linked.
See apps/arbiter/lib/arbiter/mcp/catalog.ex for the full, current catalog and which tier (worker vs. coordinator) can call each tool.
To mint a token for a coordinator to use:
arb mcp token mint --tier coordinatorThe server speaks MCP over Streamable HTTP at http://127.0.0.1:4848/mcp,
so configure a client with "type": "http" (Claude Code), httpUrl (Gemini
CLI) or a plain url (Codex CLI), and pass the token as
Authorization: Bearer <token>:
{
"mcpServers": {
"arbiter": {
"type": "http",
"url": "http://127.0.0.1:4848/mcp",
"headers": { "Authorization": "Bearer <coordinator-token>" }
}
}
}Streamable HTTP is the only transport /mcp serves. The deprecated HTTP+SSE
transport (2024-11-05, Claude Code's "type": "sse") is not served: a client
configured that way POSTs initialize and then waits for the reply on the SSE
stream, which never arrives, so it times out and reports the server as down. Use
"type": "http" — which is what arb init and worker dispatch write. The
GET /mcp SSE stream still exists, but only as Streamable HTTP's server →
client channel (server-initiated messages and keepalives); it requires a
coordinator token.
arb server startThe dashboard is at http://127.0.0.1:4848.
Visit the dashboard's Workspace page and configure:
- Repos (projects to work in)
- Worker/agent settings — model tier map, per-thinking-level args, provider overrides, and credentials (
agent.config.*) - Security policy (
agent.security.*) - Rate-limit throttling (
quota.*) and per-workspace worker concurrency (conductor.max_concurrent— the key name is historical) - Optionally a tracker (Jira, GitHub, Linear) and merge strategy
Or edit config/dev.exs directly and restart the server, or use arb config set.
Create and dispatch an issue via the dashboard or CLI:
arb issue create "Fix typo in README"
arb issue dispatch <id> my-projectWatch the worker in the dashboard, or tail the transcript:
arb worker log <task-id>Once complete, the merge queue picks it up (if configured) or you can merge manually via the dashboard.
To run Arbiter as a self-contained OTP release under a systemd user unit, install and enable it with:
arb install serviceThis writes ~/.config/systemd/user/arbiter.service (ExecStart=~/.arbiter/current/bin/arbiter start), enables it via loginctl enable-linger for machine-boot startup, and starts the release. Manage it with systemctl --user status arbiter.service and view logs with journalctl --user -u arbiter.service -f. Pass --system to install a system-wide unit instead (needs root). Secrets and PATH configuration live in ~/.arbiter/arbiter.env. Uninstall with arb install service --uninstall.
arb install service only writes the release-shaped unit above. There is no
CLI command yet for a dev-mode unit (one whose ExecStart runs
mix phx.server instead of a release binary) — on a source checkout you hand-write
that unit file yourself; see Deploying: pick the path for your install
shape below.
Arbiter supports two install shapes, and the deploy command differs between them:
- Source checkout (dev mode) — a git clone with
mix phx.serverrun directly (by hand, or supervised by a systemd user unit that pointsExecStartatmix phx.serverrather than a release binary). This is how the "Install (development)" section above sets things up, and how this repo's own coordinator instance runs today — the releases strategy is scoped but not yet built (see #754 for the history:arb server deployused to hard-requireARB_RELEASE_REPOand dead-end on a dev-mode install; it now detects the missing env var and falls back automatically). - OTP release install — a built release artifact unpacked under
~/.arbiter/releases/<tag>/, installed viaarb install service. No source checkout or Elixir/Mix toolchain on the box.
If you're on a source checkout, use the sequence below. If you have a release artifact installed, skip to OTP-release installs.
git fetch origin main
git diff --name-only HEAD origin/main -- mix.lock # non-empty → deps changed; see note below
arb server deploy --git-pull
arb server doctor # confirm CLI and server report the same versionarb server deploy --git-pull does the pull → rebuild → restart → migrate
sequence for you: git pull --ff-only on main, rebuild and install the CLI
escript if apps/arbiter_cli changed, then restart Phoenix — via
systemctl --user restart arbiter when the systemd unit is present, a plain
process bounce otherwise. The restart's boot sequence (Boot.Migrator)
applies any pending migrations before the endpoint opens, so migrations are
never run against the live server. Do not git pull by hand first — the
command diffs before_sha/after_sha itself and treats an already-current
tree as "nothing to deploy," skipping the restart and escript rebuild
entirely.
It also does not run mix deps.get for you. If the pre-check above
printed a mix.lock line, the pulled tree needs new deps that the restart
will not have — running mix deps.get before the pull is a no-op, since
git fetch doesn't move HEAD and the old lockfile is still checked out.
Instead, either run mix deps.get && arb server restart immediately after
the deploy, or skip --git-pull and do the whole thing manually: git pull --ff-only origin main → mix deps.get → (cd apps/arbiter_cli && mix escript.build && cp arb ~/.local/bin/arb) if apps/arbiter_cli changed →
arb server restart → arb server doctor. The manual form is the safer
choice when deps changed, since it never restarts against unfetched deps.
A bare arb server deploy (no flags) does the same git-pull fallback
automatically whenever ARB_RELEASE_REPO is unset, but prefer the explicit
--git-pull form on a source checkout so the command's behavior doesn't
depend silently on an environment variable you may not have set on purpose.
This subsection only applies once you have a built release artifact
installed (arb install service, or manually under
~/.arbiter/releases/<tag>/) — not to a source checkout; use
the sequence above for that. ~/.arbiter/current
is symlinked atomically to the active release. Deploy a specific version (or
latest) with:
arb server deploy --version v1.2.3This downloads the release tarball + checksum from GitHub Releases (ARB_RELEASE_REPO), verifies the SHA-256, unpacks it, atomically swaps current, restarts the service, and health-checks it — auto-rolling back to the last-known-good release if it doesn't come back green.
The deploy does not run migrations. SQLite allows exactly one writer, so a
bin/arbiter eval Arbiter.Release.migrate from the new release while the old
server is still serving would be a second writer racing the live one. Instead
the new release migrates during its own boot: Arbiter.Boot.Migrator is a
synchronous supervision-tree child that brings the schema to head before
ArbiterWeb.Endpoint binds its port, gated on the single-instance advisory
lock so only one node ever migrates. The real ordering is therefore:
stop the old server → new release boots → migrate → serve
— one writer at every instant, and the same path arb server restart,
arb server migrate and dev mix phx.server already take. The dev-mode
fallback (arb server deploy --git-pull) obeys the same ordering: with the
server up it pulls and restarts, and the pulled migrations are applied by that
boot; it only migrates standalone when the server is already down.
Auto-rollback stops at a schema change. Re-pointing current back at the
prior release after the new one has migrated would run old code against a
schema it has never seen. Before swapping the symlink, the deploy compares the
migrations packaged into the new release tree
(lib/<app>-<vsn>/priv/repo/migrations) against those in the release it would
roll back to — a pure filesystem comparison, no database connection. If the new
release adds any:
- it says so up-front, naming them, and notes that automatic rollback is disabled;
- on a health-check failure it refuses to roll back, leaves
currenton the new release, names the crossed migrations, and exits non-zero with the operator's options (fix forward, or roll the schema back first withbin/arbiter eval "Arbiter.Release.rollback(Arbiter.Repo, <version>)"and thenarb server deploy --version <prior> --force); --allow-cross-migration-rollbackoverrides the refusal, rolling back anyway with a loud warning that the prior release is now on a newer schema.
A deploy that adds no migrations keeps the automatic rollback unchanged — but
only when detection actually worked. Because an arbiter release always ships
migrations, an empty migration set from the new release tree means the globs
no longer match the packaging layout, not that the deploy is migration-free.
That case fails closed: the deploy warns up-front, refuses the automatic
rollback the same way a crossed migration does, and reports
migrations_detected: false in --json so the empty crossed_migrations list
can't be mistaken for "safe". --allow-cross-migration-rollback overrides it.
When the refusal comes from a failed swap (/api/version still reports the
old release) rather than a green-wait timeout, the message says the new
release's migrations may have been applied rather than claiming they were —
a release that never booted never ran its boot migrator.
The dashboard's auth model is "a loopback peer is trusted; there is no login" —
LiveView pages, including a terminal into every worker session, have no
credential check beyond "did this request come from the same box". Because of
that, the server binds to 127.0.0.1:4848 by default in both dev and
prod/release, in dev and runtime config alike.
Upgrade note: before this change, the default was 0.0.0.0
(dev)/[::] (prod) — reachable from the LAN, or the internet on a cloud host
with a permissive security group. An install that relied on that for remote
access will stop being reachable after upgrading. Use an SSH port-forward
instead (ssh -L 4848:127.0.0.1:4848 <host>), which needs no server-side
change; or, if off-loopback really is required, set ARB_BIND_ADDRESS
explicitly (below) — arb server doctor will keep warning about it every time
as a reminder of the exposure.
To bind elsewhere, set ARB_BIND_ADDRESS to any address :inet.parse_address/1
accepts (e.g. 0.0.0.0, ::, a specific interface IP):
echo "ARB_BIND_ADDRESS=0.0.0.0" >> ~/.arbiter/arbiter.envAny non-loopback value logs a boot WARNING naming the exposure, and
arb server doctor reports a non-fatal warning check pointing back here.
By default, arb talks to a local server on http://127.0.0.1:4848 (loopback). To point arb at a remote Arbiter server:
-
Set
ARB_HOSTto your server's URL over VPN:export ARB_HOST="http://arbiter.internal.example.com" # or export ARB_HOST="https://arbiter.example.com" for HTTPS
-
Mint a token on the server:
arb mcp token mint --tier coordinator
-
Export the token on the client:
export ARB_TOKEN="<token-from-step-2>"
-
Verify the connection:
arb prime
- Local loopback (
ARB_HOSTunset orhttp://127.0.0.1:4848) requires noARB_TOKEN— the server exempts localhost. - Remote access requires both
ARB_HOSTandARB_TOKEN, and the server must be reachable at that address in the first place — since the server now binds loopback-only by default (above), that means eitherARB_BIND_ADDRESSset server-side, or reaching it through an SSH port-forward / VPN tunnel that terminates on127.0.0.1locally. The API's own token check is unaffected either way; only the dashboard's login-free LiveView pages are the reason to prefer a tunnel over a rawARB_BIND_ADDRESSoverride.
Arbiter encrypts workspace secrets (tracker / merger credentials) at rest using
AES-256-GCM via ash_cloak. The server
refuses to start without an encryption key. Generate a 32-byte Base64 key and
add it to your environment before deploying:
echo "ARBITER_CLOAK_KEY=$(openssl rand -base64 32)" >> ~/.arbiter/arbiter.env
# (for a non-service run, put it in the project-root .arbiter.env or export it)arb install service forwards ARBITER_CLOAK_KEY from the installing shell
into ~/.arbiter/arbiter.env automatically if it is set. Treat this key like a
database password: back it up and keep it stable — rotating it (re-encrypting
existing secrets) is a separate runbook and is not yet automated. Losing it
makes existing encrypted secrets unrecoverable.
Real worker spawns and the CredentialWatchdog's health probe both authenticate
the claude CLI via Arbiter.Agents.Claude.spawn_env/1. By default that falls
back to whatever OAuth session is active for the account running the Arbiter
server (~/.claude/.credentials.json) — which means an operator's personal
session expiring blocks every workspace's dispatch until it's manually
re-authenticated.
To avoid that, set a long-TTL Claude Code OAuth token once, install-wide, in
.arbiter.env (same file/trust model as ARBITER_CLOAK_KEY / SECRET_KEY_BASE
above — loaded via EnvironmentFile= when running as a service):
echo "CLAUDE_CODE_OAUTH_TOKEN=<your-long-ttl-token>" >> ~/.arbiter/arbiter.env
# (for a non-service run, put it in the project-root .arbiter.env or export it)spawn_env/1 exports this under its own literal name (never remapped to
ANTHROPIC_API_KEY — the two are not interchangeable to the CLI) whenever it's
present in the OS environment, so the probe and real dispatch always agree.
Prefer this single install-wide var over configuring the same token as a
per-workspace credentials_ref/worker_env secret in each workspace — a
per-workspace copy only reaches real worker spawns, not the Watchdog's probe,
and duplicating an identical token across workspaces risks copy-drift if one
copy is rotated and the others aren't.
arb install service also forwards CLAUDE_CODE_OAUTH_TOKEN from the
installing shell into ~/.arbiter/arbiter.env automatically, same as the
other captured secrets above.
Precedence when both are set: a spawn can end up with both
CLAUDE_CODE_OAUTH_TOKEN (install-wide) and ANTHROPIC_API_KEY (workspace
credentials_ref/api_keys rotation) in its environment at once. Which one
the claude CLI honours is decided by the CLI itself, not by Arbiter — if it
prefers the OAuth token, a workspace that deliberately configured its own key
would silently authenticate against the install-wide account instead. If a
workspace's ANTHROPIC_API_KEY must win, verify the CLI's actual precedence
before relying on it, or unset the install-wide token for that install.
Redaction: Arbiter.Worker.ClaudeSession.start/1 adds
CLAUDE_CODE_OAUTH_TOKEN/ANTHROPIC_API_KEY values to the session's
redaction list alongside the workspace's secret-flagged worker_env values —
both the values carried in the spawn's explicit :env (the Dispatch/
ReviewGate path) and, for the start/1 callers that pass no :env at all
(the conflict-resolver and CI fix-pass workers, which still inherit the
install-wide OS environment via Port.open's {:env, …} extension
semantics), the value read directly from the OS environment. So the token is
scrubbed from worker output (worker_runs.output_lines, the live dashboard
stream, the durable output log) across all ClaudeSession.start/1 callers,
even after the per-workspace worker_env copies are removed.
Instead of pointing credentials_ref at an environment variable
(credentials_ref: "env:SHORTCUT_API_TOKEN"), you can store the token directly
on the workspace, encrypted at rest, and reference it with a secret: ref. This
needs no server-side env var and no restart:
# 1. store the encrypted secret on the workspace
arb workspace secret set tracker_token sct_rw_...
# 2. point the tracker config at it
arb config set tracker.config.credentials_ref secret:tracker_token
# inspect / remove (values are never shown — only key names)
arb workspace secret ls
arb workspace secret rm tracker_tokenExisting env: refs keep working unchanged, so you can migrate one workspace at
a time. Secret values are never returned by the API or CLI — only the key
names are listed.
A coordinator is a dedicated Claude Code session that directs work across a workspace — creating and dispatching issues, reviewing worker output, and resolving escalations — typically via the MCP tools above. It has its own working directory with persistent memory and a notes folder.
mkdir ~/coordinator && cd ~/coordinator
arb init # initializes the current directoryOr pass a path to initialize elsewhere:
arb init ~/my-coordinator
cd ~/my-coordinatorThen open Claude Code:
claudeThe coordinator session will check whether the server is running, start it if
needed, orient itself with arb prime, and be ready to coordinate your work.
A repo is a local git repository that workers check out and work in. Register your projects via the dashboard (Workspace → Repos), or directly in config/dev.exs:
config :arbiter, :repo_paths, %{
"my-project" => Path.expand("~/dev/my-project"),
"another-project" => Path.expand("~/dev/another-project")
}After editing config/dev.exs, restart the server for changes to take effect.
The CLI uses an arb <resource> <verb> grammar: arb [resource] verb [args].
Run arb help (or arb --help) for the exhaustive, always-current usage text
(apps/arbiter_cli/lib/arbiter_cli/main.ex); the table below covers the
commands you'll reach for most.
| Command | Purpose |
|---|---|
arb prime |
Mission briefing — run at session start to check workspace status |
arb issue list |
List issues in the workspace |
arb issue show <id> |
View an issue's details and history |
arb issue create <title> |
Create a new issue |
arb issue dispatch <id> [repo] |
Dispatch a worker to work on an issue |
arb worker list |
List running and completed workers |
arb worker log <task-id> |
Read a worker's full transcript (durable) |
arb worker review <task-id> |
Dispatch a review-only worker against a task |
arb message inbox |
Read (and mark read) the coordinator's escalation mailbox |
arb server start |
Boot the stack (no-op if already up) |
arb server deploy [--version vX.Y.Z] |
Deploy an OTP release from GitHub Releases (auto-rollback on failure, refused across a migration) |
arb server deploy --git-pull |
Source-checkout deploy: git pull --ff-only, rebuild the CLI if changed, restart (see Deploying) |
arb server doctor |
Health-check the server and database |
arb config get/set [workspace] |
Read/edit workspace configuration (tracker, merger, etc.) |
arb mcp token mint --tier coordinator |
Mint an MCP token for a coordinator session |
All commands accept --help and --json for structured output. Pre-<resource> <verb>
flat commands from earlier CLI versions (arb list, arb start, arb doctor, …) still
run — they print a one-line note pointing at the new form. (arb dispatch <id> is a
permanent top-level shortcut for arb issue dispatch <id>, not a legacy alias.)
Architecture and design decision records live in docs/:
- Licensing Model & Open-Core Architecture — Core vs. Pro distribution, license choices, CLA requirements, and extension seams.
- Pluggable Agent Harness Design — Pluggable agent adapters and routing policies.
- MCP Server Design — Streamable HTTP MCP server architecture.
- Quota and Auth Posture — Provider quota management and credential lifecycle.
- Worker Security Policy — Execution sandbox and security isolation for agent workers.
- Remote Access — Connecting to dashboard and sessions over SSH tunnels.