Relais is a coding-agent execution companion: it chooses a model and reasoning effort for a bounded coding task, supplies authoritative context, verifies the result, and escalates when necessary. Its objective is to reduce total cost per accepted change while preserving an explicit quality bar.
Status: in development against the target specification in docs/SPEC.md. The
commands, schemas and integrations there are proposed interfaces, not shipped
features. Relais is a working name; package availability has not been checked.
curl -fsSL https://raw.githubusercontent.com/fredericrous/relais/main/install/install.sh | sh
Pin a version or move the destination with RELAIS_VERSION and
RELAIS_BIN_DIR. Windows:
irm https://raw.githubusercontent.com/fredericrous/relais/main/install/install.ps1 | iex.
Prebuilt, checksummed binaries for Linux, macOS and Windows are on the
releases page; a release
is cut by a v* tag that matches Cargo.toml and has a CHANGELOG.md
section. Or from source: cargo install --path crates/relais.
make check # toolchain lint test msrv audit
make buildCI (.github/workflows/ci.yaml) runs the same targets on every push and
pull request: lint (fmt, clippy -D warnings, the module-cycle gate) on
Ubuntu; the test suite on Ubuntu, macOS and Windows (the
coordinator's endpoint and process handling are a different
implementation on Windows, and the product ships to a macOS workstation).
macOS is relais's primary platform (decisions ADR-0041,
ci.platform-schedule): a pull request tests macOS, and tests Ubuntu and
Windows only when it touches platform code (scripts/ci-platform-touched.sh:
the workflows, scripts, manifests, toolchain, Makefile, installers, or a
source file under cfg(windows|unix|target_os|target_family)) or carries
the full-ci label. Otherwise those two legs run on the push to main, the
run summary says so, and the release waits for all three on the tagged
commit. The suite includes the release scenarios in
crates/relais/tests/it/portable_scenarios.rs, which drive the real binary
on all three; the ones in release_scenarios.rs need a sh fake worker
and stay unix-only;
the msrv build on Ubuntu, against the rust-version it reads from
Cargo.toml; and make audit on Ubuntu, also weekly on a schedule and
deliberately non-blocking. The release workflow's own audit job does
block, and runs cargo test on the tagged commit before publishing
anything.
A green check on a PR means what a green make check means on the
workstation. Locally, msrv fails rather than skipping when the pinned
toolchain is absent (MSRV_SKIP_OK=1 opts out), and audit installs
cargo-audit if it is missing (AUDIT_SKIP_OK=1 skips instead) — so a
green make check means all four gates really ran.
One table, in crates/relais/src/main.rs, matched exhaustively. Codes are
split wherever a caller has to act differently.
| code | meaning |
|---|---|
| 0 | the command did what was asked |
| 1 | relais's own machinery failed (ledger, registry, a file it owns) — nothing is implied about the task |
| 2 | the invocation, or a file it named, is invalid |
| 3 | policy, trust, admission or the environment refuses; no model ran. This includes a kind: verify run that is blocked on its candidate (candidate_source_running, candidate_missing, candidate_foreign_repo, candidate_does_not_apply, candidate_digest_mismatch), and a relais run --intent for an intent already claimed (intent_already_claimed, naming the run that holds it). A run that waited out a budget on a loaded machine is blocked too: admission_wait_exhausted (queued for a lease past [resources] admission_wait_seconds) and disk_low (waited for free disk past disk_wait_seconds). Exhaustion is a block, never a red base or a repair: baseline_resource_exhausted (the base's failed check showed a full disk or no memory) and relais_storage_full (relais's own write met a full disk; the candidate ref and the ledger record were persisted, relais resume <run> reconciles). budget_exhausted (wall_clock) and budget_exhausted (total) exit 5 |
| 4 | the task executed and produced no acceptable candidate |
| 5 | the spending ceiling was reached; patch and evidence preserved |
| 6 | the run was interrupted; relais resume reconciles it |
| 7 | cancelled by request |
| 8 | needs a human decision; resource_exhausted is one (a candidate's failed check showed a full disk or no memory: observed, not proof the code is innocent; no repair was spent, the candidate is preserved, and relais decide <run> --answer revise --rerun-verify records a kind: verify rerun of it) |
| 9 | needs a human review |
| 10 | the run or artifact named is not on record |
| 11 | resume refused: a worker may still be running |
| 12 | resume reconciled the run; nothing was replayed |
| 13 | --write could not carry out part of its plan (the files are named) |
| 14 | nothing to train on yet |
| 15 | not enough records for one tier |
| 16 | the learner did not converge |
| 17 | recipe promote refused: the comparison does not clear every gate; nothing was written |
| 18 | trust grant refused: the key is not this policy's; relais trust show prints the current one |
| 19 | init --detect found no verification command and none was typed; nothing was written |
| 20 | learn cycle: another cycle holds the Training lease (its pid and start time are named); nothing was queued and no row was written |
| 21 | relais's own storage is full and the run could not be recorded (one stderr line names the volume); relais resume <run> reconciles from the last durable ledger state |
relais native review-outcome [--ledger <path>] [--plan-root <dir>] [--dry-run]
ingests amont-agent's plan-review outcomes ledger (default
$CLAUDE_CONFIG_DIR/amont-agent/plan-review/outcomes.jsonl, ~/.claude when
unset) and labels each reviewer clean, miss or unknown into the
review_outcomes table; relais report shows the per-class Reviewers
section and learn cycle runs the ingest at its start (a failure there is a
cycle note, never a failed cycle). It prints labelled N: clean a, miss b, unknown c; --dry-run lists one line per reviewer and writes nothing. It
exits 0 (also when there is no ledger), 1 when the ledger file cannot be read
or the relais ledger fails, 2 on a usage error, and 3 (the table above) when the ledger is a
symlink, not owned by the running user, or open to group or other. It never
writes machine.toml.
learn cycle exits 0 where it did nothing wrong and nothing at all: deferred: not idle (<n> agents active) (another session's agents are running; the cycle
stays due), not due: … under --if-due, and awaiting_promotion (an
artifact passed every gate and relais promote <id> is yours to run). An
error inside a cycle exits 1.
native cancel --run X --session S exits 0 when the run is cancelled, was
already over, or had no live driver and was marked cancelled (and also when the
driver has not ended within 10 s: cancel requested; the driver has not ended yet); 2 without --session; 3 when the coordinator cannot be reached; 10 for
a run not on record; 11 for a run with a live driver of another session
(run X belongs to live session <id>; cancel it from that session).
relais coordinator cancel --run takes the same path for a run with no bound
worker.
The session-router commands (native router-state, router-observe,
router-envelope) use 0, 1 and 2 only: 2 is a bad payload or
invocation (nothing written), 1 a failure to retry.
One crate, crates/relais, one binary. Modules follow SPEC §13:
| module | owns |
|---|---|
contract |
task contracts: validated, frozen, hashed; work plans (§4, §19) |
policy |
relais.toml, machine settings, the effective authority (§5) |
route |
deterministic routing, risk floors, the learned predictor hook (§6) |
context |
context manifests and aval resolution (§7) |
workspace |
owned worktrees, candidate snapshots, scope checks (§8) |
adapter |
the Backend trait, the Claude Code adapter, a mock (§20) |
runner |
the interpreter: observes, records, performs one effect per step (§9, §10) |
runner::machine |
the §9 observation table as a pure function, one test per row |
runner::scheduler |
bounded decomposition and integration (§19) |
verify |
verification profiles, amont gaps, receipts (§10) |
admission |
the pure admission state machine: caps, budgets, leases (§23) |
coordinator |
the per-user daemon, election, socket protocol, client (§23) |
ledger |
the SQLite ledger with additive migrations (§12) |
report, doctor, install |
reporting, diagnostics, the Claude integration |
learn |
features, dataset, learner, predict, evaluate, registry (§16, §17) |
router |
the session router's relais side: state, observations, envelope, report (§30) |
crates/relais/tests/it/release_scenarios.rs runs the §14 release scenarios
through the real binary against a fake claude; the §23 concurrency
scenarios are unit tests over the admission state machine.
Start at runner::machine::decide: it is the spec's §9 table with each
row a match arm and a unit test. runner::RunEngine::run_inner is the
interpreter around it; everything it touches — git, SQLite, processes —
is behind a Result, and a failure of the runner's own machinery ends a
run as interrupted with reason runner_failure, never as a panic.
admission::AdmissionState is the same shape for §23: every method takes
now, liveness is a callback, and the concurrency scenarios run in
milliseconds.
From Claude Code with the plugin, there is nothing to set up and nothing
to type: the model routes a bounded task through relais on its own (the
plugin's relais skill; /relais:relais invokes it by hand), and relais
picks the model for it. The first time this happens in a repository, the
plugin asks two questions in Claude Code's own dialog:
- Use these checks? It shows the commands
relais init --detectfound in the repository: a Makefilecheck/testtarget,cargo test,go test, apackage.jsontest script, pytest, with the lockfile's install as setup. On yes, it writesrelais.tomland commits that file alone. When nothing is found, you type the command. - Let relais run these commands, outside Claude Code's permission
prompts? It shows every command the grant covers. On yes, it records
the grant in
machine.toml.
Change relais.toml later and only the second question comes back, with
new or changed commands marked + or ~. Answer Not now and nothing is
written, and the model does the task without relais until you mention
relais again. The plugin asks; the model can neither word the question nor
answer it, and the plugin refuses the model's own edits to machine.toml.
By hand, the same steps are:
cd the-repo && relais init --detect --write # or plain `relais init` for the template; review, commit
relais trust show # the grant key and every command it would allow
relais trust grant --key <key> --reviewed-by <you>Every command finds relais.toml upward from the cwd to the repository
root, so a subdirectory (or a task worktree, which carries its own copy)
works; a directory outside any repository is refused by name.
Machine-owned settings live in ~/.config/relais/machine.toml and are
never written by a run. A trust grant is keyed by the PAIR of the
repository's authority hash and the repository itself (its canonical root
and, when git reports one, its origin URL), so editing relais.toml
voids the grant and the same declaration in another repository needs its
own review. relais trust grant writes it (atomically, under a lock,
keeping your comments and the file's mode); relais plan still prints the
block if you prefer to paste it. A worker is a
native agent: it runs with the tools its agent definition names and the
session's permissions, and no permission-mode flag is ever passed.
disallowed_tools here only ADDS to
the shipped deny floor (commit, merge, push, rebase, reset, tag and the
wrappers around them, plus Agent/Task: a worker never spawns subagents,
since model choice belongs to the route); it cannot shorten it:
schema_version = 1
[spending]
per_run_micros = 3000000 # $3 per run, best effort (SPEC §11)
[trust."<grant key from relais trust show>"]
granted_at = "2026-09-20T00:00:00Z" # RFC3339, or a plain 2026-09-20
reviewed_by = "you" # required
repo = "git@github.com:me/the-repo.git" # what plan filled in, for readersRuns start from Claude Code (the mcp__relais__run tool the skill calls):
relais run is refused anywhere else. A worker refused a tool ends the run
blocked (permission_denied) naming the tool; nothing is escalated. A
[permissions] allowed_tools left in machine.toml is no longer read, and
relais doctor says so.
relais keeps no OS sandbox of its own: every dispatch is a native agent of
your Claude Code session and runs under that session's sandbox and
permissions. A [sandbox] section left in machine.toml is no longer
read, and relais doctor says so. SPEC §8 has the details.
A verification worktree is a checkout of one revision and nothing else.
In a repository whose dependencies live in the tree (npm, pnpm, yarn,
bun, uv, poetry, bundler, composer), the profile's commands find no
node_modules or virtualenv there until a setup step puts one in place,
and a profile that declares none ends blocked (baseline_unrunnable)
with the block to add named. Go and Rust need no step. The setup is
declared per profile and, like the commands, is executable authority —
adding it changes the policy hash, so relais plan asks for a new
grant:
[[verification.profiles.default.setup]]
argv = ["npm", "ci"]
timeout_seconds = 600
[[verification.profiles.default.commands]]
argv = ["npm", "test"]It runs first, in every worktree the commands run in — the base, the
task worktree, each candidate's copy: a three-attempt run is five
npm ci, and cache_baseline = true drops the base's. relais doctor
and relais plan warn when a lockfile is at the root and a profile
declares no setup:
! setup package-lock.json present, and profile `default` declares no setup step: …
relais runs on a machine with none of its sibling tools. Without aval, decision keys it cannot resolve are not looked up: they are listed once as "not checked (reported, not passed)" in the receipt and the run goes on, unless [integrations] aval = "required" makes the missing tool block. Without amont, its hook and attestation checks are skipped and the missing tool is reported as a gap, not a pass. Without amont-agent, the guard it would add is absent and that is reported too. A tool that is installed but fails still blocks. relais init proposes all three as "optional", and relais doctor lists each with its mode and whether it is installed.
A programme that needs more than one session is a map, docs/programmes/<slug>/map.md,
with one decision ticket per file beside it (SPEC §31). These commands only
read; they open no ledger and no coordinator.
relais programme frontier docs/programmes/<slug> # open tickets whose blockers are closed and no prog/<slug>/<id> branch works
relais programme frontier <dir> --all --json # also the in-flight ones; JSON array
relais programme show <dir> # destination, counts, blocked tickets and their blockers, branches
relais programme check <dir> # every problem as path:line: message on stderr; exit 2 when there is one
relais programme list [--root <dir>] # every map under <root>/*/docs/programmes, with countsThe plugin can also route the model of your own session (SPEC §30): a
cheap model for an easy, checkable task, a stronger one for a hard or
uncertain one, and one tier up when a check fails after an edit or you
correct the work. relais install --claude --write turns it on when it
installs the plugin (it records the envelope in machine.toml); on a
machine where it did not, /relais-routing does. /relais-routing off
turns it off and keeps it off, and RELAIS_SESSION_ROUTING=off|shadow
narrows it for one shell. Without the envelope it runs in shadow
(decides and records, switches nothing).
relais native router-state --session <id> # what the plugin decides from (JSON)
relais report # includes "session routing": cost per completed task, routed vs held outThe plugin calls native router-observe and native router-envelope
itself; their wire contract is docs/router-protocol.md. Tuning lives in
machine.toml, every key optional:
[session_routing]
holdout_rate = 0.1 # share of sessions held out for the comparison
pinned_agents = ["my-reviewer"] # subagent types never re-routed
excluded_models = [] # full ids that do not spawn here
[session_routing.model_ids] # alias → full id, over the defaults
haiku = "claude-haiku-5-5"Upgrade relais before adding [session_routing] (or before
installing the plugin or granting the envelope, which write it): an older relais refuses machine.toml keys
it does not know, and would then refuse every command that reads the file.
docs/AUDIT-2026-09-20.md is the audit this behaviour came out of, with
the findings still open. docs/REVIEW-2026-09-21.md is the crate-wide
review v0.2.0 answers: every finding with the pull request that fixed it
or the reason it was kept, and a scorecard re-measured afterwards.
- Claude Code 2.1.x has no turn ceiling flag; attempts and wall time are enforced by the runner, turns are not.
- On Windows the coordinator's endpoint is a loopback TCP port plus a
nonce in the socket file rather than a Unix socket; the file's ACL is
the permission restriction. Worker trees are killed with
taskkill. - The decomposition scheduler executes packages sequentially in topological order; waves of independent packages are computed and recorded but not yet run concurrently.
- The local
msrvtarget proves the declared floor only when that toolchain is installed (rustup toolchain install 1.88.0); without itmake msrvFAILS and says so, rather than passing on a skip. SetMSRV_SKIP_OK=1to skip deliberately. CI always installs it. install.shandinstall.ps1refuse to install a download they cannot verify (noSHA256SUMS, no sha256 tool).RELAIS_SKIP_CHECKSUM=1is the explicit way to accept an unverified binary.
relais install --claude writes the three advisory agents
(relais-research, relais-implementation, relais-review), and installs the
relais plugin, relais@relais-local, which carries the mod, the agent
definitions (relais-worker-<model>-<effort> and the reviewer and planner ones)
and the /relais skill. The plugin is embedded in the relais binary, so a
relais installed from a release needs nothing else: install writes a directory
marketplace under relais's state directory (claude-marketplace/) and runs
claude plugin marketplace add then claude plugin install (once it is
installed, marketplace update then plugin update). The plugin carries
relais's version, so each release refreshes Claude Code's copy; relais doctor
(plugin) warns, naming relais install --claude --write, when it is absent,
disabled or at another version. relais uninstall --claude runs claude plugin uninstall and claude plugin marketplace remove and removes the directory. A
claude call that fails is reported with its stderr and the command exits
non-zero. The per-model worker agent files and the /relais skill an earlier
relais wrote under .claude/ are removed by the next install when you have not
edited them, and kept and reported when you have. relais run starts only
from Claude Code with the relais plugin, which runs it with --protocol and
RELAIS_HOST=claude-code-mod, so the worker shows as Claude Code's own agent;
from a plain terminal it is refused (exit 2), as it is on a Claude Code outside
the plugin's range (>= 2.1.291, < 2.2.0). --native is gone.
--hooks wires only the admission caps on your own Agent/Task spawns
(Agent|Task, no WorktreeCreate, no SendMessage): Claude Code makes its own
worktrees, and the plugin starts, continues and stops relais's agents. Installing
over a settings file from the hook-side native path (#171) removes relais's
SendMessage matcher and WorktreeCreate entry and keeps every entry that is not
relais's.
The relais install --claude --hooks integration — what it wires, its
handler timeouts, how to remove it, what relais doctor reports on it,
and the four limits specific to that path (subagents admitted but never
tracked as individuals, no ledger row for a coordinator restart to
adopt, no money cap on that path, and a cancellation that lasts only
until the next reconcile) — has its own document:
docs/INTEGRATIONS.md.