rig3.5.3

setup

One binary. Then the room.

rig needs one thing you already have — an OpenAI-compatible endpoint — and one thing you write — a row for the model you mean to name. Everything else lives in ~/.rig: settings, the model table, your standing instructions, the stores.

This page is the operating story: how rig replaces the router in front of your models, how a model is loaded and unloaded without a supervisor process, and which of the two faces you open in the morning.

install

Four paths, one binary

The installer is POSIX sh, needs no Go and no sudo, and lands one file in ~/.local/bin. The release asset is the same binary with the same checksums. rig -update fetches, verifies and installs atomically; the running process keeps the old binary until you restart it.

installer

curl -fsSL https://tryrig.ai/install.sh | shPOSIX sh · no Go · no sudo · ~/.local/bin

release

rig_<os>_<arch> from releases/latest, with checksums.txt; linux and macOS, amd64 and arm64.github.com/mrsirg97-rgb/rig/releases

go install

go install github.com/mrsirg97-rgb/rig/v3/cmd/rig@latestGo 1.26.6 or newer

from source

go build ./cmd/rig or make install; BINDIR=... names the directory.$(go env GOBIN), else ~/.local/bin

updating is a room operation

rig -update drains the room first: every member stops claiming at its turn boundary and leaves, and only then does the binary swap. A worker or a turn in flight is not racing the new binary. The next start is the new version; the current process is not.

the rig home

~/.rig is the machine's state

The home resolves $RIG_HOME over ~/.rig, and every file in it is optional except the model table's content: the binary ships no model rows. A present-but-malformed file refuses at start, naming the file and the field; unknown keys refuse too. This is a contract, not a filter.

~/.rig/
  settings.json      the knobs, flat, by their env names
  models.json        the model table: one row per model
  AGENTS.md          your standing instructions, ahead of the project's
  theme.json         the terminal palette (custom base + slots)
  sessions/          the state store: transcripts, usage, resume
  todo/              the task board: one file, rows carry the project scope
  rem/               memory, scoped to the repo (worktrees share)
  graph/             the code map: symbols, edges, files per worktree
  decision/          the decision store, and train/ with its jsonl splits
  plugins/           live, pending/, disabled/
  train/             the decision model's trainers, its own zone
  room/              the machine's unix sockets
  logs/              the models' children and what they served

Every knob resolves through four layers, per key: flag > env > file > embedded default. A flag you typed always wins, whatever its value; an empty env or file value descends, except the two presence keys (RIG_WEB_FETCH_PROXY and RIG_TRAFILATURA), where "set empty" is an explicit choice. On a start where the home is absent and the old ~/.config/rig exists, it is renamed once and says so.

settings.json

{
  "model": "local",
  "baseUrl": "http://127.0.0.1:8090/v1",
  "sandbox": "jailed",
  "approve": "auto",
  "theme": "warm"
}

AGENTS.md

# how I like to work
- run the tests before you say it's done
- prefer small commits, one idea each
- never add a dependency without saying why

The project's own AGENTS.md is the nearest one walking up from the workspace to the repository root, and no further: a session opened in a subdirectory reads the repo's contract, a workspace that is no repository reads only its own file. It is read when the session wires, so a session carries the contract of the workspace it opens in.

the router

rig is the router, and there is no router process

Since 3.0.0 nothing routes in front of your models. llama-swap left the stack and nothing took its place. Every rig instance on the machine joins one room over a unix socket under the rig home, and the model is a guarded key in that room: an instance sends its request to the model's port itself, once its claim is final by every live member's acknowledgement.

The order is the operation, not a process. A claim carries its mint, its key and its priority; every member folds the same fields the same way — priority first, then tick, member, nonce — so a late act takes its rightful place and a duplicate is refused by its mint. The classes are the room's priorities:

 90  the operator's turn
 70  a delegate's return
 30  a fleet worker's claim
 20  a child's start or stop
  0  an outside client

Inside a turn the batch has its own two rungs — a write 60, a read 50 — and those are the kernel's order, not the room's.

