Architecture
Published April 2026 · Updated May 2026
OpenRig is a local control plane for multi-agent coding work. It runs as three packages: a daemon that holds the state, a CLI that drives it, and a web UI that lets you watch it. Everything runs on your own machine. Nothing is hosted, and nothing phones home.
This page describes how OpenRig is put together. It is current as of version 0.3.1. A few small details (exact command counts, for example) move from release to release; where that matters, the text says so rather than pinning a number that will quietly go stale.
The Three Packages
The daemon is the heart of the system. It is a small HTTP server with a SQLite database, and it has no web framework anywhere near its core logic. The CLI and the UI both talk to it over HTTP. So does the MCP server, which is how a coding agent can drive OpenRig itself.
The CLI, the web UI, and the MCP server
|
v
Daemon HTTP routes (Hono)
|
v
Framework-free domain services
|
+-- SQLite (the canonical store)
+-- tmux / cmux session adapters
+-- runtime adapters (Claude Code / Codex / Terminal)The local control plane. A Hono HTTP server, a SQLite database, and the domain logic that does the real work. It survives restarts because its state is on disk.
The rig command. The main way both people and coding agents operate OpenRig. Human-readable by default, with --json for agents.
A React app for watching the system: an icon rail down the left, a destination in the center, and a shared detail drawer. It is an observability surface, not a second control plane.
One rule shapes the whole codebase: the web framework (Hono) is only allowed in the route layer. The domain services and the runtime adapters import zero web-framework code. Routes depend on the domain; the domain never depends on routes. That keeps the core testable and keeps the CLI, the UI, and the MCP server all building on exactly the same logic.
tmux Is the Transport, Not the Truth
Every agent session in OpenRig runs inside a tmux session. This is a deliberate architectural choice, not an implementation detail. Every terminal coding agent already runs in a terminal, so tmux gives OpenRig a single control surface that works for Claude Code, Codex CLI, or any future agent without asking that agent to integrate, install a plugin, or even know OpenRig exists.
The important word is transport. tmux is how OpenRig reaches an agent and reads what it printed. It is not where the truth lives. The durable record of what a rig is and what happened to it lives in SQLite and on the filesystem. If a tmux session disagrees with the database, the database wins.
Each agent gets a readable address: {pod}-{member}@{rig}. The name is authored, never generated. rig whoami resolves it, falling back to tmux pane metadata when nothing else is available.
rig send types text into a pane. rig capture reads a pane back. There is no custom message bus. The terminal itself carries the message.
tmux records each session's full output to a transcript file on disk. rig transcript and rig ask read that record. Every action is auditable without the agent opting in.
rig discover scans tmux for agent sessions you started yourself. rig adopt brings them under management without restarting them.
Inside the Daemon
When the daemon starts, it opens its SQLite database and runs an ordered set of migrations to bring the schema up to date. Then it constructs its domain services in dependency order (the spec and startup machinery, the runtime adapters, the transport and history layers, the coordination services), and mounts the HTTP route tree on top of all of it.
The route surface is large and grows release to release. It covers rig lifecycle, sessions, specs, transcripts, chat, the coordination layer (the queue, streams, and workflows), Mission Control, workspace and project surfaces, and a static file server for the UI itself. The point worth remembering is not the exact count of routes. It is that every one of them is a thin layer over a domain service that contains the real logic.
SQLite is the canonical store. The schema is built by an ordered set of migrations applied at daemon startup; a fresh install applies all of them in sequence. The append-only event log, the work queue, the workflow instances, the rig chat history, and the action audit trail all live in SQLite, and all of them survive a daemon restart. The content primitives (plugins, agent images, context packs, and the missions and slices tree) are instead canonical on the filesystem, and the daemon keeps an in-memory cache of them rather than copying them into the database.
The Coordination Layer
A single agent can be told what to do in a sentence. A fleet of them needs a way to pass work around that does not lose track of it. OpenRig's coordination layer is that mechanism. It is daemon-backed and SQLite-canonical, so a handoff survives a crash, a reboot, or an agent compacting its context window.
A unit of work is a queue item. It moves through a small set of states: pending, in-progress, done, blocked, failed, denied, canceled, and handed-off. The load-bearing rule is what OpenRig calls hot-potato closure: you cannot mark a queue item done without saying why it is done. The reason has to be one of a fixed set: handed off to someone else, blocked on another item, denied, canceled, finished with no follow-on, or escalated. If the reason is a handoff, a block, or an escalation, you also have to name the target. This rule is enforced inside the database transaction, so every surface (the CLI, MCP, the UI) gets the same guarantee for free. Work cannot quietly fall on the floor.
When work moves from one agent to another, two things have to happen: the old item closes as handed-off, and a new owned item appears at the new owner. OpenRig does both in a single database transaction. Either both commit or neither does. A lost handoff, where the sender thinks it passed the work and the receiver never got it, is impossible by construction.
Around the queue sit a few supporting surfaces. A stream is an append-only intake log: things arrive, get recorded, and never change. An inbox is a mailbox: an agent can drop a message for another agent without that agent claiming it as work yet. An outbox is the sender's matching record. Each transition is logged to an append-only audit trail that domain code can only add to, never edit.
The Workflow Runtime
A workflow turns an intended sequence of work into durable state. You write a workflow spec as a plain markdown or YAML file. The daemon caches it, and when you start an instance of it, the daemon tracks where that instance is in SQLite: which step is active, and how many hops it has taken.
The operating model has a name worth knowing: the owner of a step is the author of its closure, and the workflow runtime is the transactional scribe. The owner decides when their step is done. When they close it, the runtime does four things in one database transaction: it closes the current step, creates the next step's work item, records the move in an append-only trail, and updates the instance. The runtime records and projects the next step; it does not gate the closure. The hot-potato rule from the coordination layer is still the closure authority.
Mission Control is the operator-facing view over the coordination layer. It is a destination inside the UI, not a separate program. It offers a set of read-only views over running work (a personal queue, a human-decision gate, a fleet roll-up, active work, recent ships, recent activity, recent observations) and a set of write actions for acting on a queue item: approve, deny, route, annotate, hold, drop, and handoff. Every action runs as one database transaction and writes an audit row, so the history of operator decisions is reconstructable after the fact.
Runtime Adapters
The daemon never talks to Claude Code, Codex, or a shell directly. It talks to a runtime adapter. An adapter is a small object that knows how to launch and manage one kind of harness. There are three, and they all implement the same five-method contract.
interface RuntimeAdapter {
listInstalled(binding) // what is already projected for this node
project(plan, binding) // write resources to the runtime's locations
deliverStartup(files, binding)// hand the runtime its startup files
launchHarness(binding, opts) // start the harness in tmux; return a resume token
checkReady(binding) // probe whether the harness is responsive
}Projects resources into .claude/ and merges guidance into CLAUDE.md. Launches, resumes, or forks a Claude session.
Projects resources into .agents/ and merges guidance into AGENTS.md. Launches, resumes, or forks a Codex thread.
Every operation is a no-op, because the shell is the harness. Used for infrastructure nodes like dev servers, log tails, and build watchers.
A separate layer answers an honest question that most tools skip: did the harness actually resume, or did it quietly start fresh? OpenRig probes the live session rather than assuming. If a resume fails, it fails loudly and stays failed. There is no silent fallback to a fresh launch. A fresh launch is an explicit follow-up the operator chooses, never a thing that happens behind your back.
The Content Layer
Beyond rigs and agents, OpenRig has a set of content primitives that live on the filesystem rather than in the database. The daemon discovers them and serves them; it keeps an in-memory cache but adds no SQLite state for them.
A folder of related context files plus a manifest. The daemon can assemble a pack into one paste-ready bundle and send it to an agent to prime it with a coherent starting context. Shipped in 0.3.0.
A captured snapshot of a productive agent's resumable state, so you can start a new agent from where a known-good one left off. Deletion is guarded so you do not lose evidence by accident. Shipped in 0.3.0.
A read-only inspection surface for plugins discovered on disk: list, show, used-by, validate. Shipped in 0.3.1. There is no install command yet; installation today is an explicit operator copy. An install verb is planned for a later release.
A typed declaration that lets a rig name where its work lives (a workspace root, named repos, and an optional knowledge root) and have that surface through identity and node inventory. Shipped in 0.3.0.
Snapshot and Restore
The core product loop is: tear a rig down (which auto-snapshots it), bring it back up by name (which auto-restores it), work, repeat. The restore path is built to be honest about what it can and cannot recover.
- Picks the newest session by a monotonic identifier, not by timestamp alone.
- Checks live continuity state before replaying any startup.
- Refuses to restore over a node that is already live or restoring.
- Replays only restore-safe startup from the persisted context.
- Treats a missing optional file as a warning; a missing required file as a hard failure for that node.
- Reports a per-node outcome (resumed, rebuilt, fresh, or failed) and never smooths a failure into a success.
A Few Ideas That Hold the System Together
A handful of ideas run through everything described above. If you have read this far, you have already met most of them, but they are worth gathering in one place because they explain why OpenRig behaves the way it does.
tmux is the transport, not the truth. OpenRig reaches an agent and reads its output through tmux, but the durable record of what a rig is and what happened to it lives in SQLite and on the filesystem. When the two disagree, the database wins.
Restore is honest. A failed resume fails loudly and stays failed. OpenRig never quietly starts a fresh agent and dresses it up as a recovered one. A fresh launch is always something the operator chooses on purpose.
Startup is layered and predictable. What an agent knows at boot is assembled in a fixed order: the agent's own base, then its profile, then the rig's culture file, then rig, pod, and member startup on top. Each layer adds to the one before it, so you can always reason about what a given agent will see.
Identity is named, not generated. Every agent address, in the form {pod}-{member}@{rig}, is written by a person and checked by the system. Nothing is auto-generated or slugified, so the names you see are the names you chose.
Every command that changes something explains itself. When a command brings a rig up, tears it down, snapshots it, or restores it, it ends by telling you what happened, what the current state is, and what a sensible next step would be. The system tells you the truth so you can trust what you see.
The Event System
Everything that happens in a rig is recorded as an event in an append-only, SQLite-backed log. Events cover the full surface of the system: node lifecycle, the coordination queue, workflows, the watchdog scheduler, Mission Control actions, restores, and more. The UI subscribes to these events over a few server-sent-event streams, which is how the operator view stays live without polling. The log is the system's memory: it is the record you read when you want to know what a rig did, and when it happened.