Skip to content

ADR 046: Emit a JSON error envelope on the failure path

This page is generated from docs/decisions/*.yaml by task docs:export-adr-markdown. Do not edit manually.

  • Number: 046
  • Title: Emit a JSON error envelope on the failure path
  • Category: architecture
  • Status: accepted
  • Provenance: guided-ai
  • Source: docs/decisions/046-json-error-envelope-on-the-failure-path.yaml

Decision

When --json is set and a command fails, write the machine envelope to stdout carrying an error object in place of data, with kind, message and exitCode drawn from the ADR-011 taxonomy. The human-readable line continues to go to stderr unchanged, and exit codes are unaffected. A consumer decides success or failure by which key is present: data on success, error on failure. The failure shape does not vary by command, so it is published once as docs/reference/schemas/output/output.error.schema.json rather than per command. Machine output requested but not parsed still counts as requested. When flag parsing itself fails, --json is recovered from the raw arguments, because an unknown flag or unknown command is precisely when a script most needs a parseable answer. Classify malformed invocations as validation. An unknown flag, unknown command, bad flag value or wrong argument count is the caller's mistake, so it reports kind validation and exit code 2 rather than falling through to internal and exit 1.

Agent Instructions

Emit failures under --json through jsonoutput.WriteError. Do not print a bare string to stdout on the error path, and do not move the human-readable line off stderr. Do not add data to the failure envelope or error to the success envelope. The two documents are distinguished by which key is present; a null data alongside error would make a command whose successful payload is legitimately null ambiguous. When adding an error kind to internal/domain/errors, add it to Kinds(). The published schema derives its enum from that function and tests validate real emitted envelopes against it, so a kind missing there publishes a contract the CLI can violate. Exit codes stay owned by the taxonomy. The envelope reports exitCode; it does not decide it. Route errors from cmd/bb through cli.ClassifyUsageError before reporting them. It maps pflag's typed errors and Cobra's usage errors onto validation and leaves everything else untouched, so a transport or server failure keeps the kind it was classified as, and internal is what an unclassified one becomes. Cobra raises its own usage errors as plain fmt.Errorf values, so recognising them means matching message text. Do not add a marker to cobraUsageErrorMarkers without a case in TestClassifyUsageErrorMatchesCobrasRealMessages that drives the real condition through a command tree. The test exists so a Cobra upgrade that rewords a message fails the build rather than silently reclassifying malformed invocations back to internal.

Rationale

--json is documented as a stable machine contract and ADR-011 promises structured JSON error payloads, but cmd/bb printed the error as plain text regardless of the flag. The envelope machinery was wired only to the success path, so a failing command left stdout empty. The primary consumer of --json is an agent or a CI script. Empty stdout plus an unparseable string on stderr cannot be distinguished from a command that produced malformed output, and it cannot be branched on by error kind — even though the classification already existed one line above, and was already being serialised into the diagnostics logger. Carrying error where data would sit keeps the envelope self-describing without a discriminator field, and keeps the success schema, which forbids additional properties, unchanged. Nothing that parsed the old output breaks: previously there was nothing on stdout to parse. Publishing one failure schema rather than one per command matches the shape of the problem and lets a consumer handle an error from a command it has never seen. Classifying usage errors is the other half of the same defect. An envelope reporting kind internal for a typo tells a consumer the CLI broke, so an agent branching on kind retries or escalates a failure it should have fixed in its own invocation. Emitting a well-formed envelope carrying the wrong classification would have replaced an obvious gap with a misleading answer. The kind-to-exit-code mapping is unchanged; what changes is which kind a malformed invocation receives, and with it its exit code, from 1 to 2.

Rejected Alternatives

  • Write the error envelope to stderr instead of stdout: Keeps stdout free of non-data, but leaves the original defect in place: a script reading stdout for --json still sees nothing and still cannot tell failure from malformed output. It would also mix the envelope with diagnostics, which ADR-014 deliberately keeps on stderr.
  • Add error alongside a null data in the existing envelope: Requires the success schema to allow a new property and to relax data, and makes a command whose successful data is legitimately null indistinguishable from a failure without also checking error for null. Presence of one key or the other is unambiguous.
  • Publish a failure schema per command, as with success payloads: The failure shape does not vary by command, so per-command copies would be identical files that drift. One schema also lets a consumer validate an error from a command released after it was written.
  • Leave usage errors reporting internal and exit 1: Avoids changing an observable exit code, but leaves the envelope carrying a classification that actively misleads: internal means the CLI broke, so a consumer branching on kind would retry or escalate its own typo. The documented kind-to-exit-code mapping never promised that a malformed invocation exits 1; that was an unclassified error falling through the default.
  • Match Cobra usage errors by wrapping every command's Args and FlagErrorFunc: Structural rather than text-based, and appealing for that reason, but Cobra returns the unknown-command error from Find before any command-level hook runs, so it cannot be caught that way. A partial structural solution plus text matching for the remainder is more moving parts than text matching guarded by a test that drives the real conditions.