Skip to content

Latest commit

 

History

History
646 lines (515 loc) · 89.2 KB

commands.md

File metadata and controls

646 lines (515 loc) · 89.2 KB

CLI Command Reference (relavium)

Last updated: 2026-10-15

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.

Install & distribution

  • 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 tsup to a single ESM bin; released via the Release CLI workflow (pack → cross-OS install-smoke on macOS/Linux/Windows → npm publish with provenance), see release-a-surface.md.
  • Stack: commander.js for argument parsing, ink (React for terminals) for the interactive TUI, @clack/prompts for 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 archived keytar (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.ts

Output modes

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

The --json machine-output contract

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 sequenceNumber order. The line is the stable envelope (type / runId / timestamp / sequenceNumber per the schema); there is no wrapper, stream header, or version line. Every event the run emits appears, unfiltered.
  • The terminal run:completed event is the final result line — it already carries outputs + totalTokensUsed + totalCostMicrocents + durationMs, so there is no separate summary line. A run:failed terminal carries error + partialOutputs; failed and cancelled terminals also carry the engine's current cumulativeCostMicrocents. 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's run:failed/node:failed) and exits 2; 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 (--help emitting the command/flag surface as JSON, so an agent can discover the CLI without scraping prose) and a per-command effect annotation (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 a destructive entry is owned by ADR-0057 (2.5.E).

Global options

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.

Commands

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

Command manifest

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
}
  • effect is a forward-looking annotation: read never mutates, write creates/modifies, destructive irreversibly removes (today only provider.remove-key). It is marked for agent discoverability now; approval enforcement of a destructive entry is owned by ADR-0057 (workstream 2.5.E), not here.
  • modeScope lists 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 with omit = all.
  • A manifest ↔ commander drift guard (a unit test) asserts every real commander command has an entry with the same description (command + each option), so commander, the executeCommand table, and --help --json stay byte-consistent.
Example entry effect
run write
list read
provider.set-key write
provider.remove-key destructive

In-REPL slash commands

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.

relavium run

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 (exit 2). Every explicit --input for a declared secret is refused, including name=: 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-stdin reads declared secret inputs once from non-TTY stdin; TTY stdin is refused even if stdout is redirected. Supply one name=value line 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-2 diagnostics. Names such as __proto__, constructor and toString work 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 3 for a fresh relavium gate call; 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.
  • --json switches to NDJSON RunEvent output.
  • Ctrl-C (SIGINT) requests a cooperative cancel; the run drains to run:cancelled and exits non-zero (1). This is unchanged when the workflow declares mcp_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 the RELAVIUM_<PROVIDER>_API_KEY to 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: a fallback_chain provider's key (read only if the chain fails over to it) and a $ref-resolved external agent's key (until $ref resolution lands, 2.M–2.Q) are conditional and instead surface mid-run as a run failure (exit 1).
  • --allow-mcp-stdio <digest> (repeatable) authorizes a local MCP program for this invocation only, when there is no one to prompt. A workflow declaring a stdio MCP server that has not been approved on this machine exits 2 before anything spawns — see Local MCP servers need consent.
  • On a human_gate node 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 with relavium gate. The emitted human_gate:paused event carries the runId + gateId needed for the resume (relavium gate <runId> --gate <gateId>); with --json they 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>, and relavium gate list also surface pending gateIds, 2.I.)

