Skip to main content
The CLI emits one of a fixed set of structured error codes on every failure. The catalog is the single source of truth — every err() and raiseError() callsite is lint-checked against it, and the codes are append-only post-v1.0. Wire ralphy into a CI pipeline by matching on the code field; never grep the message (it’s templated and may carry a {detail} that changes between runs).

Shape

Every error writes a JSON payload on stderr:
Pretty mode formats this with icons but the JSON shape is still recoverable. The exit code maps from class (see below).

Exit-code class mapping

Mapping lives in classifyExitCode() in cli/lib/errors/catalog.ts. The contract is append-only: classes can’t be renamed, only added.

The catalog

Codes group by class. Every row carries a message template (with {placeholder} tokens interpolated at raise time) and a remediation hint that names a verb / file / doc — never paraphrasing the message.

User errors (exit 2)

Provider errors (exit 3)

Environment errors (exit 4)

Quality-gate refusals (exit 5)

Runtime + cancellation

Class semantics

User (2) — the user’s input is wrong. Re-running with the same flags will fail the same way. Fix the flag or the resource and retry. Provider (3) — the model provider misbehaved. Transient or model-specific. Retry, switch model, or wait. The hint names the swap. Env (4) — the local machine isn’t ready. Missing key, missing binary, fs permission. ralphy doctor catches all of these without making a model call. Gate (5) — quality / policy refusal, not a technical failure. Two consecutive gate refusals on the same project is the agent’s stop signal — report concrete options, don’t render over the gate. Runtime (1) — bug. The CLI surfaces this rather than silently exiting on uncaughtException / unhandledRejection. File an issue with the stderr payload. Cancelled (130) — SIGINT. Append-only logs and manifests are preserved; re-running resumes from where you left off.

Append-only contract (post-v1.0)

The catalog is locked at v1.0 per 01-D-07:
  • Renames are forbidden.
  • Removals are forbidden.
  • Deprecating a code requires deprecated: true plus replacedBy: "E_...". Deprecated codes continue to be emitted for at least one major version.
  • Adding a new code requires a CHANGELOG entry.
Tests in tests/unit/errors-catalog.test.ts enforce the contract:
  • Every code matches /^E_[A-Z][A-Z0-9_]+$/.
  • Catalog has fewer than 40 entries (v1.0 budget).
  • Every entry carries a non-empty message, hint, relatedDocs.
  • Hints never restate the message verbatim.
  • Hints end with full-sentence punctuation.
  • Deprecated entries name a known replacement.
  • Placeholder braces are well-formed {name}.

Placeholder template syntax

Messages and hints carry {name} tokens that interpolate at raise time from the context object:
Allowed token names are part of the catalog’s contract — adding a new placeholder counts as a breaking change unless you keep the old one working too.

Wiring into CI

Match on the code field, not the message: