Skip to content

Every failure exits 1 with plain-prose stderr — scripts can't branch on failure class, even under --json #55

Description

@dark-sorceror

Summary

Every failure mode — not found, invalid API key, network down, usage errors — produces the same observable outcome: exit code 1 and a plain-English error: <prose> line on stderr. This holds even under --json: stdout stays empty and the error is still unstructured prose. A script or coding agent driving traceroot cannot distinguish "retry later" (network) from "re-authenticate" (auth) from "give up" (not-found) from "I built the command wrong" (usage) — short of parsing English strings that silently break the first time a message is reworded.

The machinery already exists and is unused: CliError carries an exitCode field (src/output.ts:74-82) that defaults to 1, and none of the 43 new CliError(...) call sites passes a second argument. The uniform behavior is pinned by tests/output.contract.test.ts, so the contract tests need to evolve with the fix.

Current behavior

  1. traceroot traces get <bad-id> --json → stderr error: Trace not found, stdout empty, exit 1.
  2. traceroot status --api-key tr-invalid --json → stderr error: Invalid API key, stdout empty, exit 1.
  3. traceroot traces list --host http://localhost:9999 --json → stderr error: request to … failed: fetch failed, exit 1.
  4. traceroot traces list --limit banana → stderr error: --limit must be a positive integer, exit 1.

Four failure classes; identical exit code, no machine-readable output anywhere. Mechanics: reportError (src/output.ts:111-115) returns err.exitCode (always the default 1) and has no awareness of --json.

Proposal

  1. Distinct exit codes per failure class — e.g. 2 usage/validation, 3 auth (401/403), 4 not-found (404), 5 network/timeout, keeping 1 for unexpected internal errors. Map HTTP statuses to classes centrally in src/api/client.ts where the CliError is thrown.
  2. JSON error envelope under --json: emit {"error":{"code":"not_found","message":"Trace not found"}} (one line; pick stdout or stderr and document it) so a JSON consumer never falls back to prose parsing.
  3. Document the exit-code table in README and --help.
  4. Related: src/cli.ts:81-83 calls process.exit(code) immediately after the stderr write, which can truncate the message on piped stderr (notably Windows); process.exitCode is already set — let the process drain naturally.
  5. Extend tests/output.contract.test.ts to pin the new codes and envelope.

Impact

  • Automated consumers retry the wrong thing or give up wrong — they can't tell auth-expired from doesn't-exist. This is the gap between "has --json" and "is actually scriptable".
  • CI cannot gate on failure class; a network blip and a genuine error look identical.
  • Prose-parsing workarounds turn every error-message improvement into a silent breaking change.

Acceptance criteria

  • Not-found, auth, network, and usage failures exit with distinct, documented codes; unexpected internal errors keep exit 1.
  • With --json, every failure also emits a single-line {"error":{"code":<stable-string>,"message":<text>}}; code values are a stable, documented enum.
  • Without --json, stderr output is unchanged in shape; only the exit code differs.
  • The exit-code table appears in README and is discoverable from --help.
  • process.exit immediately after a stderr write is removed in favor of process.exitCode.
  • Contract tests assert exit code per failure class, envelope shape under --json, and stdout purity for human-mode errors.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions