* docs: add contributor maps and a developing-openrig skill Add ARCHITECTURE.md (packages, request path, counts with the commands that produce them, where to add a command, route, migration, adapter, skill, context pack or scenario), docs/as-built/arteries.md (high-impact areas, their dependents and past regressions) and docs/as-built/test-layers.md (what to run before a pull request and what each layer proves). Add a repo-local developing-openrig skill for Claude Code (.claude/skills) and Codex (.agents/skills) that routes to these maps, with narrow .gitignore exceptions so only that skill is tracked. Point CONTRIBUTING.md at the maps and correct its skill-mirror instructions. Replace the private-host paragraph in the shipped openrig-skills router with a pointer to the repository skill. * docs: correct six factual points from review Newest as-built markers; skills may have one or several copies; the host scenario runner drops rather than refuses TMUX and daemon variables; scenario-10 declares its own normaliser; the managed Claude launch applies only with an explicit permission mode; Dockerfile.scenarios is layered by run-pr-scenarios.sh. Also tighten several wordings the review flagged. --------- Co-authored-by: v-openrig-build <v-openrig-build@users.noreply.github.com>
22 KiB
OpenRig architecture
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
1347d825(v0.6.3-33-g1347d825c). Each count shows the command that produced it, so you can re-run it.
What OpenRig is
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 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.
Packages
| 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 (18 at this commit), for example
@openrig/daemon/attention, which points at packages/daemon/src/attention-surface.ts. The CLI
uses 16 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.
The request path
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, 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), the firstapp.use("*", ...)middleware 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 fiveRuntimeAdapterimplementations 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.
Key counts
At commit 1347d825. Run these from the repository root to refresh them.
| What | Count | Command |
|---|---|---|
| Database migrations | 89 (latest: 089_classification_identity_provenance.ts) |
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 |
85 | grep -c 'program.addCommand(' packages/cli/src/index.ts |
| Runtime adapters | 5 (claude-code, codex, pi, terminal, stub) |
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 |
18 | node -p 'Object.keys(require("./packages/daemon/package.json").exports).length' |
| Library scenarios | 11 | grep -l '^scenario:' packages/test-system/scenarios/*.yaml | wc -l |
Where to add things
Each recipe names the files to touch. Read a neighbouring example before you start; the code is the authority.
A CLI command
- 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.
A daemon route
- 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).
A database migration
- 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(089 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.
A runtime adapter
- 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.
A shipped skill
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.
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.- The full apply,
npm run mirror-skills, regenerates all three edges from the maintainers' skill source, which lives outside this repository. It needsOPENRIG_SKILL_CANON_ROOTand three authority-file environment variables, so an outside contributor cannot run it. - To fix a skill: edit the file in every edge that carries it (same bytes), run
node scripts/regen-edge-digests.mjsto refresh the hashes from disk, then runnpm run mirror-skills:check. Say in the pull request that you edited mirrored skill files: the next full apply regenerates these copies from the maintainers' source, so the change has to land there too. - 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).
A context pack
- 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.
A scenario
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, from a shell that is not inside tmux:node --import tsx packages/daemon/scripts/run-scenarios.mjs <file.yaml>. It uses a private daemon and a private tmux server, and refuses to start ifTMUXis set. 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 two cases, both built around queue durability (libraryisqueue-baton-survives-restart.yaml). Adding a case today 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. 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.
Guards you will meet
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 the one generated file a contributor refreshes, withnode scripts/regen-edge-digests.mjs(see "A shipped skill"). - 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.
Pointers
- CONTRIBUTING.md: setup, what a pull request needs, what to expect from review.
- docs/reference/developing.md: which checks block and which are
advisory. It predates the hosted Tests workflow;
.github/workflows/tests.ymlis current. - docs/reference/worktree-builds.md: building in a git
worktree, with its own
npm installrather 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. Most modules were last verified against
7eaf524c(2026-05-16, around v0.3.1); the two pages this change adds,arteries.mdandtest-layers.md, are verified against1347d825. The older modules' counts are historical:architecture/daemon-core.mddescribes 40 migrations, and there are 89 today. 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.