A claim that finds the row's port silent holds its place, publishes the start, and sends when the port answers. When another row is resident it publishes the swap on that row's key instead, so the resident one stops at its holders' end and before the incoming one starts. One child at a time — the rule the retired router had, now an order. What is loaded is read from the world, never replayed: the port is probed at a join, and a child's /health saying 503 is what "still loading" means.

mesh in settings.json extends one room to many machines: a TCP door the tailnet reaches, gossip converging the machines' folds. The mesh's order is advisory — a key's home machine is its authority — and a foreign claim past the horizon reposts at the home machine, where that row's child runs. The cost is one horizon at the front of a remote request, about 1.3 s at the defaults, measured on loopback.

what this buys

The room's design note measured the problem it replaced: behind an arrival-ordered server, the operator's turn waited behind whichever of six workers arrived first. Two conversations decoding at once raised the total to about 90 tok/s against 64 alone, but a single overlapped stream was a lottery — of 111 overlapped replies the median ran 45 tok/s, one in ten under 32, the slowest at 12; alone, the slowest was 55. A model swap took about 50 s and ended whatever was in flight, and nothing ordered it. The fix was not a faster router; it was deciding who is waiting at the engine.

models

Loading and unloading are claims and swaps

A model row is the whole model: its window, its budget, its dials, and — when the machine serves it — the child half. The child half is how a row says "I know how to start this model", and nothing else starts it:

command

The child's command line, with ${PORT} substituted. Detached; its log lands at ~/.rig/logs/<model id>.log.

env

The child's environment, as the row spells it.

port

The child's fixed port, chosen at creation from the range the import carries in. The provider sends to http://127.0.0.1:<port>/v1 directly.

aliases

The names that resolve to the row.

stopGrace

Seconds the SIGTERM waits before the kill. A stop sends to the child's session, not to a pid file whose start time differs.

places

How many requests the model serves at once — two with two sequences, three for an -np 3 child. One counted key, one holder set; no key per place, because the server has no identity per place.

The row with its child half, and a hosted row beside it:

[
  {
    "id": "local",
    "window": 393216, "maxTokens": 65536,
    "reserve": 104858, "keepRecent": 98304,
    "effort": "xhigh", "efforts": ["low", "medium", "xhigh"],
    // the child half:
    "command": "llama-server -m ~/models/qwen.gguf --port ${PORT} -np 2",
    "port": 8090,
    "aliases": ["qwen"],
    "stopGrace": 10,
    "places": 2
  },
  {
    "id": "sonnet",
    "window": 200000, "maxTokens": 8192,
    "reserve": 16384, "keepRecent": 40000,
    "provider": "openrouter",
    "baseUrl": "https://openrouter.ai/api/v1",
    "apiKey": "sk-or-…",
    "reasoning": "reasoning"
  }
]

Loading is a claim that found the port silent: the instance publishes the start and waits for the port, and the child is the row's own command. Unloading is a swap's stop, ordered on the resident model's key, at the holders' end and before the incoming start — never eviction of a request mid-flight. A strictly higher-priority claim may enter while the places are full; the entry is one holder beyond the places, never eviction, and a same-priority claim queues for a place. Between claims, the continuation wins: within one priority, the claim naming the conversation already in the model goes first, because a dropped reply costs a full prompt re-read.

Inside a session, the verbs are the same table:

/models

Lists the table with the active row marked; /models <id> switches for the next turn and moves the open session's row with it.

/effort

The reasoning dial. Bare shows the active level and the row's levels; /effort <level> sets the next turns.

/role

The stance: default, architect, reviewer — prose between the system prompt and AGENTS.md.

/approve

auto, or manual: every mutating call pauses for the operator's y/n at the ask row. A denial is a teaching refusal.

/theme

warm, cool, custom. Custom is ~/.rig/theme.json: a base plus the slots you want to change.

the faces

The TUI, or the page — one session

Four frontends run over one session: the TUI, the piped CLI, the -p one-shot, and the web dashboard. They are not four products with four states; they are four doors onto the same loop, the same stores, and the same commands.