Implementation status (as of workstream 2.G). run is wired to the @relavium/core engine: path/id resolution, --input coercion, the full lifecycle event stream, exit codes 0/1/2/3, SIGINT→cancel, and the stable --json NDJSON machine contract (stdout = pure RunEvent stream, diagnostics → stderr; see above) are live. The interactive ink TUI (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-color it keeps the TUI but suppresses ANSI color; it falls back to the plain line renderer when no TTY is attached or CI=true, and to NDJSON under --json (the three renderers are one onEvent seam over one bus). Provider keys resolve from the OS keychain → RELAVIUM_<PROVIDER>_API_KEY env var → error (2.C; manage them with relavium provider), and runs persist to durable history (2.H). The interactive human-gate prompt + out-of-band relavium gate resume are live (2.G): on a TTY a human_gate node renders a @clack/prompts card inline (approve / reject + comment / input) and the run continues; under --json/CI/no-TTY there is no prompt and the run exits 3, resumable later by relavium 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.

relavium list

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.

relavium create

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.

relavium import / relavium export

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.yaml into 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, so export <id> stays unambiguous): a same-kind slug collision is exit 2 unless --force overwrites it; a cross-kind collision (the id already names the other kind) is exit 2 always — rename one, since --force would leave both files in place.
  • export <id> [--out <path>] [--force] resolves <id> across both catalogs (an id naming neither is exit 2; an id that is both a workflow and an agent is exit 2, "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); --out overrides it. Under --json it emits a single { id, kind, path } record, where path is cwd-relative (the same shape import --json emits — 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.

relavium logs <runId>

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 type this binary does not know — is skipped, and a bounded note: naming the affected seq numbers goes to stderr (in both modes, so --json's stdout stays a pure RunEvent stream). 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 type this binary knows whose body does not parse — fails the command (exit 1), naming the run and the row's seq. It is never silently dropped. Other runs stay listable: logs/status/gate list isolate a damaged run so one bad row cannot blank the whole listing.

relavium status

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.

relavium models

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 exit 0 (an empty catalog is not a fault, like relavium list). Human output is one line per model (<modelId> <provider> ctx=<n> [<source>]).

  • relavium models refresh re-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. --providers and --catalog address 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 / skipped and the others still refresh. A per-provider failure is therefore not a command failure (exit 0 with the report). An explicit --providers refresh with zero providers connected (no key at all) is a clean exit 2 naming how to add a key, because nothing could be fetched.

    The catalog half needs no key at all — which is what makes --catalog the 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 (default false) governs whether a bare relavium models refreshes it first; an explicit models refresh fetches 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 (--input prompt, --output completion, --cached cache-read); media prices are per BILLED UNIT — --image per image, --audio and --video per 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/--output are given together or not at all, and a media-only invocation is legitimate: it is how you satisfy a strict_cost_cap refusal 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 stated 0 is believed and recorded as free, which for the media columns is distinct from unpriced. The row is written as source='user' and a live models refresh never 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, so budget.max_cost_microcents / [chat].max_cost_microcents degraded to allow for it; once user-priced, the cap is enforced (pre-egress and realized) on run, a run resumed via relavium gate, chat / chat-resume (incl. a /clear rebuild, re-read fresh), the Home chat, and one-shot agent run. --clear retires the override; the model falls back to the catalog price, or to unpriced if the catalog has none. Guards (each a clean exit 2, nothing written): an unregistered provider is refused (register it first with relavium 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/--output pair, is refused; --clear beside 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 listModels seam) and is never logged, persisted (the cache holds no key), or placed in the report / --json payload / any error message. A failing provider surfaces only the seam's already-redacted message (or a generic refresh failed), never a raw cause. models pricing writes 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 } (null for an absent optional; source ∈ static | live | user; lastRefreshedAt is 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 }, where status ∈ refreshed | skipped-no-key | skipped-unsupported | failed, the three counts are the model ids added / refreshed-in-place / soft-deactivated (null unless status is refreshed), and error is a short, secret-free reason (null unless failed). And, when the catalog axis ran, exactly one catalog record: { source, status, models, added, reason? } — source is always the literal 'catalog'; status ∈ refreshed | failed; models is the total the refreshed catalog describes and added the count the shipped snapshot did not carry (both 0 on a failure — the snapshot still answers); reason is present only on failed and 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; source is always user). A field is omitted when that flag was not given — the columns were preserved, not written, and reporting a 0 would claim a price nobody set. overriddenCatalogPrice is null when the catalog does not price the model.

Read-command --json output

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

relavium gate

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 / --input is 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 (exit 2).
  • --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 a gate_type=input gate 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 --json stream, and argv itself leaks into ps / shell history / CI logs — exactly the exposure relavium 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_KEY resolution path.
  • --gate <gateId> selects which pending gate to resolve. The resume contract is engine.resume(runId, gateId, decision) — gateId is mandatory on the resume path (it is carried on the human_gate:paused event; see sse-event-schema.md and resume_run in ipc-contract.md). --gate is 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 (exit 2) listing the pending gateIds.
  • Read the pending runId + gateId from the run's own output: the human_gate:paused event line under --json, or the paused at gate <gateId> (<type>) line the plain/TUI renderer prints. relavium gate list, relavium status, and relavium logs <runId> (2.I) also surface them out-of-band.
  • --secret-stdin re-supplies the run's secret-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 one name=value line on stdin; blank lines are ignored, and anything else is an invalid invocation (exit 2). 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 to ps, shell history and CI logs — the same rule relavium provider set-key follows. 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 opaque 401.)
    • --secret-stdin makes the invocation non-interactive. Reading the credential drains stdin, so a resumed run that reaches a second human gate cannot prompt: it exits 3 and is resolvable by another relavium gate, exactly as a --json or 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 secret input 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.
  • Idempotent. A doubled decision — the run already finished, or the named gate was already resolved — is a clean exit-0 no-op, never a double-advance (it leans on the engine's checkpoint/gate-state idempotency). An unknown runId is exit 2. 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-0 loop should pin --gate <gateId> to avoid resolving later gates unattended.

Implementation status (2.G). relavium gate runs in a fresh process from the original run: it reloads the run's frozen WorkflowDefinition + inputs from the durable history snapshot (2.H), reconstructs the paused checkpoint from the persisted event log, and calls engine.resumeFromCheckpoint over the same store — then drives the resumed run to its terminal (exit 0 complete / 1 failed / 3 paused again at a later gate). The recorded decidedBy is the constant cli (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 separate relavium budget resume surface. A run that declares a secret-typed input is resumed by re-supplying it on stdin with --secret-stdin (above); without the flag relavium gate still fails closed (exit 2), 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. The relavium gate list multi-gate listing is live (2.I).

relavium budget resume

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-amount accepts decimal digits representing a non-negative safe integer in microcents, including 0; signs, fractions, exponents and overflow are invalid invocations (exit 2). 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, --gate is 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. Human relavium status and 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 list and relavium gate continue to cover ordinary gates only.
  • A legacy gate with no frozen allowance, an unpriced quote or an unrepresentable amount offers --abort only on this surface. There is no input payload, comment or generic --approve flag.
  • 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.
  • --abort rejects the selected budget gate, dispatches no agent and fails the run with budget_exceeded (exit 1). It calls no credential factory and creates no MCP client. A doubled resolved budget gate or terminal-run decision is a clean exit-0 no-op before resource construction. For a nonterminal run, naming a resolved ordinary human gate is an invalid invocation (exit 2), not a budget no-op.
  • --secret-stdin follows 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 exit 1, 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 5 with the delivered terminal retained in the outbox. Nonterminal ownership uncertainty refuses with exit 6; 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 aggregate run:paused is durably acknowledged, retaining the command's database and cancel handler through that barrier. It then returns exit 3 with the gate pending. Once cooperative cancellation is requested, an already queued pause cannot trigger that exit: the command retains its resources through the acknowledged run:cancelled terminal and returns exit 1. 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 exit 3 only 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 fixed cleanup_pending diagnostic to stderr and continues waiting. A failed cleanup acknowledgement emits cleanup_failed and retains the owner rather than treating failure as completion. JSON diagnostics are nondurable objects with type, code and message; 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 through budget 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.

relavium gate list

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 unknown runId exits 2). Budget-cap pauses (budget:paused) are excluded — those are the separate relavium budget resume surface (ADR-0028).
  • It rests on the same persisted-event reconstruction the gate resume 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 --json each pending gate is one NDJSON record — { runId, gateId, nodeId, gateType, message, expiresAt? } (see Read-command --json output).

relavium chat-list

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.

relavium chat-export

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.

relavium agent run

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.yaml path or a .relavium/-discoverable agent id (resolved by the same strict parser relavium chat --agent uses). An unknown agent is an invalid invocation (exit 2).
  • The prompt is read from stdin (the echo … | relavium agent run idiom); an empty stdin is an invalid invocation (exit 2).
  • --input k=v is reserved — currently rejected (exit 2): a session does not yet interpolate {{ctx.*}} into the agent's prompt (the engine passes system_prompt verbatim), 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 exits 2.
  • --json emits the SessionEvent NDJSON stream on stdout (the same shape chat --json produces); otherwise the assistant reply streams in human form.
  • --allow-mcp-stdio <digest> (repeatable) authorizes a local MCP program for this invocation only. agent run is 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 unapproved stdio MCP server exits 2 before anything spawns. See Local MCP servers need consent.
  • --allow-fs-root uses 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.db to 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. A history.db that cannot be opened fails the invocation.
  • The exit code is the turn's outcome: 0 on success, 1 on a turn error; an invocation fault is 2. It is never 4 (that is the interactive REPL's session-ended code). A termination signal exits 128+signo (130 on Ctrl-C) after tearing any MCP servers down — this surface has no cooperative-cancel contract to preserve, unlike run (ADR-0088 §1.3).
  • Ctrl-C leaves no orphan. Any stdio MCP server this invocation spawned is reaped before the process exits, including on a kill, a closed terminal window, or a second impatient Ctrl-C.

relavium provider

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 provider
  • set-key reads the key from stdin, never a CLI argument (argv leaks into ps, 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.
  • list shows 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 SAME validateProviderKey seam provider test + /doctor --deep use — and reports verified / failed — <redacted reason> / no key in the status column. A provider with no resolvable key (keychain → env both empty) is reported no key and never probed (so --verify never hangs on a keyless provider); the key is never echoed. list honors --json (ADR-0049): one key-free NDJSON record per provider — { name, baseUrl, keySet, verified, verifyDetail }, where verified is null without --verify (else true/false) and verifyDetail is a short reason — a redacted failure message, or "no key" when a probed provider has no resolvable key, else null (so a verified: null record with verifyDetail: "no key" is a probed-keyless provider, distinct from the un-probed verifyDetail: null). The --verify probes run concurrently (each timeout-bounded), so verifying N providers costs one timeout, not N.
  • add / set-key auto-register the provider row. --base-url <url> on add records 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 (streaming generate/stream + the models.list refresh) 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-url on anthropic / gemini is refused with a clear message (exit 2), as is a non-HTTPS / private-loopback / credential-bearing URL, or one carrying terminal-control / bidirectional characters (fail-fast at add, so the stored value is terminal-safe on every surface). The provider-id set stays closed — a custom endpoint reuses the openai / deepseek id (ADR-0065 §6).
  • --pricing-url <url> on add overrides the seeded pricing_reference_url — the public pricing page where you find a model's price to hand-enter via relavium models pricing (2.5.G S10, ADR-0065 §1). Each known provider is seeded with its default pricing page; the add confirmation 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-add preserves a previously-set custom pointer.
  • test does a 1-token generate through @relavium/llm; --model <id> overrides the cheap default. A bad key fails cleanly (exit 2) without echoing the key.
  • Key resolution (used by run + test): OS keychain → RELAVIUM_<PROVIDER>_API_KEY env var → error. The env var is the headless/CI source; the secrets.enc encrypted-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.

Secret stdin limits

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.

Local MCP servers need consent

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

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.

Exit codes

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 combined 8 warning 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 has type: diagnostic, code, message, terminalDurability (durable, uncertain or none) and effectNeedsAttention. 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 5 and 6 have different remedies: 5 means a terminal exists and the CLI will retry writing it, so re-check after the next invocation; 6 means this invocation cannot claim an outcome — read relavium logs <runId> for current truth. A terminal-free fenced close uses 6 when neither final-money nor effect attention takes priority; it never promises an outbox retry.

Exit codes 7 and 8 must 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's 6 instead 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 3 lets CI distinguish a pause-for-approval (a run:paused event — 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 as 3.

Under --json, a pre-run fault (exit 2) 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 4 is the canonical chat-session-ended code: it marks a deliberate /exit (or its --json equivalent, a final session:cancelled/end event) from the relavium chat REPL, 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 as 4.

Exit code 5 is durability-uncertain, and it is deliberately neither 0 nor 1. 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, beside history.db and deliberately not inside it) and retried by the next relavium run, relavium gate or relavium budget resume — these three commands construct a WorkflowEngine for their run/resume path. chat, agent run and the bare-invocation Home run on AgentSession and have no engine to drain with. A script seeing 5 should treat the run as done-but-unrecorded; relavium status NAMES such a run (terminalHeld in --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 as 5.

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 code 4, 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 conventional 128+signo (130 SIGINT / 143 SIGTERM) so a pipeline still detects the interruption.

CI/CD usage

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 --json

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

Phase 2 note

In Phase 2, relavium gains cloud-mode commands (e.g. relavium auth login via 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.

Qualified usage and cost totals

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.