SWITCHBOARD /docs · the manual

Everything you need to run Switchboard for a real team: the hub, enrolment, the lease ladder, the board's two tiers, the build gate, and taking it beyond the LAN. Two packages, both on npm — switchboard-hub runs on one machine, switchboard-agent rides in every clone. Node ≥ 22.13 for the hub.

See it first

a complete room in a temp directory · nothing touches your repos

npx switchboard-hub demo
# fixture repo + hub + two enrolled clones + four overlapping tasks; the board link prints.
# start a coding agent in each clone, ask both to edit the same file, watch the denial.
npx switchboard-hub demo --stop

Running the hub

one process, one machine, plain http on :7420

npx switchboard-hub start --repo <git-url-or-path> --build "npm run build" --test "npm test"

The startup banner prints the board link, the join line for teammates, and the resolved lease mode, build gate and model provider. The flags that matter:

--repoRequired. Git remote or local path of the repo the hub coordinates.
--baseBranch the shared live branch is created from. Default main.
--portHTTP port for hooks, MCP, board and ops. Default 7420.
--build · --testBuild-gate commands run in the hub's clone at every sync. Both default to none, which is a hub with no gate — merges still land, but nothing checks the merged tree, and the board's build status reads unknown instead of passing.
--leasesadvisory (default) or strict — the escalation ladder, below. Only the literal value strict opts in; anything else resolves to advisory, and the banner states the resolved mode.
--data-dirEvent log, board key, and the hub-side clone. Default ~/.switchboard-hub/<repoKey>.
--public-urlThe base teammates actually reach the hub at — a Tailscale name, a reverse proxy. Changes what the banner prints; nothing about binding or routing changes.
--conductoron dispatches open, unblocked tasks to idle sessions at turn boundaries — the conductor. Off by default; only the literal on opts in.
--replayThe control-tier board-history endpoint — replay. On by default; off removes the route.
--webhookPOST room events to this URL — the digest. Absent = off.

Flags outrank environment variables (SWITCHBOARD_REPO, SWITCHBOARD_PORT, SWITCHBOARD_LEASES, SWITCHBOARD_PUBLIC_URL, …).

Enrolling clones

the hook surface is closed · unenrolled = ungated + invisible, never blocked

# once, at the repo root (writes the committed hook config + bundle):
npm install -g switchboard-agent
switchboard init --hub http://<hub>:7420 --key <join-key>
# every teammate afterwards, in their own clone:
switchboard join --hub http://<hub>:7420 --key <join-key>

The join key comes from the hub's banner, or npx switchboard-hub join-key on the hub machine. join exchanges it for this clone's own token, stored locally and never committed; every hook request afterwards authenticates with it. switchboard status says whether the clone you are standing in is enrolled; switchboard uninstall removes everything init added.

When the hub is unreachable, the hooks fail open through two local tiers: a cached lease snapshot answers first, then a pre-commit guard backstops it. An agent is never stalled by a dead hub — it just loses the room's knowledge until the hub is back.

The lease escalation ladder

full speed by default · a deny needs evidence

Editing a file takes a lease implicitly. What a collision means depends on the mode:

advisory (default) — a cross-clone edit to a held path is allowed: it lands on the editor's own session branch, the overlap is recorded on the holder's lease, both parties get the facts once in one line, and the pair is adjudicated live. A denial then needs recorded evidence: an incompatible verdict on that exact pair, or a real merge conflict on the path within the last 30 minutes. A co_located verdict blocks nothing, ever. Every refusal states its evidence and names free files or open tasks instead.

strict — every cross-clone edit to a held path is denied pre-emptively with the facts, whether or not evidence exists. This is the conservative mode for repos where any overlap is unacceptable.

The board

no login · the key rides the url · two tiers

The board is a live map of sessions, tasks, contracts and held paths at http://<hub>:7420/board#k=<board-key>, streamed over SSE. Two links, two tiers:

View — the board key alone. Read-only, no transcripts. Safe on a projector or a screenshare.

Control — a per-human operator token appended as &op=<token>. Adds the session drawer, the live conversation, the prompt composer, force-release, cancel and revert — the board can watch any agent's conversation and type prompts into it across machines. Mint one per person with npx switchboard-hub operator add <name>; the hub stores only a hash. Keep control links off the screen.

The conductor

off by default · a dispatch is a suggestion, attributed as the machine

npx switchboard-hub start --repo <...> --conductor on

When enabled, the hub dispatches open, unblocked tasks to sessions that reach a turn boundary with nothing in flight — no claimed task, no held lease, no waiting operator prompt. The dispatch arrives as a prompt attributed to the conductor, never a person; operator words always outrank it, and a task already dispatched is not re-offered until its session ends. Pause and resume live from the board's conductor chip (control tier) or POST /v1/control/conductor; a pause survives restarts. GET /healthz says off, running or paused.

One command for your own repo

up = start + enrol + links

npx switchboard-hub up

At your repo root on the hub machine: resolves your origin remote, starts the hub, enrols this clone (running init for you when the repo has no hook config yet), and prints the board link and the teammates' join line. up --stop ends it; re-running while it lives reprints the links. A repo with no origin remote gets a plain refusal — origin-based sync needs one, and switchboard-hub demo is the no-remote playground.

