Skip to content

feat(cli): structured JSON/YAML error and warning output for --output mode #2040

Description

@rhuss

Problem

When --output json or --output yaml is active, successful command output is properly structured, but errors still flow through miette's human-readable diagnostic formatter. An automation consumer parsing stdout gets an unexpected non-JSON/YAML blob when a command fails.

--output json/yaml was introduced in PR #1989 (still pending) to enable automation and MCP tool wrappers to consume structured data instead of parsing ANSI-colored human output. This issue addresses the error path that PR #1989 intentionally deferred to be solved at the framework level.

Current behavior (--output json, gateway unreachable):

Error: openshell::gateway_connection

  × failed to connect to gateway 'openshell' at localhost:5173.
    Start the gateway service with the installed package manager,
    or register a different endpoint with `openshell gateway add <endpoint>`.

  ╰─▶ error sending request for url (https://localhost:5173/)

Exit code: 1, stdout: miette-formatted text (not JSON).

Expected behavior:

{"error": "failed to connect to gateway 'openshell' at localhost:5173", "code": "gateway_unreachable"}

Exit code: 1, stdout: valid JSON.

Motivation

During review of PR #1989, @johntmyers identified that the error output path was not fully addressed (#1989 (comment)). The current PR handles per-command error labels correctly for human output, but structured error output affects all commands and should be solved once at the framework level rather than per-command.

Primary consumers: MCP tool wrappers, CI scripts, and agent harnesses that parse CLI output programmatically. These tools currently have to detect and handle miette-formatted error text as a special case.

Design Sketch

Error Output (exit code ≠ 0)

When --output json or --output yaml is active and a command fails, emit a structured error on stdout with a non-zero exit code. The exit code is the authoritative error signal; the JSON payload provides the message and optional machine-parseable category.

Why stdout, not stderr: The purpose of --output json is "give me one parseable stream." Splitting success to stdout and errors to stderr forces automation to merge two streams. stderr in structured mode stays reserved for debug/trace logging only.

Warnings (exit code = 0)

When a command succeeds but has warnings, include them as a warnings array alongside the data in the JSON output. The array is omitted when empty. Consumers who don't care about warnings ignore the field.

Scope

  • All commands that accept --output (current and future)
  • Centralized error handling (one code path, not per-command)
  • Non-goal: changing the default human-readable error format

Metadata

Metadata

Assignees

No one assigned

    Labels

    state:triage-neededOpened without agent diagnostics and needs triage

    Type

    No type
    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