Skip to content
fredericrousPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

relais

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.

Install

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.

Build

make check   # toolchain lint test msrv audit
make build

CI (.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.

Exit codes

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.

Layout

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.

Reading the code

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.

Using it on a repository

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:

  1. Use these checks? It shows the commands relais init --detect found in the repository: a Makefile check/test target, cargo test, go test, a package.json test script, pytest, with the lockfile's install as setup. On yes, it writes relais.toml and commits that file alone. When nothing is found, you type the command.
  2. 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 readers

Runs 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: …

Runs alone

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.

Programme maps

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 counts

Session routing

The 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 out

The 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.

Known limits

  • 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 msrv target proves the declared floor only when that toolchain is installed (rustup toolchain install 1.88.0); without it make msrv FAILS and says so, rather than passing on a skip. Set MSRV_SKIP_OK=1 to skip deliberately. CI always installs it.
  • install.sh and install.ps1 refuse to install a download they cannot verify (no SHA256SUMS, no sha256 tool). RELAIS_SKIP_CHECKSUM=1 is 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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages