This map is not the territory. It is a zoomed-out guide for finding your way around the code. It is incomplete by design and it may be stale. Where it disagrees with the code, the code is right; please fix the page. Every path, symbol, count and command below was checked against commit
c750304f(0.6.6 development). Each count shows the command that produced it, so you can re-run it.
OpenRig runs a team of coding agents on your machine as one system. A local daemon keeps the team's
state in SQLite and runs each agent as an ordinary Claude Code or Codex session (or a plain
terminal, a Pi or Oh My Pi (OMP) runner, or a scripted test stub) inside tmux. People and agents drive it through the
rig CLI, a terminal UI (TUI) and an MCP server, all of which talk to the daemon over local HTTP.
The agents themselves are unmodified. What OpenRig adds is the coordination around them: launching
and restoring agents, delivering messages, keeping durable work in a queue, and giving each agent
the right skills and context.
| Directory | Package | What it is | How it ships |
|---|---|---|---|
packages/daemon |
@openrig/daemon (private) |
The daemon: HTTP routes, domain services, SQLite migrations, runtime adapters, built-in specs, skills, plugins, static context-pack sources | Copied into @openrig/cli |
packages/cli |
@openrig/cli |
The rig command, the MCP server, daemon start and stop |
The only published package |
packages/tui |
@openrig/tui (private) |
The terminal UI | Copied into @openrig/cli; opened by rig tui, by bare rig in a terminal, or by the openrig-tui bin |
packages/ui |
@openrig/ui (private) |
The React web UI, in maintenance mode: it still ships and runs, but gets no new feature work. The CLI and TUI are the supported surfaces | Built files copied into @openrig/cli and served by the daemon |
packages/test-system |
none (not an npm workspace) | The stub-agent scenario library, the CI scenario harness, live-model eval cases | Not shipped |
What bundles what. packages/cli/package.json publishes LICENSE, dist, daemon, ui,
tui and scripts. scripts/build-package.sh (the CLI's prepublishOnly) builds the daemon, UI,
TUI and CLI, then assembles them inside packages/cli/: the daemon's dist, assets, specs and
policies, the generated context packs and a copy of docs/reference/ go under daemon/; the UI
build goes to ui/dist; the TUI build goes to tui/dist. So docs/reference/ ships to every
user; docs/as-built/ and this file do not.
Sharing code between packages. The CLI and TUI import daemon code only through the subpaths in
the exports field of packages/daemon/package.json (21 at this commit), for example
@openrig/daemon/attention, which points at packages/daemon/src/attention-surface.ts. The CLI
uses 19 of them and the TUI uses 2. In development they resolve through the npm workspace. At
package time scripts/rewrite-daemon-imports.mjs rewrites them to the shipped daemon/dist copy
and fails the build on a specifier with no exports entry. To share new daemon code, add a
*-surface.ts file and an exports entry.
rig (CLI) TUI MCP server (rig mcp serve, stdio)
\ | /
+---- HTTP to the daemon (port 7433 by default) --------------+
|
packages/daemon/src/server.ts createApp(): context middleware, /healthz, app.route("/api/...")
|
packages/daemon/src/routes/ one Hono router per area; reads services with c.get(...)
|
packages/daemon/src/domain/ services, repositories, orchestrators, event bus
/ | \
SQLite (db/) tmux (adapters/tmux.ts) runtime adapters (adapters/*-adapter.ts)
claude-code, codex, pi, omp, terminal, stub
Inside the daemon (packages/daemon/src/):
- Startup.
index.tsis the process entry: it resolves where to listen, starts the server and runs the periodic queue sweeps. It callscreateDaemon()instartup.ts, which opens the database (db/connection.ts: WAL mode, foreign keys on), runsmigrate(db, ALL_MIGRATIONS), constructs every service and adapter, and passes them tocreateAppWithWebSocket(deps)inserver.ts. server.ts. IncreateApp(deps), an earlyapp.use("*", ...)middleware (after the request-phase observer and the JSON body-parse tracker) puts each service on the request context (c.set("<name>" as never, deps.<name>)), serves/healthz, and mounts each router withapp.route("/api/<area>", ...). Unknown/api/*paths get a JSON 404; other GET requests serve the web UI's built files.routes/. Thin handlers. For example,routes/ps.tsreadspsProjectionServicefrom the context and returns its entries as JSON.routes/terminal-ws.tsregisters the terminal WebSocket, androutes/require-sender-identity.tsis a shared helper rather than a router.domain/. The behaviour: repositories over SQLite (rig-repository.ts,queue-repository.ts,session-registry.ts), orchestration (startup-orchestrator.ts,node-launcher.ts,rigspec-instantiator.ts), message delivery (session-transport.ts,seat-delivery-guard.ts) and events (event-bus.ts). Larger areas have their own subdirectories, such asgateway/,provider/,policies/,scope/,context-packs/andmission-control/.adapters/. The outside world.tmux.tswraps tmux. The sixRuntimeAdapterimplementations (OMP's extends Pi's) launch each runtime and project skills and files into it.cmux.tsandcompose-services-adapter.tscover other integrations.db/.connection.ts,migrate.ts,all-migrations.tsandmigrations/.middleware/auth-bearer-token.ts. The bearer-token check for operator write routes, plus the loopback and Tailscale bind detection used at startup.- Live updates go out as server-sent events:
routes/events.tsuses Hono'sstreamSSE, and the TUI subscribes to/api/activity/events. - State lives in
$OPENRIG_HOME(default~/.openrig). The database isopenrig.sqlitethere (daemon-db-path.ts).
The clients:
- CLI.
createProgram()inpackages/cli/src/index.tsregisters every top-level command; the commands live inpackages/cli/src/commands/. Most are thin clients: they call a daemon route throughDaemonClient(packages/cli/src/client.ts) and render the result. A few lifecycle helpers open the database file directly. Barerigin a terminal opens the TUI (packages/cli/src/front-door.ts). - MCP.
createMcpServer(client)inpackages/cli/src/mcp-server.tsregisters the tools over the sameDaemonClient.rig mcp serve(packages/cli/src/commands/mcp.ts) runs it on stdio. - TUI.
packages/tui/src/daemon-client.tsis its only HTTP module. It reads existing daemon projections, for example/api/ps,/api/rigs/summaryand/api/queue/list, and the/api/activity/eventsstream when the daemon offers it. The daemon URL comes from--url,OPENRIG_URL, orhttp://127.0.0.1:7433. Agents can drive a running TUI through its control socket (packages/tui/src/socket-server.ts). - Web UI. Served by the daemon from the bundled
ui/dist.
In a development checkout, rig daemon start prefers packages/daemon/dist over the bundled
copy (resolveDaemonPath in packages/cli/src/daemon-lifecycle.ts), and the TUI launcher prefers
packages/tui/dist (packages/cli/src/front-door.ts). Rebuild the package you changed. A daemon
that is already running keeps running the code it started with.
At commit c750304f. Run these from the repository root to refresh them.
| What | Count | Command |
|---|---|---|
| Database migrations | 94 files (latest: 095_rig_non_interruptive.ts; there is no 093) |
git ls-files packages/daemon/src/db/migrations | wc -l |
Files in routes/ |
67 | git ls-files packages/daemon/src/routes | wc -l |
| ... of which create a Hono router | 65 | git grep -l 'new Hono' -- packages/daemon/src/routes | wc -l |
app.route(...) mounts in server.ts |
69 | grep -c 'app.route(' packages/daemon/src/server.ts |
Top-level rig commands |
87 | grep -c 'program.addCommand(' packages/cli/src/index.ts |
| Runtime adapters | 6 (claude-code, codex, pi, omp, terminal, stub); the command counts 5 because OmpRuntimeAdapter extends PiRuntimeAdapter |
git grep -l 'implements RuntimeAdapter' packages/daemon/src | wc -l |
| MCP tools | 18 | grep -c 'server.tool(' packages/cli/src/mcp-server.ts |
Daemon exports subpaths |
21 | node -p 'Object.keys(require("./packages/daemon/package.json").exports).length' |
| Library scenarios | 15 | grep -l '^scenario:' packages/test-system/scenarios/*.yaml | wc -l |
Each recipe names the files to touch. Read a neighbouring example before you start; the code is the authority.
- Create
packages/cli/src/commands/<name>.tsexporting a function that returns a commanderCommandand takes optional injected dependencies.packages/cli/src/commands/unarchive.tsis a short example. - In the action, get the daemon with
getDaemonStatus(...)anddaemonStatusGuard(status)(packages/cli/src/daemon-lifecycle.ts), then call it withnew DaemonClient(getDaemonUrl(status))(packages/cli/src/client.ts). Offer--jsonfor agents, and setprocess.exitCodeon failure. - Register it in
createProgram()inpackages/cli/src/index.tswithprogram.addCommand(...). If it takes injected dependencies, add them toProgramDepsin the same file. - Add
packages/cli/test/<name>.test.ts.packages/cli/test/archive.test.tsshows how tests inject mocked lifecycle dependencies. - If the command needs new daemon behaviour, add a route as well.
- Create
packages/daemon/src/routes/<area>.tswith a Hono router. Both styles exist: a constant (export const psRoutes = new Hono()inroutes/ps.ts) and a factory (whoamiRoutes()inroutes/whoami.ts). - Read services from the request context with
c.get("<name>" as never). Keep the behaviour inpackages/daemon/src/domain/. - Mount it in
createApp()inpackages/daemon/src/server.tswithapp.route("/api/<area>", ...), above the/api/*404 catch-all. - A new service needs three more edits: a field on
AppDepsand ac.set(...)line inserver.ts, and its construction increateDaemon()inpackages/daemon/src/startup.ts. - Test it the way
packages/daemon/test/whoami-routes.test.tsdoes: build a small Hono app,c.setthe service, mount the router and callapp.request(...).createFullTestDb()inpackages/daemon/test/helpers/test-app.tsgives you an in-memory database migrated with the test fixture list (migrationsForFullTestDb).
- Create
packages/daemon/src/db/migrations/<NNN>_<name>.tsexporting aMigration({ name: "<NNN>_<name>.sql", sql: "..." }; the type is inpackages/daemon/src/db/migrate.ts).089_classification_identity_provenance.tsis a one-line example. - Take the next number after the highest on
main(095 at this commit). Parallel pull requests often pick the same number, so check again and renumber ifmainmoved before you merge. - Import it in
packages/daemon/src/db/all-migrations.tsand append it toALL_MIGRATIONS. Startup runs exactly that list (startup-migrations-mirror.test.tspins this). - Add it to
migrationsForFullTestDbinpackages/daemon/test/helpers/test-app.ts, or list it inmigrationsForFullTestDbExclusionswith a reason.migration-fixture-parity.test.tsfails otherwise. migrate()sorts migrations by name and records each applied name inschema_migrations, so an install that already applied a migration never runs it again. Editing a migration that has shipped changes nothing for those installs; add a new one instead. A long-lived install applies every new migration on its next start, against real data.
- Implement
RuntimeAdapterfrompackages/daemon/src/domain/runtime-adapter.ts:runtime,listInstalled,project,deliverStartup,launchHarnessandcheckReady. The smallest implementation ispackages/daemon/src/adapters/terminal-adapter.ts; the scripted test double isstub-runtime-adapter.ts. - Register it in
packages/daemon/src/startup.ts. The adapter maps are keyed by runtime name:adapters:(for the pod instantiator) andruntimeAdapters:. The context monitor has its own map for the runtimes it samples. - Accept the runtime name in rig spec validation:
SUPPORTED_RUNTIMESinpackages/daemon/src/domain/rigspec-preflight.ts. Other runtime lists, such asRuntimeHintindomain/discovery-types.tsandLEGACY_KNOWN_RUNTIMESindomain/rigspec-schema.ts, may need it too; search for an existing runtime name such as"pi"to find them. - Launch, projection and readiness are arteries. Read docs/as-built/arteries.md first.
Skills ship from three copies in this repository (the "edges"), listed in
scripts/skill-edge-layout.generated.json:
| Edge | Path | Layout |
|---|---|---|
spec |
packages/daemon/specs/agents/shared/skills/ |
by category (core, pm, pods, process) |
plugin |
packages/daemon/assets/plugins/openrig-core/skills/ |
flat |
canonical |
skills/_canonical/ |
public mirror of the spec copy |
Each skill in the layout file lists which edges carry it. At this commit, 33 skills are in spec
and canonical, 17 only in plugin, and 2 in all three.
- Each skill has one editable home: its
speccopy when it has aspecedge, otherwise itsplugincopy. Its other edges are generated from that home. npm run mirror-skills:check(part ofnpm test) hashes every file in each edge and compares the hashes withscripts/skill-edge-digests.generated.json. It also fails on aSKILL.mdthe layout does not list, on a generated copy that differs from its home, and on a home holding text the public copies would drop.- The full apply,
npm run mirror-skills, regenerates every generated copy from its home and refreshes the digests. It reads nothing outside the repository and never writes to a home. - To fix a skill: edit its home, run
npm run mirror-skills, then runnpm run mirror-skills:check. - To add a skill: open an issue first. Its layout entry is generated from files that are not in this repository.
- Every shipped skill also becomes a context pack at package time (next recipe).
- Static packs are hand-written: a directory under
packages/daemon/context-packs-src/<name>/with amanifest.yamland its files. Keepversion: "0"in the manifest; the generator stamps the package version. A pack file may be a symlink to a file underdocs/reference/, so a reference document has one source (for examplehelp/help.mdpoints todocs/reference/help.md). Any other symlink fails the build. There are 5 static packs at this commit. - Generated packs go to
packages/daemon/context-packs/, which is gitignored and never edited by hand.scripts/generate-context-packs.mjsbuilds it at package time from every shipped skill and every static pack. npm run generate-context-packs:check(part ofnpm test) validates every pack through the daemon's own manifest parser, so build the daemon first. It also runs the leak scan over the static sources, including thedocs/reference/files they link to.- The daemon serves the shipped packs through
rig context get;startup.tsregisters the bundled directory as its built-in root.
Scenarios run the real CLI, daemon, tmux and SQLite with scripted stub agents (runtime: stub)
and no model cost.
- Add
packages/test-system/scenarios/<defect-class>.yaml, named for the class of bug it catches. Topology fixtures sit beside it (*-stub.yaml). - The format is checked by
validateScenarioinpackages/daemon/test/helpers/scenario-schema.ts. Step verbs:up,down,send,restart,restore,emit,mutate,policy,seed_regression,daemon, plusexpect.expectmay only read shipped surfaces:ps,queue,stream,scope,pane,transcript,tui_socket,policy_provenance. - Today the runner binds
up,down,send,restartanddaemon.restore,emit,mutateandpolicyfail withUnboundActionError, andseed_regressionneeds a fault controller (packages/daemon/test/helpers/scenario-real-deps.ts). - Write a header comment naming the defect class and the seed: what a seeded regression plants,
and which
expectmust catch it. A scenario counts only as a pair: it passes on healthy code and fails on the seeded run (packages/test-system/README.md). - Run it on your machine after
npm run build:node --import tsx packages/daemon/scripts/run-scenarios.mjs <file.yaml>. It uses a private daemon and a private tmux server, and builds its environment fromHOME,PATHandTERMonly, so your session'sTMUXand daemon variables are dropped and it is safe to run from inside tmux, including an agent's pane. It supplies no fault controller, so aseed_regressionstep fails there; the seeded pair runs in the container path. - CI. The
installed-scenariojob runsscripts/run-pr-scenarios.shin disposable, network-less containers built from the packed package. It runs four cases:fixtureandlibrary(queue-baton-survives-restart.yaml), each with a healthy run and a seeded lost-baton run, plustranscriptandcapture, healthy runs only. Adding a seeded case means changing three files: the--caselist inscripts/run-pr-scenarios.sh,CASESinpackages/test-system/ci/result.mjs, and the fault controller inpackages/test-system/ci/run.mjs; a healthy-only case goes inPASSING_CASESin the first two and needs no fault controller. docs/as-built/test-layers.md covers both run modes, running a case on your own Docker host, and what a stub can and cannot prove.
npm test runs test:repo (builds the daemon, runs node --test scripts/*.test.mjs, the docs
guard, mirror-skills --check and generate-context-packs --check) and then the daemon, CLI and
TUI suites. npm run test:ui runs the web UI suite separately. npm run lint typechecks all four
packages.
- Docs guard (
scripts/check-docs-guard.mjs). Underdocs/, git may track onlydocs/as-built/,docs/reference/,docs/releases/and exactlydocs/DESIGN.md;.gitignoreignores the rest ofdocs/. Remember thatdocs/reference/ships inside the npm package, and the daemon copies its Markdown files to$OPENRIG_HOME/reference/when it starts. Files indocs/as-built/carry alast-verified-against-sourcecommit in their frontmatter. - Internal-leak guard (
scripts/internal-leak-scanner.mjs, with rules inscripts/internal-tokens.generated.json). It refuses maintainer-internal names and paths in content that ships. It runs ingenerate-context-packs.mjs(static pack sources, on everynpm test), in the full skill mirror apply (each public skill), inscripts/build-package.sh(the daemonspecs/tree before it is copied into the package), and in the release-time substance gate (scripts/check-substance-gate.mjs,npm run gate:substance) over the packed CLI. No workflow runs it over a pull request's whole diff. - Portability report (
.github/workflows/portability-report.yml, runningscripts/portability-report.mjs). On each pull request it lists added lines containing machine-specific values: credentials, home and temporary paths, network addresses, email addresses. It never fails the job; you read it and decide. - Generated files. Do not hand-edit
scripts/*.generated.jsonor anything underpackages/daemon/context-packs/. The skill-edge digests are refreshed bynpm run mirror-skills(see "A shipped skill"). The internal-token rules support optionalallowed_context_linesfor reviewed generic examples: the complete line must match exactly, including indentation and case. Unlikeallowed_context_substrings, this does not exempt a line with added or substituted text; internal-path checks and the substance gate's human-review dispositions still apply. - Hosted CI (
.github/workflows/tests.yml):build-and-package,typecheck,repo-checks,package-tests(macOS; daemon, CLI, TUI and UI suites with no credentials and no external network) andinstalled-scenario.
- CONTRIBUTING.md: setup, what a pull request needs, what to expect from review.
- docs/reference/developing.md: which checks block and which are
advisory, and what
.github/workflows/tests.ymlruns. - docs/reference/worktree-builds.md: building in a git
worktree, with its own
npm cirather than a symlinkednode_modules. - docs/reference/: user and operator reference; it ships with the package.
- docs/as-built/: module-by-module descriptions of the system. These
are being re-verified for 0.6.6. Each page's
last-verified-against-sourcenames the commit it was checked against; until a page is re-verified, treat its counts as historical. Use them for orientation, then check the code. - docs/as-built/test-layers.md: what each test layer covers and what it cannot catch.
- docs/as-built/arteries.md: the areas where a small change has a large effect, what depends on them, and what broke there before.
- packages/test-system/ci/README.md: the CI scenario job.
- docs/releases/ and CHANGELOG.md: release history, written by the maintainers at release time.