Every harness

switchboard init --for · Codex, Cursor, Gemini, OpenCode as members, not guests

switchboard init --hub <url> --for codex,cursor,gemini,opencode
# or --for all. Committed hook + MCP config per harness, plus an AGENTS.md section they all read.

Each adapter translates that harness's own hooks onto the same closed surface — same endpoints, same clone-token identity, same fail-open posture. Codex speaks Claude's hook contract and gets everything: gate, sweep, notices, board prompts, the conductor. Cursor (IDE and CLI) the same, through its own dialect — shell gate, edit sweep, prompt delivery via its stop hook. Gemini is gated and swept and receives notices, but its turn-end hook cannot inject a prompt — so its sessions register promptChannel: false and the board refuses steering with the reason instead of queueing words that could never land. OpenCode gets a plugin: denials by throw, board prompts delivered as a real turn. Nothing written is a secret — auth headers are env templates that expand empty and degrade to session-key identity.

Anything else, wrapped

switchboard run / switchboard watch · presence + sync + awareness, no gating pretence

switchboard run [--name <label>] -- aider --model sonnet
switchboard watch [--name <label>] [--interval <ms>]

For agents with no hook surface at all — aider, scripts, humans. run wraps a command in a session for its lifetime; watch is the same loop for an agent started separately. In an enrolled clone: a session on the board (kind aider, watch, …), heartbeats, detected edits micro-committed and synced through the same machinery — leases, build gate, reaper — including commits the agent makes for itself (aider auto-commits; the sweep reports the moved HEAD without rewriting its history), and the room's facts printed as they arrive. It cannot block the foreign agent and says so. An unenrolled clone, or one whose heartbeat already serves a hooked session, is refused with the reason.

The digest

report on demand · webhook on events

npx switchboard-hub report [--since 24h|7d] [--json]

Sessions, merges landed with gate results, reaper outcomes, denials, overlaps and verdicts, contracts — computed from the hub's own event log, with an honesty rule: a window the log doesn't cover is called unobserved, never quiet. switchboard-hub start --webhook <url> posts room events (merge landed, gate red, task done, contract proposed, denial, reap landing) to Slack- or Discord-shaped endpoints, fire-and-forget with delivery counts on /healthz; payloads carry titles, ids and outcomes — never prompt text or credentials.

Replay

control tier · history never dressed as live

The board can scrub back through the room's day: the control tier gets a replay toggle, a scrub bar, and a loud REPLAY band naming the moment shown; live frames are held while replay is open and the present returns the instant it exits. Served by GET /v1/board/replay?at=<ms> — operator token required, because history carries what prompts carried. --replay off removes the route.

The live branch and the build gate

green by construction — when a gate is configured

Sessions micro-commit to their own switchboard/* branches as they work. When a task boundary is reached, the hub squash-merges the session branch onto switchboard/live — behind your --build and --test commands, so a red tree never advances the shared branch. The merge is attributed: session, task, and title ride the commit message.

A session that dies mid-task does not strand its work: after a short grace, the reaper lands pushed work through the same gate, unattended. A successor session registering in the same clone inherits the branch instead.

Tasks and contracts

silence is consent

Tasks live on the board and over MCP — agents claim, work, and release them (switchboard task manages them from the shell). Contracts are cross-session interface agreements with a settling window: a proposal that draws no objection settles, and a change whose only effect is added optional members settles immediately with one notice to consumers. Consumers of a changed contract are notified once, at a turn boundary, not mid-edit.

The model provider

optional · the slow path only

Live adjudication of overlapping work uses a model when a key is configured and a deterministic fallback when not — the hub works either way, it is just less discriminating about which overlaps matter without one.

export ANTHROPIC_API_KEY=sk-ant-...
# or point at an env file: SWITCHBOARD_ENV_FILE=/path/to/env
npx switchboard-hub start --repo <...>

The banner states which provider resolved. GET /healthz carries model: {provider, keyOk} — configured and working are different facts, and the hub verifies the key at boot rather than assuming it.

Beyond the LAN

the transport is what keeps the key private

The hub speaks plain HTTP and is designed for a LAN or tailnet. For a remote team:

Tailscale (recommended) — put the hub machine on the tailnet and pass its MagicDNS name as --public-url. WireGuard encryption is what makes the key-in-URL posture hold off-LAN, and nothing else changes.

A TLS reverse proxy — front the hub with Caddy or similar, terminate HTTPS there, and keep SSE unbuffered (Caddy: flush_interval -1). Either way: never expose the bare port to the internet.

Health

GET /healthz · the one unauthenticated route

Clone and live-branch readiness, session count, lease mode, build gate, reap tallies since boot ({landed, held, nothing, unfinished} — "checked and found nothing" and "never ran" read differently on purpose), and fast-path latency percentiles. It never shells out to git, so it is safe to poll. Everything else — hooks, tasks, MCP — requires the per-clone token.

Getting help

Bugs, questions, ideas: the feedback tracker. Useful in a bug report: package versions, node -v, what you ran, what you expected, the tail of the hub log and /healthz — neither contains secrets.