Skip to content

CLI conventions: the result envelope and exit codes

Protean is standardizing two contracts so that a script or an agent can consume a command's output uniformly: one result envelope for machine-readable output, and one exit-code convention. This page is the durable reference for both, and the shape a new command that emits --json output should follow.

check and verify follow these contracts on every output format. events catalog, subscriptions status, projection status, and outbox halted follow them for their --json machine output: a successful run and any usage or load error are the envelope, with the exit codes below. Their human (non---json) output keeps its historical shape, so a load failure there still exits 1. eventstore verify follows the same --json contracts, and in both modes it exits 2 when it cannot connect to the event store. ir diff follows the exit-code convention: 2 for a usage, load or config error, printed on stderr, and 3 for its own "non-breaking changes only" result. Its --format json output keeps its own shape and does not use the envelope. upgrade-check follows the exit-code convention too: 1 for warning findings and 2 for a usage or environment error, printed on stderr. Its --format json output is still a plain list of findings, without the envelope. ir check predates the contracts and is not yet converged: its exit codes and output shape still differ.

The result envelope

Each command listed above as following the envelope wraps its machine-readable output (under --json, or --format json for check) in one stable JSON object:

{
  "version": "0.1.0",
  "status": "pass",
  "data": { "...": "command-specific detail" },
  "diagnostics": [ "...typed Diagnostic records..." ]
}
Field Type Description
version str Semantic version of the envelope format.
status str Coarse verdict: pass, fail, or error (see below).
data object The command's own detail: check's report, verify's stage tree, and so on.
diagnostics array The typed Diagnostic records (may be empty).

status is the coarse verdict that maps to the exit-code class:

  • pass: Success (exit 0).
  • fail: The command ran and found a failure it is designed to detect (a gating diagnostic, a failed stage). The fine-grained severity is in data and diagnostics, not the status.
  • error: A usage or environment error that stopped the command from running (a bad option, no or unloadable domain, malformed config, IO). Maps to the usage class (exit 2).

The envelope ships a pinned, versioned JSON Schema at src/protean/cli/schema/v0.1.0/envelope.schema.json (mirroring the IR schema precedent). Conformance tests check the --json output of each command that follows the envelope against this schema, so a change that breaks the shape fails the tests.

data and diagnostics split the payload deliberately, so a consumer should read both. diagnostics carries the lint findings (the recoverable, per-element diagnostics). data carries the command's own detail and its fatal errors: a domain that would not load, a malformed [lint] config, a failed init stage. The command reports those fatal errors in data (e.g. data.error, or data.stages.init.error for verify). It does not report them in diagnostics. A diagnostics-only consumer will miss them.

SARIF and GitHub-annotations output (check --format sarif / --format github-annotations) are not wrapped: they are external standard schemas consumed by other tools, and stay as-is.

The exit-code convention

Exit code Meaning
0 Success.
1 The command ran and reports a failure it is designed to detect. The severity/stage detail is in the envelope, not the code.
2 A usage or environment error it could not run past: a bad option or argument (Click's own default), no or unloadable domain, malformed config, IO.
>=3 Command-specific failure classes, documented per command.

Because 2 = usage is Click's own default for a malformed command line, a command that sets no explicit exit code already conforms.

Per-command codes

protean check

Exit code Meaning
0 Nothing at or above the configured [lint].level floor.
1 A findings failure: validator errors, an IR-build error, or a warning/info at or above the floor.
2 Usage/environment: a bad --domain, --level, or --format; a domain that will not load; a malformed [lint] config.

protean verify (it gives each stage a fixed code from 3 up, in addition to the shared 0/2):

Exit code Meaning
0 All green: init, check, and tests pass.
2 Usage: the domain was not found, --path is not a directory, or --env is blank.
3 Init failed: the domain was found but did not initialize.
4 Check failed: a malformed [lint] config, or findings at or above the floor.
5 Tests failed.

protean ir diff (it follows the codes above, without the envelope):

Exit code Meaning
0 No changes, breaking changes under strictness = "warn", or strictness = "off".
1 Breaking changes.
2 Usage, load or config error, printed on stderr.
3 Non-breaking changes only.

See protean ir diff for the details.

protean upgrade-check (it follows the codes above, without the envelope):

Exit code Meaning
0 No findings, or info findings only.
1 At least one warning finding, including CHECK_FAILED.
2 Usage or environment error, printed on stderr.

See protean upgrade-check for the details.

Clean stdout

Under machine-readable output, stdout carries only the envelope: exactly one JSON object and nothing else. Every log line, warning, and human error message goes to stderr, so protean check --format json | jq (or protean verify --json | jq) stays parseable.