the TUI — rig

Chosen when stdout is a terminal; --tui=false forces the piped reference, --tui=true forces the TUI (a pty, so tmux capture-pane works).

  • three status rows: identity (model · used/window), stance (effort · role · auto|manual), usage totals.
  • a live region redrawn in place: the activity row, the input line.
  • the todo and scheduler blocks, and the usage line at every turn's end.
  • the swarm band: live counts per role, one row for a delegated batch.
  • steering: a line typed while a turn is live interrupts it and is delivered at the next prompt, latest wins.

the dashboard — rig serve

Loopback only (-addr, default 127.0.0.1:7777), behind a token minted once and stored 0600, exchanged for a cookie. A non-loopback address is refused by name. From Safari: share → add to home screen, and it installs as an app.

  • the live session streamed in the TUI's grammar — approvals answered in place, stop while a turn runs, the same ❯ prompt and commands.
  • sessions per workspace with transcripts and resume.
  • the queue, the jobs with their run audit, the swarm.
  • the model table, switch and effort dial.
  • the plugins' three zones and the forge's save into pending/.
  • the room page — where it opens: the members, each model's residency and who waits, whether the port answers, the served log's tail, and the machine's vitals read from /proc, /sys and nvidia-smi.

The dashboard is the only face for the room: there is no slash command for it. The TUI is the only face for memory: there is no memory view on the page — rem is the model's, read in the terminal.

scheduled workers

The crontab is the clock

A scheduled job is a one-shot rig -p in its own cwd, on your crontab, wrapped in the sandbox profile of your choice. The crontab is the scheduling truth; the store is its audit and its repair. A job's fire is its own process, so it cannot outlive the schedule.

# crontab — the job's key is its one positional
0 */2 * * *  rig run-job j8-health

# or from inside a session, the same store:
/scheduler create health "check the fund" 0 */2 * * *
jailed

The default: bubblewrap, unshare-all. Netless except the one bound socket the model calls ride; the home is a scratch directory inside the job's cwd, so a jailed worker never discovers your plugins — protection by absence.

landlock

The in-kernel profile. rig -exec applies the domain and syscall.Execs the argv, so every thread inherits a wall an in-process call cannot put on threads that already exist.

off

The explicit act: the worker runs as you, and says so once per run.

binds

sandboxBinds: absolute paths, read-only unless the entry ends :rw — the operator's need, e.g. a python venv.

Each job carries its own timeout and budget; a model that is not resident refuses by name, naming the holder. The interactive REPL and one-shot runs never consult the sandbox code: the jail is for work that outlives you.

verify

Four checks, no trust

# 1. the binary
rig --version

# 2. the endpoint, with the model row you wrote
rig --base-url $YOUR_ENDPOINT --model $NAME --system "be terse"

# 3. the terminal: the three-row status, a streaming turn, a tool line
# 4. the same session to a capturable pane
tmux new-session -d 'rig --tui=true' \; capture-pane -p

If nothing streams, check the endpoint first: rig surfaces provider faults verbatim and loudly, and does not hide them. The piped path (--tui=false) is the byte reference — the TUI adds to those bytes, never changes them.

gotchas

The refusals you will meet first

no model

The binary ships no model rows and has no model default. Naming an ID the table does not know refuses at start, before any store opens, naming the three ways to set it: --model, RIG_MODEL, settings.json's model.

bwrap

Missing bubblewrap refuses with both settings keys named. On Ubuntu 24.04 and friends, AppArmor blocks bwrap's user namespace: the fix is kernel.apparmor_restrict_unprivileged_userns=0, named in the refusal.

plugins.max

The cap is read with the settings at startup; raising it takes a restart. An over-cap load is skipped, naming the cap — the first N files by name win.

allow

A settings.json that writes its own allow key replaces the built-in default whole. It must carry plugin, or every door call is refused; a plugins entry is dropped with a notice.

resume

--resume <id> rebuilds the session from the state store in one read-only transaction. It refuses beside -p: a one-shot stays one-shot.