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
Problem
When
--output jsonor--output yamlis 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/yamlwas 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):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 jsonor--output yamlis 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 jsonis "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
warningsarray alongside the data in the JSON output. The array is omitted when empty. Consumers who don't care about warnings ignore the field.Scope
--output(current and future)