Last updated: 2026-10-15
- Status: Reference (partial — surface defined, exact flags to be finalized as the CLI is built)
- Surface: CLI (
relavium) - Scope: Phase 1, local-first. Same
@relavium/coreengine as every other surface. - Related: home.md, chat-session.md, ../vscode/extension-api.md, ../desktop/routes-and-screens.md, ../contracts/workflow-yaml-spec.md, ../contracts/agent-yaml-spec.md, ../contracts/sse-event-schema.md, ../shared-core/built-in-tools.md, ../../tutorials/cli/run-a-workflow-in-ci.md, ../../runbooks/add-a-provider-key.md
The relavium CLI is the terminal surface of the platform and the fastest way to run a workflow non-interactively (scripts, CI/CD). It embeds the same @relavium/core engine as the desktop app and VS Code extension — there is no separate "CLI engine," so behavior is identical across surfaces (see ../../architecture/shared-core-engine.md). The CLI is built second (right after the engine) and serves as the engine's canonical integration-test harness.
- Package: published to npm as
relavium, installed globally. The artifact is an engine-inlined ESM bundle — the proprietary@relavium/*engine is bundled in; every third-party dependency (including the prebuilt native addons) installs normally (ADR-0051). A global install needs no compiler toolchain. - Build: TypeScript bundled with
tsupto a single ESMbin; released via theRelease CLIworkflow (pack → cross-OS install-smoke on macOS/Linux/Windows → npm publish with provenance), see release-a-surface.md. - Stack:
commander.jsfor argument parsing,ink(React for terminals) for the interactive TUI,@clack/promptsfor setup wizards. - API keys: stored in the OS keychain via
@napi-rs/keyring(macOS Keychain / Windows Credential Manager / Linux libsecret) — never plaintext, and never the archivedkeytar(see ADR-0019 and add-a-provider-key.md). - Workflow discovery: reads workflows from the
.relavium/directory in the project root, or from an explicit path argument.
npm install -g relavium
relavium run ./workflows/code-review.relavium.yaml --input file=./src/index.tsThe CLI auto-detects its environment and switches presentation accordingly:
| Mode | When | Behavior |
|---|---|---|
| Interactive TUI | TTY attached, no --json |
ink-rendered live view: animated per-node status, streaming token output for the active node, final cost/duration summary |
| Plain | No TTY or CI=true (and no --json) |
The TUI is disabled; a terse line-per-lifecycle-event human renderer writes to stdout |
| NDJSON | --json (anywhere on the command line) |
The machine contract: stdout is a pure NDJSON stream — RunEvents for run / gate / budget resume, or SessionEvents for chat / agent run — and all diagnostics go to stderr. See The --json machine-output contract |
NDJSON is engaged only by --json (the explicit machine opt-in); a non-TTY or CI=true
environment disables the interactive TUI but does not by itself switch stdout to NDJSON
(ADR-0049). Exit codes are CI-friendly
(see Exit codes).
--no-color (and --color) do not change the mode — the interactive TUI stays active and only ANSI
color/dim are suppressed (plain output without a renderer swap). A swap to the Plain renderer happens only on
no-TTY / CI=true, and to NDJSON only on --json.
Color precedence. ANSI styling is orthogonal to the output mode above, resolved with this precedence:
an explicit --color / --no-color flag > the NO_COLOR env var (any non-empty value ⇒ off, the
no-color.org accessibility contract) > the FORCE_COLOR env var (0/false ⇒ off,
the supports-color convention) > on by default. NO_COLOR intentionally beats FORCE_COLOR — a user
who opts out of color wins over a tool/CI that opts in. (A --json/CI/no-TTY stream carries no ANSI regardless
— that is the separate output-mode selection above.)
Every record is serialized with stringifyJsonLine, not a bare JSON.stringify. The wire contract is
unchanged — JSON.parse round-trips to the identical string, which is exactly why the escape was chosen over
a strip — but what reaches a terminal differs: JSON.stringify escapes ESC and leaves DEL, the C1 block
(including U+009B, a working escape-sequence introducer) and the Trojan-Source bidi family RAW in content
the model, a tool, or an imported artifact controls. Enforced by lint rather than review; the reasoning and
the one allowlisted exception live in
security-review.md.
Under relavium run --json, the CLI emits a stable machine contract a CI job can pipe and assert
on (ADR-0049). The contract covers a workflow
run; --help, --version, and a bare no-command invocation are exit-0 meta-operations that
print their human text (usage / version) to stdout as usual, --json notwithstanding. (On a genuine
interactive TTY a bare relavium instead opens the interactive Home — see home.md for
the gate; the help + exit-0 meta-op is preserved byte-for-byte on every non-interactive path: --json,
a pipe/redirect, or CI.)
- stdout is a pure NDJSON stream of RunEvents — one event
serialized verbatim per line, in
sequenceNumberorder. The line is the stable envelope (type/runId/timestamp/sequenceNumberper the schema); there is no wrapper, stream header, or version line. Every event the run emits appears, unfiltered. - The terminal
run:completedevent is the final result line — it already carriesoutputs+totalTokensUsed+totalCostMicrocents+durationMs, so there is no separate summary line. Arun:failedterminal carrieserror+partialOutputs; failed and cancelled terminals also carry the engine's currentcumulativeCostMicrocents. The canonical envelope qualifies this known sum with usage and price coverage. These additive fields remain optional when reading historical events. - All diagnostics go to stderr, never stdout. A pre-run CLI fault (bad arguments, workflow not
found, missing key) is written to stderr as a structured
{ "type": "error", "code", "message" }envelope (distinct from the run stream'srun:failed/node:failed) and exits2; stdout stays empty. A pipe consumer reads stdout for events and uses the exit code + stderr for faults. - Secret-typed values stay masked — the engine masks them as
{ "secret": true, "ref": … }before any renderer sees them; the NDJSON carries that masked shape, never a raw secret.
Exit codes are CI-friendly (see Exit codes).
Agent-readable command surface — now realized by the command manifest. Because Relavium's own thesis is that work starts in an agent, the CLI is a natural tool surface for other agents. The two affordances once proposed here — a machine-readable help mode (
--helpemitting the command/flag surface as JSON, so an agent can discover the CLI without scraping prose) and a per-commandeffectannotation (read/write/destructive) so an agent's tool policy can gate destructive commands behind approval — are delivered by the command manifest (ADR-0056, 2.5.C). Approval enforcement of adestructiveentry is owned by ADR-0057 (2.5.E).
These flags are position-independent — they may appear anywhere on the command line, so
relavium run wf --json and relavium --json run wf are equivalent (the CLI extracts them
before parsing the subcommand).
| Flag | Effect |
|---|---|
--json |
Emit machine-readable NDJSON output (disables the TUI) — see Output modes. |
--color |
Force colored (ANSI) output on. |
--no-color |
Disable colored (ANSI) output. |
--cwd <dir> |
Run as if started in <dir> (project discovery and relative paths resolve from here). On chat resume this cannot replace the original workspace. |
--config <path> |
Use an explicit global config file instead of ~/.relavium/config.toml — the project .relavium/ layers still apply (config-spec.md). Chat resume uses this current global override while discovering current project layers at its original workspace. |
--allow-fs-root <absolute-directory> |
Repeatable invocation-only filesystem grant for bare Home, chat, chat-resume and agent run; at most 16 arguments, at most 4096 characters each, naming existing accessible directories. Other commands, including workflow run, refuse it (exit 2). Project/workspace selectors can narrow these grants. |
--no-alt-screen |
Keep the byte-identical inline renderer for the bare Home + relavium chat (no full-screen alternate screen) — the screen-reader fallback. Overrides [preferences].alt_screen; a non-TTY / --json / CI path is always inline regardless (ADR-0068). |
--no-mouse |
Disable terminal mouse reporting (DECSET 1002+1006) inside the full-screen renderer. The wheel stops scrolling the transcript (PgUp/PgDn + Ctrl+Home/Ctrl+End still page), in-app selection and copy-on-select go with it, and the emulator's own click-drag selection works again without a modifier. Overrides [preferences].mouse, and forces [preferences].copy_on_select off. Ignored outside the alt screen (ADR-0068 §e, accessibility.md). |
-v, --verbose |
Print verbose diagnostics to stderr. |
-q, --quiet |
Suppress non-essential output. (--verbose and --quiet cannot be combined → exit 2.) |
-V, --version |
Print the version and exit 0. |
-h, --help |
Print help for the program or a subcommand and exit 0. |
Filesystem grants undergo native admission before dispatcher history/provider/key/MCP composition. Invalid or unavailable roots refuse with value-free diagnostics. No environment/config/session/model output grants a root. The flag works without selector config; fresh resumes require fresh grants under the session authority contract.
The command set below is the confirmed surface. Commands ship per workstream: run (2.D), gate + gate list (2.G/2.I), provider (2.C), the read commands list / logs / status (2.I), the whole agent-first chat family — chat (2.M), chat-resume (2.N), chat-list (2.O), chat-export (2.P), and chat --json + agent run (2.Q) — and the YAML-lifecycle authoring commands create / import / export (2.J) are all live; budget resume is live under W7 (ADR-0097). Invoking a not-yet-shipped command exits with a clean "not available yet (lands in …)" message. Subcommands marked (planned) are intended but not yet locked.
| Command | Purpose |
|---|---|
relavium run <workflow> [--input k=v] [--secret-inputs-stdin] [--allow-mcp-stdio <digest>] |
Execute a workflow. Streams progress; resolves with the workflow output. |
relavium chat [--agent <ref>] |
Start an interactive agent session (the agent-first REPL). See chat-session.md. |
relavium chat-resume <sessionId> |
Reload a persisted session from history.db and continue the conversation. |
relavium chat-list |
List past agent sessions (id, agent, last activity), the way relavium list lists workflows. |
relavium chat-export <sessionId> |
Export a session to a .relavium.yaml scaffold for review (ADR-0026). |
relavium agent run <agent> [--fixture <path>] [--json] [--allow-mcp-stdio <digest>] |
Run a single agent one-shot (non-interactive) on the same AgentSession infra — the prompt is read from stdin, one turn, then exit. See relavium agent run and agent-run-fixture.md. |
relavium list |
List discovered workflows (and, with a flag, agents) in the current project. |
relavium create [--force] |
Scaffold a new agent or a minimal single-agent workflow YAML via an interactive wizard (schema-validated before write). |
relavium import <path> [--force] |
Import an external .relavium.yaml / .agent.yaml into the project, validated + slug-deduplicated. |
relavium export <id> [--out <path>] [--force] |
Export a workflow/agent to a portable, canonical YAML copy. Serialization drops comments and preserves authored values, including sensitive literals; review before sharing. Configured provider keys use keychain references; author MCP env credentials as {{secrets.*}}. |
relavium logs <runId> |
Print the persisted event/log stream for a past run. |
relavium status |
Show active runs and their per-node status. |
relavium gate <runId> |
Resolve a pending human gate (approve / reject / provide input). |
relavium gate list [<runId>] |
List pending human gates (all active runs, or one run) — the multi-gate subcommand for resolving one of several concurrently-pending gates. |
relavium budget resume <runId> [--gate <gateId>] --approve-amount <microcents> | --abort |
Resolve a recorded budget gate with its exact frozen amount, or reject it. See budget resume. |
relavium init (planned) |
Initialize a .relavium/ directory in the current project. |
relavium agent <subcommand> (planned) |
Manage agents (list / create / test). |
relavium models |
List the cached model catalog (refreshes on first run if the cache is empty). See relavium models. |
relavium models refresh |
Re-fetch both axes: each connected provider's live model list (availability) and the models.dev catalog (price, ceilings, reasoning tiers). Reports per-source outcomes. |
relavium models refresh --providers |
Availability only — the provider lists. ADR-0064's original behaviour. |
relavium models refresh --catalog |
Metadata only — the models.dev catalog. Needs no provider key. |
relavium models pricing <model> |
Hand-enter a user price for a model the registry does not know, so the cost cap enforces it. See relavium models. |
relavium provider <subcommand> |
Manage providers and API keys in the OS keychain (list / add / set-key / remove-key / test). |
Canonical home for the command manifest (ADR-0056, 2.5.C). The runtime form lives in
apps/cli/src/commands/manifest.ts(a CLI-only contract — no other surface consumes a CLI command list).
The command manifest is the one source the shell command surfaces derive from — the commander parser, the executeCommand dispatch table (apps/cli/src/commands/dispatch.ts), and relavium --help --json — so they can never diverge. (The in-REPL / palette + slash commands are a separate, curated registry — see In-REPL slash commands below.) The set is deliberately small and alias-free; every entry is canonical by construction (there is no per-entry alias flag). Each entry is:
{
id // stable id; a subcommand is dotted — `provider.set-key`, `agent.run`, `gate.list`
label // a short human label — "Run workflow", "Set provider key"
description // the one-line help; MUST match commander's .description() (the --help --json text)
args? // [{ name, type: 'string'|'number'|'boolean', required?, description? }]; name is the camelCase CommandInput key
effect // 'read' | 'write' | 'destructive' (see below)
modeScope? // chat modes a command is available in; omit ⇒ all modes
}
effectis a forward-looking annotation:readnever mutates,writecreates/modifies,destructiveirreversibly removes (today onlyprovider.remove-key). It is marked for agent discoverability now; approval enforcement of adestructiveentry is owned by ADR-0057 (workstream 2.5.E), not here.modeScopelists the chat modes a command appears in (omit ⇒ all). The mode values (ask/plan/accept-edits/auto) are defined in ADR-0057 (2.5.E); 2.5.C ships the field withomit = all.- A
manifest ↔ commanderdrift guard (a unit test) asserts every realcommandercommand has an entry with the same description (command + each option), socommander, theexecuteCommandtable, and--help --jsonstay byte-consistent.
| Example entry | effect |
|---|---|
run |
write |
list |
read |
provider.set-key |
write |
provider.remove-key |
destructive |
The interactive / palette + slash commands inside the Home and chat are a SEPARATE, curated surface (ADR-0056 amendment, 2.5.C) — the runtime registry is apps/cli/src/commands/repl-commands.ts (REPL_COMMANDS), the single source for the palette, the /help list, and the unknown-slash hint. It surfaces only the commands that make sense in a live REPL: lifecycle (/exit, /cancel, /export, /clear), info/discovery (/help, /workflows, /cost, /doctor), and — in a chat — /mode, /effort, /thinking (also Ctrl+T), the ADR-0062 context commands (/compact, /trim), /models, and the ADR-0068 §e copy-and-search hatches /scrollback, /edit + /copy. Their in-chat behavior is spec'd in chat-session.md and its ADRs — the /models live-reseat vs bare-Home write-default and its reasoning-effort sub-step + picker UX (ADR-0059 reseat, ADR-0064/ADR-0063 catalog+write), /effort's per-turn override (ADR-0066), and /thinking's panel toggle (2.5.H) — this reference does not restate them. The heavy, session-starting shell commands (run, chat, provider, …) are never in-REPL slashes — they stay shell-only (relavium <cmd> …). A bare / at an empty prompt opens the filterable palette (the footer hint-bar surfaces / for commands there, 2.5.C S6); an unknown slash — or an undeclared argument on a known command (/exit now) — prints a sanitized, secret-free hint. A command may declare flags (/doctor --deep) or a single positional value (/mode plan); the palette runs the bare form, so a flag/value is opt-in by typing it. There is no separate /shortcuts command — the palette's own nav hints (↑/↓ · Enter · Esc) + the footer keep keys discoverable in context.
Runs a workflow end-to-end. The argument is a path to a .relavium.yaml file (or a workflow id/slug resolvable inside .relavium/).
# interactive
relavium run ./workflows/code-review.relavium.yaml --input file=./src/index.ts
# CI: machine-readable event stream
relavium run ./workflows/code-review.relavium.yaml --input file=./src/index.ts --json
# A workflow with a declared secret input: supply it through a non-TTY pipe
printf 'api_key=%s\n' "$API_KEY" | relavium run ./workflows/code-review.relavium.yaml --secret-inputs-stdin--input k=v(repeatable, one distinct key per input) supplies ordinary typed workflow inputs (see workflow inputs). Values are coerced to their declared types. An unknown or repeated key, a missing required input, or an invalid value is an invocation fault (exit2). Every explicit--inputfor a declaredsecretis refused, includingname=: argv can appear in process lists, shell history and CI logs. Raw malformed-input and run-parser diagnostics do not echo pasted argument values.--secret-inputs-stdinreads declaredsecretinputs once from non-TTY stdin; TTY stdin is refused even if stdout is redirected. Supply onename=valueline per secret. Blank lines are ignored, the name is trimmed, the value after the first=is preserved verbatim (including spaces and further=), and a carriage return at the end of a line is removed. Repeated names after trimming, malformed lines, unknown or non-secret names, and empty or whitespace-only values are refused with payload-free exit-2diagnostics. Names such as__proto__,constructorandtoStringwork only when declared; accumulation uses own properties in a null-prototype map. Required secrets must be supplied; omitted optional secrets remain absent. The common reader limit applies before line parsing. A workflow with no declared secrets refuses the flag without reading stdin.- Run admission rejects forbidden ordinary secret arguments before reading the secret pipe, constructing credential/provider resolvers, connecting MCP or opening run history. The CLI then coerces ordinary values, merges stdin secrets, and runs the pure core input contract (including length, format, pattern, enum and numeric bounds) before those effects. A rejected invocation creates no run. After unknown CLI keys are refused, core contract refusals identify a bounded set of already-declared inputs with sanitized labels and value-free reasons. Unknown, duplicate and malformed ordinary argument diagnostics keep raw argv names and values opaque. The engine validates again at start; admitted secret input slots and input-node results are masked in run events/history, while the authored workflow snapshot is kept faithful. This does not redact arbitrary authored literals, tool output or other content.
- Consuming the secret pipe removes interactive prompt authority for this invocation. A later unresolved human gate pauses with exit
3for a freshrelavium gatecall; an unapproved local MCP program refuses instead of prompting. An existing exact consent grant or explicit--allow-mcp-stdio <digest>remains the authorization path. The flag itself grants no tool or transport authority. --jsonswitches to NDJSON RunEvent output.Ctrl-C(SIGINT) requests a cooperative cancel; the run drains torun:cancelledand exits non-zero (1). This is unchanged when the workflow declaresmcp_servers: the spawned servers are torn down alongside the drain, and the MCP teardown deliberately does not take the exit code — a signalled run still records its terminal and releases its lease (ADR-0088 §1.3).- A missing API key for an inline agent's primary provider is caught pre-flight as an invalid invocation (exit
2) naming theRELAVIUM_<PROVIDER>_API_KEYto set, before the run starts. The pre-flight is a strict subset of the keys a run may touch, so it never blocks a valid run: afallback_chainprovider's key (read only if the chain fails over to it) and a$ref-resolved external agent's key (until$refresolution lands, 2.M–2.Q) are conditional and instead surface mid-run as a run failure (exit1). --allow-mcp-stdio <digest>(repeatable) authorizes a local MCP program for this invocation only, when there is no one to prompt. A workflow declaring astdioMCP server that has not been approved on this machine exits2before anything spawns — see Local MCP servers need consent.- On a
human_gatenode the run pauses: in interactive mode it prompts inline; in CI mode it exits with the gate-paused code (3, see Exit codes) and can be resumed withrelavium gate. The emittedhuman_gate:pausedevent carries therunId+gateIdneeded for the resume (relavium gate <runId> --gate <gateId>); with--jsonthey are on the NDJSON event line, otherwise the plain/TUI renderer prints them inline (paused at gate <gateId> (<type>), also echoed in the final summary). (relavium status,relavium logs <runId>, andrelavium gate listalso surface pendinggateIds, 2.I.)
Implementation status (as of workstream 2.G).
runis wired to the@relavium/coreengine: path/id resolution,--inputcoercion, the full lifecycle event stream, exit codes0/1/2/3, SIGINT→cancel, and the stable--jsonNDJSON machine contract (stdout = pure RunEvent stream, diagnostics → stderr; see above) are live. The interactiveinkTUI (2.E) renders the live run on a TTY — per-node status + spinners, the active node's streaming tokens, a running cost/duration footer, and a persistent final summary. Under--no-colorit keeps the TUI but suppresses ANSI color; it falls back to the plain line renderer when no TTY is attached orCI=true, and to NDJSON under--json(the three renderers are oneonEventseam over one bus). Provider keys resolve from the OS keychain →RELAVIUM_<PROVIDER>_API_KEYenv var → error (2.C; manage them withrelavium provider), and runs persist to durable history (2.H). The interactive human-gate prompt + out-of-bandrelavium gateresume are live (2.G): on a TTY ahuman_gatenode renders a@clack/promptscard inline (approve / reject + comment / input) and the run continues; under--json/CI/no-TTY there is no prompt and the run exits3, resumable later byrelavium gate <runId>. The CLI supplies reviewed filesystem, process and egress host capabilities; tool availability remains subject to invocation authority, per-tool policy and wired delegates. The built-in tool reference owns their current host behavior and security floors.
Lists the workflows discovered under the project .relavium/workflows/, grouped by tag, each annotated with its last-run status from durable history (the latest run per workflow — a SQLite ROW_NUMBER() OVER (PARTITION BY workflow_id …) pick, since SQLite has no DISTINCT ON). --agents lists the agents under .relavium/agents/ instead (agents carry no tags or run history, so they list flat). Disk is the catalog source of truth, distinct from run history — a discovered file that fails to parse is listed and flagged (invalid: <reason>) rather than hidden. Outside a .relavium/ project this is reported clearly and exits 0 (an empty catalog is not a fault).
Under --json, each entry is one NDJSON record — { kind, slug, name, tags, path, valid, error?, lastRun }, where name is null when the file declares none, error is a short, secret-free parse-failure reason present only when valid is false, and lastRun is { runId, status, completedAt } or null for a never-run workflow (and is omitted entirely for agents). See Read-command --json output.
Interactive scaffolder (@clack/prompts) that asks kind (an agent → .agent.yaml, or a minimal single-agent workflow → .relavium.yaml, i.e. an input → agent → output scaffold wrapping one inline agent), then name → provider → model → system prompt → tools (comma-separated, optional). The id is the slugified name; the file lands at .relavium/agents/<id>.agent.yaml or .relavium/workflows/<id>.relavium.yaml.
The answers are assembled into a typed definition and validated against the appropriate @relavium/shared schema before any write — the agent schema for an .agent.yaml, the workflow schema for a .relavium.yaml — so a bad model/provider/name is the same clean exit-2 fault a run would raise (a name with no usable id characters is rejected up front). The id must be unique across both catalogs (see import/export below): a same-kind clash needs --force, a cross-kind clash is always rejected. The wizard needs an interactive terminal on both ends (a TTY stdout to draw the prompt and a TTY stdin to read keys), so it fails loud (exit 2) under --json or a non-TTY pipe; a cancel (Ctrl-C / ESC) writes nothing and exits 0. The result is a plain YAML file ready to commit — pure file I/O, no keychain, no run state.
Both are surface-agnostic git-native YAML operations — they read/write .relavium/ files only, never the keychain or run state. A document's kind is detected from its filename suffix (.agent.yaml / .relavium.yaml), falling back to a content sniff; a file that is neither a valid workflow nor a valid agent is a clean exit-2 fault naming both parse failures.
import <path> [--force]copies an external.relavium.yaml/.agent.yamlinto the project after validating it against the schema. It writes the re-serialized canonical form to.relavium/<workflows|agents>/<slug>.<suffix>. Ids are unique across both catalogs (a bare id resolves project-globally, soexport <id>stays unambiguous): a same-kind slug collision is exit2unless--forceoverwrites it; a cross-kind collision (the id already names the other kind) is exit2always — rename one, since--forcewould leave both files in place.export <id> [--out <path>] [--force]resolves<id>across both catalogs (an id naming neither is exit2; an id that is both a workflow and an agent is exit2, "rename one"), re-validates it, and writes a portable copy. The default destination is./<id>.<suffix>in the cwd (a copy to share, outside the catalog);--outoverrides it. Under--jsonit emits a single{ id, kind, path }record, wherepathis cwd-relative (the same shapeimport --jsonemits — no absolute filesystem path is printed in either mode).
Review before sharing. Relavium does not insert a resolved provider key into authored YAML: its provider-key configuration uses OS-keychain references. Arbitrary authored strings, including prompts, metadata and MCP env, can still contain a pasted credential or other sensitive content. export/import re-serialize faithfully and drop comments; they preserve authored values and do not scrub them. Author MCP credentials as {{secrets.*}} placeholders, never inline a literal. See key custody and content retention.
create, import and run apply a bounded, best-effort review of the parsed authored definition using the existing finite diagnostic secret-shape detector. A suspicious shape produces a value-free notice on stderr; an incomplete scan is also disclosed. These notices neither reject nor redact the document and add no JSON stdout record. The original parsed definition and the run's admitted augmented snapshot remain faithful. Absence of a notice does not certify safe sharing or absence of credentials. Supplied runtime secret-input values are outside this authored-content scan; their admission and recorded-slot masking follow the input contract.
create and import emit these notices only after their YAML file is written. A refused write emits no authored-content notice or success output.
The review visits at most 4,096 nodes through depth 64, checks strings of at most 16,384 UTF-16 code units and at most 262,144 checked code units in total, including property names. It reports only a count of strings matching the existing finite detector (prefixed OpenAI/Anthropic or Google keys, URL userinfo, Bearer/Basic credentials and literal secret query names). It does not classify every credential format. Over-budget strings or structure, repeated objects and unsupported object shapes produce a separate fixed incomplete-review notice. Object-key enumeration relies on the existing parser source ceiling; the node limit is not an independent bound on that enumeration. Neither notice includes values or arbitrary property names.
For this detector probe only, a recognised bare template reference without a path or filter is projected to an already masked span, provided its raw reference contains no recognised credential shape. This keeps Bearer {{secrets.token}} and equivalent query placeholders quiet. Adjacent literals, credential-shaped identifiers, paths, filter arguments and unknown or malformed references retain their evidence. This projection neither resolves references nor changes authored content.
Replays a past run's persisted run_events in seq order (the same data the desktop run-detail drawer replays) — a terse line per event in human mode. Under --json it emits each raw RunEvent as one NDJSON line — the same RunEvent data relavium run --json streamed (this is the "raw RunEvent JSON" the run-detail replay consumes — no separate --raw flag). For a run paused at a gate, the human_gate:paused event surfaces the gateId to copy into relavium gate <runId> --gate <gateId>. An unknown runId is an invalid invocation (exit 2).
Raw historical events preserve absent fields: the reader does not synthesize usageCoverage,
cumulativeUnpricedCalls or invocation correlation; absence cannot certify complete or free coverage.
Two rows the replay may not be able to read, and it says which is which (ADR-0074 §5):
- a row written by a newer Relavium — an event
typethis binary does not know — is skipped, and a boundednote:naming the affectedseqnumbers goes to stderr (in both modes, so--json's stdout stays a pureRunEventstream). Without the note the event count and the[seq]column would present a short log with a gap as a complete one, which is exactly what the event contract tells consumers to read as data loss. Upgrade to read them. - a damaged row — unreadable JSON, or a
typethis binary knows whose body does not parse — fails the command (exit1), naming the run and the row'sseq. It is never silently dropped. Other runs stay listable:logs/status/gate listisolate a damaged run so one bad row cannot blank the whole listing.
Shows the currently active/paused runs (from runs + step_executions) and each one's per-node status. Pending human and budget gates are reconstructed from the strict ordered event log even when only the authoritative budget pause survived a crash; discovery never claims ownership or supplies a decision. Useful while a long workflow runs in another terminal or was launched detached. For any run paused at a human gate it also prints the pending gateId(s) (with gate type and node id), so a CI author can pass the right one to relavium gate <runId> --gate <gateId> — required when a run has more than one gate pending at once. It takes no argument (it lists every active run; a terminal run is not shown — inspect one with relavium logs <runId>). A run whose event log is damaged is still listed, without its gate detail, rather than aborting the listing (ADR-0074 §5). A run whose TERMINAL is held in the terminal outbox is named as such — it reads running in the derived projection because its terminal never became durable, so without the marker it is indistinguishable from a run still working (exit code 5). The listing READS the outbox and never drains it: draining claims a run lease, and a status read must not take ownership of a run another process may be finishing. Under --json each active run is one NDJSON record — { runId, workflowId, status, totalCostMicrocents, usageCoverage, unpricedCalls, terminalHeld, startedAt, steps, pendingGates, pendingBudgetGates }, where each steps entry is { nodeId, nodeType, status, attemptNumber, startedAt, completedAt, durationMs, costMicrocents, usageCoverage } and each pendingGates entry is { gateId, nodeId, gateType, message, expiresAt? } (the same pending-gate shape gate list emits). Each pendingBudgetGates entry is { gateId, nodeId, allowance, expiresAt? }: allowance is { kind: "amount", microcents }, { kind: "legacy" }, { kind: "reject_only", reason: "unpriced" } or { kind: "reject_only", reason: "unrepresentable" }. Each allowance variant may also carry excludedEntries?: string[]: recorded excluded-candidate notices with redacted, quoted, truncated and escaped model identifiers and fixed exclusion reasons. These display strings grant no allowance. The JSON projection exposes no full quote, priced-model identity, endpoint or price provenance. Plain output adds the exact approval/abort commands for an amount-bearing gate. Terminal controls and secret-shaped identifiers are redacted and bounded in the budget display; a modified or shell-active identifier gets generic instructions rather than an executable hint. A damaged log emits empty gate arrays plus gatesUnavailable: true and gatesUnavailableReason: "corrupt_event_log", so absence of readable authority is distinguishable from no pending gate. Run amounts and per-step amounts/lifecycle are qualified by usage coverage from one SQLite snapshot. A concurrent completion cannot attach an older complete-usage fold to a newer step amount. If strict usage replay fails, the record carries usageUnavailable: true, usageUnavailableReason: "unreadable_event_log" and explicitly unverified run/step coverage; independently readable step detail remains visible.
The live model catalog (2.5.G, ADR-0064). The catalog is a local cache in history.db (model_catalog) that records which model ids each connected provider key can currently reach; the static registry (pricing.ts) stays the pricing authority, so the cache holds no price and no API key.
relavium models # list the cached catalog
relavium models refresh # force a live re-fetch of every connected provider
relavium models pricing my-custom-model --provider openai --input 3 --output 9 # hand-enter a price-
relavium models(no subcommand) lists the cached catalog (read-only). On the very first run — when the cache is empty — it does one minimal blocking refresh, then lists; an empty result stays a clean exit0(an empty catalog is not a fault, likerelavium list). Human output is one line per model (<modelId> <provider> ctx=<n> [<source>]). -
relavium models refreshre-fetches two axes, because "refresh what I know about models" is one intent, not two (ADR-0071 §4a): the provider lists say which models a key can reach, and the catalog says what they cost and which reasoning tiers they take.--providersand--catalogaddress one axis each; neither flag (or both) means both.The provider half is per-provider isolated: one provider's failure (bad key, network, endpoint drift) or a provider without a list endpoint never fails the whole command — that provider is reported
failed/skippedand the others still refresh. A per-provider failure is therefore not a command failure (exit0with the report). An explicit--providersrefresh with zero providers connected (no key at all) is a clean exit2naming how to add a key, because nothing could be fetched.The catalog half needs no key at all — which is what makes
--catalogthe form a user with no provider configured yet can still run to see what things cost. A failed catalog fetch is a note on stderr, never an error: the shipped snapshot still answers, so a refresh can never leave Relavium knowing less than it shipped knowing. Under--json, each source emits its own record ({ provider, … }per provider, then{ source: 'catalog', status, models, added }). -
The catalog is EMBEDDED and answers offline. Relavium ships a generated models.dev snapshot — every price, ceiling and reasoning control — so a fresh install with no network prices every model it ships.
[catalog] auto_refresh(defaultfalse) governs whether a barerelavium modelsrefreshes it first; an explicitmodels refreshfetches regardless, because a command the user typed is consent. A local-first tool does not contact a third party unless asked. -
relavium models pricing <model> --provider <slug> [--input <usd> --output <usd>] [--cached <usd>] [--image <usd-per-image>] [--audio <usd-per-second>] [--video <usd-per-second>]hand-enters the price of a model. It MAY be a model the generated catalog (packages/llm/src/catalog/) already prices — the USER outranks the catalog (ADR-0071 §5): a negotiated rate, an enterprise discount, or a price the snapshot has not caught up with wins over the shipped one (2.5.G S10, ADR-0065 §1–2). Token prices are USD per million tokens (--inputprompt,--outputcompletion,--cachedcache-read); media prices are per BILLED UNIT —--imageper image,--audioand--videoper second (ADR-0044 §3's canonical units; ADR-0089 §4). All are stored as integer micro-cents (usd × 1e8, never a float), and the two denominators carry separate implausibility ceilings — $1000 per image is already absurd where $1000/Mtok is merely expensive.--input/--outputare given together or not at all, and a media-only invocation is legitimate: it is how you satisfy astrict_cost_caprefusal on a model the catalog already prices for tokens, without writing a user row that would then outrank the catalog for tokens too. An omitted flag leaves its column alone (the store preserves it); a stated0is believed and recorded as free, which for the media columns is distinct from unpriced. The row is written assource='user'and a livemodels refreshnever clobbers it. Overriding a catalog price is not silent: the command echoes the catalog price it replaces, so the divergence is stated at set-time (ADR-0071 §5). This also closes the cost-cap gap (ADR-0064 §6) for a model the catalog does NOT price: before, an unknown model had no price, sobudget.max_cost_microcents/[chat].max_cost_microcentsdegraded to allow for it; once user-priced, the cap is enforced (pre-egress and realized) onrun, arunresumed viarelavium gate,chat/chat-resume(incl. a/clearrebuild, re-read fresh), the Home chat, and one-shotagent run.--clearretires the override; the model falls back to the catalog price, or to unpriced if the catalog has none. Guards (each a clean exit2, nothing written): an unregistered provider is refused (register it first withrelavium provider add); a negative / non-finite / implausibly-large price is refused (per-unit and per-Mtok have their own ceilings); an invocation with no price at all, or a half-stated--input/--outputpair, is refused;--clearbeside any price flag — media included — is refused rather than half-applied; a price under a provider the catalog does not anchor the model to is refused (it would never apply); and the same model id already user-priced under a different provider is refused (the overlay keys by model id, so a second provider's price could not be distinguished — use a distinct id or re-price under that provider). -
Security. A provider key is read only to make the live request (over the bounded, abortable, secret-free
listModelsseam) and is never logged, persisted (the cache holds no key), or placed in the report /--jsonpayload / any error message. A failing provider surfaces only the seam's already-redacted message (or a genericrefresh failed), never a raw cause.models pricingwrites only a model id + provider + integer prices — no key, ever. -
--json(ADR-0049) emits one NDJSON record per line, stdout-pure, key-free:relavium models --json— one record per model:{ provider, modelId, displayName, contextWindowTokens, maxOutputTokens, source, lastRefreshedAt, deprecationDate }(nullfor an absent optional;source∈static | live | user;lastRefreshedAtis epoch-ms).relavium models refresh --json— the two axes each emit their own record (§4a). A per-provider record:{ provider, status, added, updated, deactivated, error }, wherestatus∈refreshed | skipped-no-key | skipped-unsupported | failed, the three counts are the model ids added / refreshed-in-place / soft-deactivated (nullunlessstatusisrefreshed), anderroris a short, secret-free reason (nullunlessfailed). And, when the catalog axis ran, exactly one catalog record:{ source, status, models, added, reason? }—sourceis always the literal'catalog';status∈refreshed | failed;modelsis the total the refreshed catalog describes andaddedthe count the shipped snapshot did not carry (both0on a failure — the snapshot still answers);reasonis present only onfailedand is a short, secret-free sentence (never a raw fetch error, which can carry a URL).relavium models pricing --json— one record:{ model, provider, source, inputCostPerMtokMicrocents, outputCostPerMtokMicrocents, cachedInputCostPerMtokMicrocents, mediaImageCostMicrocents, mediaAudioCostMicrocents, mediaVideoCostMicrocents, overriddenCatalogPrice }(the stored integer micro-cents;sourceis alwaysuser). A field is omitted when that flag was not given — the columns were preserved, not written, and reporting a0would claim a price nobody set.overriddenCatalogPriceisnullwhen the catalog does not price the model.
The non-streaming read commands (list / status / gate list / chat-list / models / provider list, and logs) keep the CLI to one machine-output idiom: --json emits one result record per line (NDJSON, jq-friendly, stdout-pure with diagnostics on stderr) — the same line-oriented shape relavium run --json uses for its RunEvent stream (ADR-0049). For logs --json the records ARE raw RunEvents — the same RunEvent data the run streamed (re-serialized from the persisted log, so the field order may differ from the live run --json bytes); for the others they are the per-command result records documented above. An unknown runId (logs / gate list) is the structured pre-run fault on stderr with exit 2, stdout empty — exactly as for run. (chat-export --json is not a read command — it emits a single session:exported event, not a result record, since the export is a session-lifecycle action.)
Resolves a pending human gate from the terminal — the surface-agnostic resume path for human_gate:paused:
relavium gate <runId> --approve
relavium gate <runId> --reject --comment "Too risky"
relavium gate <runId> --input '{"region": "us-east-1"}' # for gate_type=input
relavium gate <runId> --gate <gateId> --approve # disambiguate when >1 gate is pending
# a run with `secret`-typed inputs: re-supply them on stdin, never in argv
printf 'api_key=%s\n' "$API_KEY" | relavium gate <runId> --approve --secret-stdin- Exactly one of
--approve/--reject/--inputis required and they are mutually exclusive;--comment <text>annotates an approve/reject rationale and is invalid with--input(which carries the payload). A bad combination is an invalid invocation (exit2). --input <value>is parsed as JSON when it parses ('{"k":1}'/'42'/'true'→ a structured payload), else kept as the raw string (--input some-token→"some-token"); the result becomes the gate node's output. (The interactive prompt for agate_type=inputgate takes the typed value as a raw string.)- Do not pass secrets via
--input. The value reaches the durable event log (human_gate:resumed.payload) and the--jsonstream, and argv itself leaks intops/ shell history / CI logs — exactly the exposurerelavium provider set-key's stdin-only rule avoids. Use a non-secret gate input. Re-supply declared workflow secret inputs with--secret-stdin; provider API keys have their separate OS-keychain /RELAVIUM_<PROVIDER>_API_KEYresolution path. --gate <gateId>selects which pending gate to resolve. The resume contract isengine.resume(runId, gateId, decision)—gateIdis mandatory on the resume path (it is carried on thehuman_gate:pausedevent; see sse-event-schema.md andresume_runin ipc-contract.md).--gateis optional on the CLI: when exactly one gate is pending the CLI fills it in automatically; when more than one gate is pending it is required, and omitting it is an invalid invocation (exit2) listing the pendinggateIds.- Read the pending
runId+gateIdfrom the run's own output: thehuman_gate:pausedevent line under--json, or thepaused at gate <gateId> (<type>)line the plain/TUI renderer prints.relavium gate list,relavium status, andrelavium logs <runId>(2.I) also surface them out-of-band. --secret-stdinre-supplies the run'ssecret-typed inputs (ADR-0083 §6). The raw value is not persisted in the declared input slot — the durable record holds only a masked{ secret: true, ref }marker there — so a secret-bearing run cannot resume without the value being handed back. Each is onename=valueline on stdin; blank lines are ignored, and anything else is an invalid invocation (exit2). The shared reader limit applies.- Values never travel through argv, which is why this is a boolean flag and not
--secret <name=value>: a credential on a command line leaks tops, shell history and CI logs — the same rulerelavium provider set-keyfollows. The name in the line is not a secret, and carrying it there rather than in a repeated flag removes the ordering dependency that would silently swap two credentials. - Every masked slot must be supplied and nothing else may be. A missing slot, a name the run has no slot for, a repeated name, and an empty or all-whitespace value are each exit
2. A value may contain=(the split is on the first one); it may not contain a newline. The name is trimmed; the value is taken verbatim after the first=, so leading and trailing spaces are part of the credential — only a trailing line carriage return is removed. (An earlier version trimmed the whole line, which silently stripped a trailing space from a pasted key and surfaced hours later as an opaque401.) --secret-stdinmakes the invocation non-interactive. Reading the credential drains stdin, so a resumed run that reaches a second human gate cannot prompt: it exits3and is resolvable by anotherrelavium gate, exactly as a--jsonor CI resume does.- Without the flag, a secret-bearing resume is refused (exit
2) and the message names the slots and this remedy. With the flag on a run that has no secrets, the command refuses rather than blocking on a pipe nobody attached. - What this proves is the slot, not the credential: the engine verifies that the same named
secretinput was re-supplied and cannot tell whether the value is the same key or a rotated one. ADR-0083 §6 states that limit rather than implying more.
- Values never travel through argv, which is why this is a boolean flag and not
- Idempotent. A doubled decision — the run already finished, or the named gate was already resolved — is a clean exit-
0no-op, never a double-advance (it leans on the engine's checkpoint/gate-state idempotency). An unknownrunIdis exit2. Idempotency is per gate, though: on a sequential multi-gate workflow a blind repeat without--gate(after the first decision advanced the run and it re-paused at the next gate) auto-fills and resolves that gate — so an automated retry-until-exit-0loop should pin--gate <gateId>to avoid resolving later gates unattended.
Implementation status (2.G).
relavium gateruns in a fresh process from the originalrun: it reloads the run's frozenWorkflowDefinition+ inputs from the durable history snapshot (2.H), reconstructs the paused checkpoint from the persisted event log, and callsengine.resumeFromCheckpointover the same store — then drives the resumed run to its terminal (exit0complete /1failed /3paused again at a later gate). The recordeddecidedByis the constantcli(a deterministic, non-PII marker; the desktop/portal supply a real user id). Budget-cap pauses (budget:paused, ADR-0028) are not resolved here — that is the separaterelavium budget resumesurface. A run that declares asecret-typed input is resumed by re-supplying it on stdin with--secret-stdin(above); without the flagrelavium gatestill fails closed (exit2), because secrets are never persisted in plaintext (only a masked placeholder is, ADR-0006/0036) and there is nothing to restore. The engine enforces the same rule independently — it refuses a resume whose masked slot was not filled, and refuses the placeholder itself as a value. Therelavium gate listmulti-gate listing is live (2.I).
Resolve a budget gate from another process or a non-interactive shell. The engine validates and activates the recorded allowance; the CLI transports the decision without recomputing the quote.
relavium budget resume <runId> [--gate <gateId>] --approve-amount <microcents>
relavium budget resume <runId> [--gate <gateId>] --abort
- Exactly one decision flag is required.
--approve-amountaccepts decimal digits representing a non-negative safe integer in microcents, including0; signs, fractions, exponents and overflow are invalid invocations (exit2). It must equal the frozen amount exactly. A wrong amount is refused before secret input, key resolver construction, MCP connection, engine construction or ownership. - With one pending budget gate,
--gateis filled automatically. With multiple budget gates it is required; ordinary human gates are separate. Pin the gate id in an automated retry so a later gate is never selected accidentally. Humanrelavium statusand ambiguous resume discovery report budget ids, scalar allowance amounts and the recorded excluded candidates. Candidate model identifiers are redacted, quoted, truncated and escaped; they cannot supply an approval amount. Priced model/rate provenance stays out of this display.relavium gate listandrelavium gatecontinue to cover ordinary gates only. - A legacy gate with no frozen allowance, an unpriced quote or an unrepresentable amount offers
--abortonly on this surface. There is no input payload, comment or generic--approveflag. - Approval preserves the governor and its dispatch-owned allowance. It does not lift the workflow cap.
The CLI first checks recorded priced quantities against current prices without seeding database
rows, before secret input, credential-resolver construction or MCP connection. That check grants
no approval. The engine independently prepares the actual request and rechecks the complete
current quote before claiming the gate; a stale quote remains pending and is refused (exit
2). Human and budget resume build their provider resolvers over the command's existing database connection. See the preparation/resume contract. --abortrejects the selected budget gate, dispatches no agent and fails the run withbudget_exceeded(exit1). It calls no credential factory and creates no MCP client. A doubled resolved budget gate or terminal-run decision is a clean exit-0no-op before resource construction. For a nonterminal run, naming a resolved ordinary human gate is an invalid invocation (exit2), not a budget no-op.--secret-stdinfollows the human-resume stdin contract. A secret-bearing frozen run still needs its masked slots re-supplied, including on abort. The advice preserves the selected run, gate and decision; values stay out of argv. Drained stdin disables any later interactive prompt.--allow-mcp-stdio <digest>follows the existing declaration consent gate. Both human and budget resume reconnect against the original recorded workspace, with the frozen tool grant: removed, replaced or newly discovered MCP ids refuse rather than changing the authorized request. A budget rejection does not connect MCP. Signal teardown is armed before connect and removed after cleanup. Ctrl-C during connect/build returns exit1, leaves the decision unrecorded and the gate pending, and reaps any connected child. The interruption guard also covers resumes without MCP. Once engine resume starts, cancellation reaches both passive preparation and the active execution. A decision append may already be in flight: its successful acknowledgement stays durable, while cancellation prevents later dispatch and follows the engine's cancelled outcome; it never rolls back a durable approval. Signal subscriptions remain armed through database/client cleanup.- A successful native decision write yields to queued OS signal handling before its acknowledgement
admits the approved dispatch. Terminal persistence uncertainty returns exit
5with the delivered terminal retained in the outbox. Nonterminal ownership uncertainty refuses with exit6; its diagnostic does not claim that an earlier durable approval is absent. See the exit codes. - Under
--json, stdout remains a pure run-event stream, with diagnostics and no-op/interruption notices on stderr. There is no prompt under JSON, CI or no TTY. On a TTY the inline budget card shows the frozen scalar amount and requires that exact amount or rejection; unpriced/unrepresentable gates offer rejection only. A legacy inline card may explicitly continue without an allowance under current budget checks; the amount-bearing shell command cannot invent an amount for it. The card also shows the safely projected excluded candidates. An engine-refused stale or invalid inline budget approval stops further prompts and drains the actual ordered event stream until the aggregaterun:pausedis durably acknowledged, retaining the command's database and cancel handler through that barrier. It then returns exit3with the gate pending. Once cooperative cancellation is requested, an already queued pause cannot trigger that exit: the command retains its resources through the acknowledgedrun:cancelledterminal and returns exit1. Other competing terminals retain their own outcomes while the primary stream is being consumed. Cancellation from a dismissed card also stops all queued inline prompts. The primary subscription stays open through renderer teardown: cancellation accepted during Ink unmount is durably acknowledged before SQLite closes and before the single persistent summary is printed. A custom renderer that omits the summary barrier still cannot release the command resources before the cancellation terminal. Voluntary paused finalization releases input without printing an irreversible summary, then joins engine-owned local departure. The one primary reader remains active through both acknowledgements: a gate deadline or decision that advances the run keeps its real outcome. A stable pause returns exit3only after local detachment; it makes no claim about subsequent progress by another host. Terminal and fenced outcomes also join admitted work before SQLite or MCP resources close. See the canonical engine lifecycle. A join pending for 250 ms emits a fixedcleanup_pendingdiagnostic to stderr and continues waiting. A failed cleanup acknowledgement emitscleanup_failedand retains the owner rather than treating failure as completion. JSON diagnostics are nondurable objects withtype,codeandmessage; stdout remains the run-event stream. SIGINT stays owned through input and engine acknowledgement. On receiving an actual terminal, Ink immediately writes a fixed provisional human outcome to stderr (Run completed/failed/cancelled; run cleanup is pending.), even if its input mount has already exited or its last frame is throttled. This notice carries no final receipt verdict or provider error text. Ink prints one persistent summary for the actual final local outcome; plain/NDJSON add no synthetic terminal or summary. The operator can inspect or reject the pending gate throughbudget resume. Ordinary human-gate prompts retain their existing decisions. - An emitted budget rejection stops later queued cards and drains its real failed terminal. A cancellation or terminal arriving during live-view suspension suppresses the stale card; one arriving while a card is open dismisses it without requiring another answer. Late answers cannot replace the winning decision or terminal. A losing idempotent rejection does not stop subsequent genuine gates, and ordinary human-gate rejection retains its existing continue behaviour.
- Authority and matching companions produce one budget notice per
(runId, nodeId, gateId)in plain and TUI views. They never fall through to an ordinary human approval prompt. The durable protocol is defined once in the event contract.
Lists the pending human gates so an operator can pick the gateId to resolve — the multi-gate discovery surface the gate command's --gate requirement points at.
relavium gate list # every active run's pending human gates
relavium gate list <runId> # just one run's- With no argument it scans every active run; with a
<runId>it lists just that run's pending gates (an unknownrunIdexits2). Budget-cap pauses (budget:paused) are excluded — those are the separaterelavium budget resumesurface (ADR-0028). - It rests on the same persisted-event reconstruction the
gateresume path uses, so the listing and the resume can never disagree on what is pending. - Human output is one line per gate (
<runId> <gateId> <gateType> node=<nodeId> "<message>"); under--jsoneach pending gate is one NDJSON record —{ runId, gateId, nodeId, gateType, message, expiresAt? }(see Read-command--jsonoutput).
Lists past agent sessions from durable history.db, most-recently-updated first — the session counterpart of relavium list. Human output is one line per session (<id> <agentSlug> [<status>] <updatedAt> "<title>"); an empty history is reported clearly (exit 0). Under --json each session is one NDJSON record — { sessionId, agentSlug, title, status, modelId, createdAt, updatedAt, totalCostMicrocents, usageCoverage, unpricedCalls }, where title / modelId are null when absent (see Read-command --json output). The known actual amount, usage coverage and independent missing-price count come from one durable accounting snapshot. Human output remains identity/lifecycle metadata only. Soft-deleted sessions are excluded.
JSON discovery exhausts all live sessions in indexed 256-row pages within one read snapshot; page size is a working-memory bound, not a result limit. Identity, known money, usage coverage and missing-price counts therefore remain consistent across pages while another process writes. No per-session attribution or transcript is loaded for this listing.
Exports a persisted session to a .relavium.yaml scaffold for review before commit (ADR-0026) — the same contract the in-REPL /export drives. Writes <sessionId>.relavium.yaml in cwd by default (the file name is keyed on the unique session id, so two sessions never collide); --out <path> overrides, --force overwrites an existing target. The session row is marked exported with the written path. Under --json it emits a single session:exported event ({ type, sessionId, timestamp, sequenceNumber, workflowPath }). An unknown sessionId or an existing target without --force exits 2; success is exit 0.
Successful chat-export writes a fixed review/continuity notice on stderr, even in JSON mode. Live /export uses the rendered transcript in interactive chat and Home, and stderr on plain/JSON drivers. The scaffold cannot reproduce the session's summarised, trimmed or omitted working context; retained user/assistant text, attachments, shell output and authored agent literals may be sensitive. Review before running or committing. Strict structural tool-content exclusion remains in force; there is no raw-tool-result opt-in. Deterministic ASCII title normalisation and bounded session-identity fallback follow the canonical mapping. The notice adds no machine stdout record and is not emitted for a refused export.
If chat-resume or chat-export encounters a session message that fails persistence-format validation, it refuses with exit 2 and a fixed, content-free diagnostic. Restore a safe history backup or start a new session. A refused export writes no scaffold; JSON mode keeps stdout empty and puts one structured error on stderr.
Committed effect metadata is swept at one-shot teardown; unresolved effects remain audit evidence. No transcript is persisted and no earlier invocation is replayed. A failed one-shot identity reservation cannot clean another session's rows; see effect-journal.md §9.
Runs a single agent one-shot (non-interactive) on the same AgentSession infra as relavium chat — a session with one turn, then exit. The agent-first headline as a scriptable, CI-friendly primitive.
echo "summarize ./README.md" | relavium agent run code-reviewer
echo "review it" | relavium agent run ./agents/coder.agent.yaml --json
echo "review it" | relavium agent run code-reviewer --fixture ./fixtures/review.cassette.json --json- The
<agent>argument is required — a.agent.yamlpath or a.relavium/-discoverable agent id (resolved by the same strict parserrelavium chat --agentuses). An unknown agent is an invalid invocation (exit2). - The prompt is read from stdin (the
echo … | relavium agent runidiom); an empty stdin is an invalid invocation (exit2). --input k=vis reserved — currently rejected (exit2): a session does not yet interpolate{{ctx.*}}into the agent's prompt (the engine passessystem_promptverbatim), so the flag is failed loud rather than exposed as an inert no-op. It re-opens when session prompt interpolation lands (a tracked engine follow-up, deferred-tasks.md).--fixture <path>replays a recorded LLM cassette so the run is deterministic and fully offline (no key, no network, no keychain) — the format is documented in agent-run-fixture.md. A malformed cassette exits2.--jsonemits theSessionEventNDJSON stream on stdout (the same shapechat --jsonproduces); otherwise the assistant reply streams in human form.--allow-mcp-stdio <digest>(repeatable) authorizes a local MCP program for this invocation only.agent runis the one-shot, pipeline-facing member of the chat family — its stdin carries the prompt, so it can never prompt for consent, and an agent declaring an unapprovedstdioMCP server exits2before anything spawns. See Local MCP servers need consent.--allow-fs-rootuses the global invocation contract, including when no selector config exists.- The transcript is not persisted — a stateless invoke (no transcript or resumable session; only an already-tombstoned durable effect-identity bookkeeping row), unlike the REPL. It does still open
history.dbto attach the effect journal (effect-journal.md): an external effect is carried forward by the target, not by the run, so an unattached journal would refuse every effectful tool on this surface. Its effect metadata is read at teardown for the owned committed-row sweep, never for replay. Ahistory.dbthat cannot be opened fails the invocation. - The exit code is the turn's outcome:
0on success,1on a turn error; an invocation fault is2. It is never4(that is the interactive REPL's session-ended code). A termination signal exits128+signo(130on Ctrl-C) after tearing any MCP servers down — this surface has no cooperative-cancel contract to preserve, unlikerun(ADR-0088 §1.3). - Ctrl-C leaves no orphan. Any
stdioMCP server this invocation spawned is reaped before the process exits, including on akill, a closed terminal window, or a second impatient Ctrl-C.
Registers LLM providers and manages their API keys in the OS keychain (workstream 2.C; @napi-rs/keyring,
ADR-0019). The llm_providers row stores
only the keychain account ref; provider displays expose a hint, not the key value. The host resolves
the credential into process memory for provider requests, including verification/model discovery;
key custody does not mean the adapter can call a provider without reading its credential. See
the keychain/secret contract.
Known providers: anthropic, openai, gemini, deepseek.
relavium provider list # registered providers + whether a key is set
relavium provider add anthropic # register a provider (its default base URL + pricing page)
relavium provider add openai --pricing-url https://example.com/prices # override the pricing reference page
echo "$ANTHROPIC_API_KEY" | relavium provider set-key anthropic # store a key (read from STDIN, never argv)
relavium provider test anthropic # verify the key with a minimal live request
relavium provider remove-key anthropic # delete the key from the keychain
relavium provider list --verify # + a live key-verification probe per providerset-keyreads the key from stdin, never a CLI argument (argv leaks intops, shell history, and CI logs); pipe it or use a heredoc. The key is stored in the OS keychain under the canonical entry-naming scheme (keychain-and-secrets.md). The common secret-stdin limit applies before key storage.listshows each registered provider, its base URL, and whether a key is set — a fast, offline read (no key is read; the status is derived from the stored keychain ref).--verify(2.5.G S11, ADR-0065 §6) additionally runs a bounded, key-redacted live probe per provider — the SAMEvalidateProviderKeyseamprovider test+/doctor --deepuse — and reportsverified/failed — <redacted reason>/no keyin the status column. A provider with no resolvable key (keychain → env both empty) is reportedno keyand never probed (so--verifynever hangs on a keyless provider); the key is never echoed.listhonors--json(ADR-0049): one key-free NDJSON record per provider —{ name, baseUrl, keySet, verified, verifyDetail }, whereverifiedisnullwithout--verify(elsetrue/false) andverifyDetailis a short reason — a redacted failure message, or"no key"when a probed provider has no resolvable key, elsenull(so averified: nullrecord withverifyDetail: "no key"is a probed-keyless provider, distinct from the un-probedverifyDetail: null). The--verifyprobes run concurrently (each timeout-bounded), so verifying N providers costs one timeout, not N.add/set-keyauto-register the provider row.--base-url <url>onaddrecords a custom endpoint that is now actually used at request routing (2.5.G S9, ADR-0065 §3–4 — the earlier "dead-config" gap is closed): the resolver rebinds that provider's adapter to the custom endpoint and routes all its egress (streaminggenerate/stream+ themodels.listrefresh) through the shared SSRF-validated hop (connectValidated— HTTPS-only, no embedded credentials, every resolved IP range-blocked, connect pinned to the validated IP for DNS-rebinding safety). Custom endpoints are OpenAI-compatible only this round (openai/deepseek); a--base-urlonanthropic/geminiis refused with a clear message (exit2), as is a non-HTTPS / private-loopback / credential-bearing URL, or one carrying terminal-control / bidirectional characters (fail-fast atadd, so the stored value is terminal-safe on every surface). The provider-id set stays closed — a custom endpoint reuses theopenai/deepseekid (ADR-0065 §6).--pricing-url <url>onaddoverrides the seededpricing_reference_url— the public pricing page where you find a model's price to hand-enter viarelavium models pricing(2.5.G S10, ADR-0065 §1). Each known provider is seeded with its default pricing page; theaddconfirmation echoes it. It is a display-only pointer, never fetched (not an egress target), so — unlike--base-url— it needs no SSRF gate; it is validated as an HTTPS URL with no embedded credentials and stored normalized (control bytes percent-encoded, so it is terminal-safe). Omitting the flag on a re-addpreserves a previously-set custom pointer.testdoes a 1-tokengeneratethrough@relavium/llm;--model <id>overrides the cheap default. A bad key fails cleanly (exit2) without echoing the key.- Key resolution (used by
run+test): OS keychain →RELAVIUM_<PROVIDER>_API_KEYenv var → error. The env var is the headless/CI source; thesecrets.encencrypted-file fallback is deferred past v1.0 (keychain-and-secrets.md). Keychain writes and deletion surface a fixed operation-specific error when unavailable (locked / no Linux Secret Service). Runtime resolution may still use the documented environment source; it never creates a plaintext fallback file.
The common reader used by provider set-key, run --secret-inputs-stdin,
gate --secret-stdin and budget resume
accepts at most 65,536 input bytes (64 KiB) across all chunks, including whitespace.
It refuses an oversized pipe rather than truncating it. String chunks count their UTF-8
encoding; byte chunks are assembled before decoding, preserving split Unicode characters.
This compatibility limit applies to the incoming pipe, before caller-specific trimming
or line parsing.
The reader refuses TTY stdin before reading; redirecting stdout does not enable terminal
secret entry. It preserves admitted whitespace: provider set-key trims the key, while
run/gate line parsing preserves values. Empty/whitespace-only input, over-limit input and
read failures produce payload-free exit-2 diagnostics. Early refusal stops reading.
This byte bound does not impose a deadline on a stalled pipe.
Every command that can open an MCP-bearing artifact — relavium run, relavium chat,
relavium chat-resume, relavium agent run, relavium gate, relavium budget resume, and the bare-invocation Home — passes through the same gate
before any local program starts (ADR-0084). An
agent or workflow with a stdio MCP server declares a program on your machine; the artifact chooses it,
so the decision is yours. Network transports (http, sse, websocket) need no consent — there is no local
process — and a server already approved on this machine is not asked about again.
The prompt asks once per server, showing the resolved executable, each argument on its own line, the
environment (a secret shows as a <secret:NAME> marker, never its value), the working directory, the artifact
that declared it, and the fingerprint. It defaults to No, and Ctrl-C is a refusal. A refused server means
the invocation stops before anything spawns.
Approval is recorded per machine in ~/.relavium/mcp-consent.ndjson. It covers the exact declaration — a
changed executable, argument, environment value or working directory is a different program and is asked about
again.
Asking requires all four of: a TTY stdout, a TTY stdin, no --json, and no CI environment. Missing any of
them — a pipeline, a piped stdin, a machine-readable stream — the invocation refuses with exit 2 rather
than prompting into a stream nobody reads or hanging a job on a question nobody answers. The refusal prints
each unapproved server and its digest on stderr:
fs: /usr/local/bin/npx v1:9f2c…
The digest is a hash of the declaration, not a secret. To authorize those servers for a non-interactive invocation, review each one and pass it back:
relavium run pipeline.relavium.yaml --allow-mcp-stdio v1:9f2c… --allow-mcp-stdio v1:41ab…--allow-mcp-stdio is repeatable, authorizes only that invocation, and writes no grant — a CI runner
does not accumulate standing trust. It exists on relavium run, relavium agent run, relavium gate
and relavium budget resume, including their non-interactive execution-resume paths.
relavium budget resume <runId> --abort does not start MCP servers. Human-gate decisions resume
workflow execution and use the same MCP consent rules; the interactive chat family answers at the
prompt instead.
relavium media prune-temporaries --legacy-offline --force [--json]Explicit offline cleanup of the two historical ownerless media-temp layouts under the canonical global home CAS. Both flags are required strict booleans; absence or a string-valued equivalent is an invocation refusal before CAS/service construction. Stop old producer binaries first: their ownerless files cannot prove quiescence. A live or uncertain new-protocol producer refuses the entire operation before any legacy deletion. An absent store reports zero and is not created. The command opens no project config, history database, keychain, provider or network service.
Human output reports removed/retained counts and fixed retention reasons. --json emits one
{ removed, retained, reasons } object; retained reason counts sum to retained. Neither form
contains paths, native diagnostics or file bytes. Successful maintenance, including conservatively
retained candidates, exits 0; missing flags/exclusive-offline refusal exits 2; an operational
filesystem failure exits 1. This command never changes published CAS objects or read grants.
Exact temp grammars, ownership/recovery rules and reason vocabulary belong to the
physical media lifecycle contract,
under ADR-0106.
CI relies on deterministic exit codes. The table is the closed set for run and gate; the signal codes
128+signo (129 SIGHUP, 130 SIGINT, 131 SIGQUIT, 143 SIGTERM) belong to the surfaces that end on a
signal rather than draining through one — the interactive Home and agent run. relavium run is deliberately
not one of them: its Ctrl-C is a cooperative cancel that exits 1.
| Code | Meaning |
|---|---|
0 |
Workflow completed successfully |
1 |
Workflow failed (a node errored and exhausted retries/fallbacks) |
2 |
Invalid invocation (bad arguments, workflow not found, schema validation error) |
3 |
Run paused at a human gate (CI/non-interactive mode) — resume with relavium gate |
4 |
A chat session ended — via /exit, /cancel (or Ctrl-C in TTY mode), or an input-stream EOF — a user-initiated end of a relavium chat REPL — see chat-session.md |
5 |
The run produced a terminal, but whether it reached the durable log is not known (ADR-0078 §5). The terminal is held in the outbox and retried on the next start |
6 |
Ownership was refused before starting, or this host was fenced out and stopped without claiming an outcome (ADR-0079 §5, §7). Inspect current history; the successor may already have finished |
7 |
An external effect needs attention, including a prior unresolved attempt or an admitted effect whose final receipt remained unresolved after host joining (effect-journal.md §4, §8). Do not retry: inspect the target, then resolve the row out of band |
8 |
Required money receipts were not durably acknowledged. Do not repeat paid work: inspect relavium logs <runId> and the ledger before recovery. This is separate from terminal-outbox uncertainty |
Final required-money uncertainty takes priority (
8), then effect attention (7), then the actual terminal/ownership disposition (5/6) and ordinary outcome. A combined8warning also directs the operator to inspect the effect target and resolve its row. Final health never replaces the actual terminal, its error code or durable history; it selects the command's disposition after joining admitted work. The fixed final-health stderr diagnostic hastype: diagnostic,code,message,terminalDurability(durable,uncertainornone) andeffectNeedsAttention. It reports one highest-priority warning; a combined money/effect case includes both remedies. These fields are nondurable and never added to stdout RunEvents.Codes
5and6have different remedies:5means a terminal exists and the CLI will retry writing it, so re-check after the next invocation;6means this invocation cannot claim an outcome — readrelavium logs <runId>for current truth. A terminal-free fenced close uses6when neither final-money nor effect attention takes priority; it never promises an outbox retry.Exit codes
7and8must be surfaced to a person, never automatically re-invoked. An external effect or paid call may already have happened without its required receipt. Repeating it can duplicate the effect or charge.A pre-start live-ownership refusal (
6) can be retried after backoff. A fenced host's6instead requires inspecting current history before resuming: local departure does not certify a successor's present status. An invalid invocation (2) requires correcting the call.Exit code
3lets CI distinguish a pause-for-approval (arun:pausedevent — the run's aggregate suspension, a human/approval/budget gate — in non-interactive mode) from a hard failure. This is the canonical home for the gate-paused code; other docs reference it as3.Under
--json, a pre-run fault (exit2) writes its structured{ "type": "error", … }detail to stderr while stdout stays empty (ADR-0049) — the exit code is the primary fault signal; read stderr for the detail.Exit code
4is the canonical chat-session-ended code: it marks a deliberate/exit(or its--jsonequivalent, a finalsession:cancelled/end event) from therelavium chatREPL, kept distinct from a successful workflow run (0) and a hard failure (1) so a wrapper script can tell "the user quit the chat" apart from either. Other docs reference it as4.Exit code
5is durability-uncertain, and it is deliberately neither0nor1. The run may well have COMPLETED — the outputs are in the delivered terminal — and only its durable record is missing, so reporting success would be as wrong as reporting failure. The terminal is held in the host's terminal outbox (~/.relavium/terminal-outbox.ndjson, besidehistory.dband deliberately not inside it) and retried by the nextrelavium run,relavium gateorrelavium budget resume— these three commands construct aWorkflowEnginefor their run/resume path.chat,agent runand the bare-invocation Home run onAgentSessionand have no engine to drain with. A script seeing5should treat the run as done-but-unrecorded;relavium statusNAMES such a run (terminalHeldin--json) so the state is visible, but it does not itself drain — draining claims a run lease, and a status read must not take ownership of a run another process may be finishing. Other docs reference it as5.The bare-invocation interactive Home (2.5.B, home.md) is a long-lived mode whose clean exit is
0(Ctrl-C / Ctrl-D on an empty prompt). A chat launched from inside the Home has its own exit code4, which the Home loop consumes — a chat ending returns to the Home, never leaked. An external signal to the Home runs teardown then exits the conventional128+signo(130SIGINT /143SIGTERM) so a pipeline still detects the interruption.
The CLI is designed to run inside pipelines. A typical pattern: install globally, provide the API key via the OS keychain or an environment variable, and run with --json for parseable output.
# illustrative CI step
- run: npm install -g relavium
- run: relavium run .relavium/code-review.relavium.yaml --input file=src/index.ts --jsonA workflow whose agents declare a stdio MCP server will not spawn one in a pipeline: there is no one to
ask, so it exits 2 and prints each server's digest. Review each program, then authorize it per invocation —
see Local MCP servers need consent.
- run: >
relavium run .relavium/code-review.relavium.yaml --input file=src/index.ts --json
--allow-mcp-stdio v1:9f2c…For a complete walkthrough (key handling, gates, artifacts, exit-code checks), see run-a-workflow-in-ci.md.
In Phase 2,
relaviumgains cloud-mode commands (e.g.relavium auth loginvia OAuth Device Flow, and switching execution to the cloud). The engine interface is identical in both modes — the CLI requires no code changes to target cloud execution. See ../portal/api-reference.md and ../../architecture/cloud-phase-2.md.
Numeric cost totals are known realized sums. Human totals qualify unverified history, pending invocations, unknown usage and missing prices; machine records expose usageCoverage and independent price information. Unknown usage at a numeric zero is never free. The /cost panel includes models with uncertainty but no realized-money row; estimate release leaves usage uncertainty intact. Resumed/reseated views seed the existing durable coverage instead of a fresh zero. Compaction notices distinguish known observed tokens from complete summary usage. See the seam, event contract, and database contract.