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:
{
  "error": {
    "code": "E_PROVIDER_RATE_LIMIT",
    "class": "provider",
    "message": "OpenRouter rate-limited the request (retry after 30s)",
    "hint": "Wait and retry, or swap to a different model with --model.",
    "relatedDocs": "MODELS.md"
  }
}
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

ClassExitMeaning
user2Bad flag, missing arg, wrong id, malformed input
provider3OpenRouter / ElevenLabs / yt-dlp HTTP failure, rate limit, budget
env4Missing API key, missing dep on PATH, fs permission
gate5Quality gate or ref-required refusal
runtime1E_INTERNAL, uncaught exception
cancelled130SIGINT
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)

CodeMessage templateHint
E_INPUT_INVALIDInvalid input for <field>: <detail>Check the flag value against ralphy <verb> --help and retry.
E_FLAG_MISSINGMissing required flag: —<flag>Run ralphy <verb> --help to see required flags.
E_FLAG_UNKNOWNUnknown flag value for —<flag>: <value>Allowed values: <allowed>. See ralphy <verb> --help.
E_NOT_FOUND<kind> not found: <id>Run ralphy <kind> list to see available ids.
E_ALREADY_EXISTS<kind> already exists: <id>Pick a different id with --as <id> or delete the existing one first.
E_FILE_UNREADABLECannot read file: <path>Verify the path exists and is readable; ensure --cwd points at the project root.
E_FILE_MALFORMEDMalformed <format> in <path>: <detail>Fix the syntax error and re-run; use bun run lint for a strict check.
E_VALIDATION_FAILEDValidation failed for <target>: <detail>Adjust the input to match the schema; see ralphy models show <target> for valid values.
E_AGENT_UNSUPPORTEDAgent not supported in v1.0: <agent>Use --agent claude|cursor|codex.
E_WIZARD_NEEDS_TTYInteractive wizard needs a TTY (<verb>)Re-run on a terminal, or pass explicit flags to skip the wizard.
E_TEMPLATE_VERSION_UNSUPPORTEDTemplate version not supported: <version> (template <id>)Upgrade ralphy or downgrade the template’s version: field.
E_TEMPLATE_INPUT_MISSINGTemplate <id> requires a <requirement> but none was suppliedSupply via the matching flag (e.g. --brand <slug>).
E_TEMPLATE_SLUG_INVALIDTemplate slug rejected: <slug> (<reason>)Pick an archetypal slug instead — describe what the template does.
E_PROJECT_NOT_LINKEDNo project linked or auto-detected from <cwd>Pass --project <id> or cd into a workspace; see ralphy project list.

Provider errors (exit 3)

CodeMessage templateHint
E_PROVIDER_HTTP<provider> returned HTTP <status>: <detail>Check the provider status page; retry or swap model.
E_PROVIDER_AUTH<provider> rejected the request as unauthorizedRe-check the API key with ralphy doctor.
E_PROVIDER_RATE_LIMIT<provider> rate-limited the request (retry after <retryAfter>s)Wait and retry, or swap to a different model with --model.
E_PROVIDER_INVALID_REQUEST<provider> rejected the request as invalid: <detail>Check params against ralphy models show <id>.
E_PROVIDER_UNAVAILABLE<provider> is unavailable: <detail>Wait and retry; check the provider’s status page.
E_BUDGET_EXCEEDEDEstimated cost &lt;estimate&gt; exceeds budget cap <cap>Raise the cap via ralphy config set budgets.<scope> <usd> or trim the plan.

Environment errors (exit 4)

CodeMessage templateHint
E_ENV_KEY_MISSINGRequired API key not set: <key>Run ralphy setup and paste the key, or export <key>=....
E_ENV_KEY_INVALIDAPI key for <provider> failed verificationRegenerate the key and re-run ralphy setup.
E_DEP_MISSINGRequired dependency not found on PATH: <dep>Install with brew install <dep>, then re-run ralphy doctor.
E_FS_PERMISSIONCannot write to <path>: <detail>Check directory permissions; ensure the user owns the workspace dir.

Quality-gate refusals (exit 5)

CodeMessage templateHint
E_GATE_SCENARIOScenario quality gate refused: <detail>Rework the scenario (rewrite hook, tighten VO, swap model) and retry.
E_GATE_IMAGEImage quality gate refused for <slot>: <detail>Regenerate with a stronger reference or swap to a different image model.
E_GATE_VIDEOVideo quality gate refused for <slot>: <detail>Regenerate with different start/end frames or swap to a different video model.
E_REF_REQUIREDReference required for named entity: <entity>Attach a reference via ralphy ref add or pass --no-ref-consent to override.

Runtime + cancellation

CodeClassExitMeaning
E_INTERNALruntime1Catch-all internal error. File an issue.
E_CANCELLEDcancelled130SIGINT. Append-only state preserved.

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:
raiseError("E_NOT_FOUND", { kind: "Project", id: "demo-001" });
// → "Project not found: demo-001"
// → "Run `ralphy project list` to see available ids."
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:
ralphy doctor --json > doctor.json
if [ $? -ne 0 ]; then
  jq -r '.error.code' doctor.json
  exit 1
fi
# Distinguish provider hiccup (retry) from user error (fail).
ralphy render demo-001 --json 2> render.err
case "$(jq -r '.error.class // ""' render.err)" in
  provider) sleep 60; ralphy render demo-001 ;;
  ""       ) echo "ok" ;;
  *        ) cat render.err >&2; exit 1 ;;
esac