Skip to content

enhancement: let commands contribute structured details to the --json envelope #388

Description

@codeforester

Goal

Give a command a supported way to contribute structured fields to the --json lifecycle envelope,
instead of only being able to emit text that the framework nests as an escaped string.

Background

In JSON mode the lifecycle captures the command's stdout and places it in details.stdout as a
JSON string (_emit_json_success() / _emit_json_error(),
lib/python/base_cli/_run.py:591-640). docs/json-contracts.md explains the rationale, and it is
sound: "A command's human output is represented as a JSON string, so it cannot introduce prose or
ANSI escapes as a second stdout record."

The gap is that there is no other channel. The same document says:

The lower-level success_envelope(), error_envelope(), dumps_envelope(), and
redact_json_value() helpers are public for commands that need to publish their own structured
details records.

But a command that does that while --json is active gets its envelope captured and re-encoded
inside the framework's envelope — two envelopes, one stringified inside the other. So the
documented affordance for structured details only works when JSON mode is off, which is the
opposite of when a consumer wants it.

Verified evidence

Reviewed 2026-09-30 at a58ec109349fa3f3d03eae5b0de078b39ea361a2. A command printing one
structured line under --json:

{"schema_version":1,"schema":"base-cli.output","code":"ok","type":"success","message":"Success",
 "details":{"exit_code":0,"stdout":"{\"records\": [{\"host\": \"web-1\", \"status\": \"ok\"}]}\n"},
 "run_id":"..."}

A machine consumer must parse JSON, extract details.stdout, then parse that string as JSON again.
There is no supported alternative: Context exposes json_output so a command can detect the
mode, but nothing to write into.

Scope

Add a lifecycle-owned structured result channel, for example:

  • Context.set_result(mapping) / Context.add_details(**fields) that the envelope merges into
    details (under a reserved sub-key such as details.result to keep exit_code and stdout
    stable), with redact_json_value() applied as it already is; or
  • a documented StructuredResultWriter binding on Context that routes to details in JSON mode
    and to stdout in NDJSON/text mode, so one command body serves all output contracts.

Whichever shape is chosen, the field must be additive to the frozen v1 envelope, schema-validated,
and versioned.

Acceptance criteria

  • A command can attach structured data that appears as real JSON in the envelope, not as an escaped
    string, without disabling stdout capture.
  • The existing v1 envelope keys (schema_version, schema, code, type, message,
    details.exit_code, details.stdout, run_id) are unchanged for commands that do not use the
    new channel.
  • docs/schemas/v1/output.schema.json and error.schema.json are updated, with golden fixtures.
  • docs/json-contracts.md shows one recipe that produces the same data in --json, ndjson, and
    human text.
  • Redaction applies to the new channel and is tested.

Validation

Validate updated fixtures with both the Python validator and scripts/validate_contract_fixtures.mjs,
per the existing cross-language contract check.

Non-goals

  • Do not remove or change the meaning of details.stdout.
  • Do not let a command emit a second top-level envelope.
  • Do not make this the NDJSON replacement; large record sets still belong in NDJSON.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or product improvement

Type

No type

Projects

  • Status
    Backlog

Relationships

None yet

Development

No branches or pull requests

Issue actions