Skip to content

fix(cli): every --json failure answers with one envelope, and --json only where supported - #6622

Merged
jrusso1020 merged 5 commits into
mainfrom
fix/cli-agent-mode-json
Oct 28, 2026
Merged

jrusso1020 merged 5 commits into
mainfrom
fix/cli-agent-mode-json

Conversation

@jrusso1020

@jrusso1020 jrusso1020 commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

What

Last of four stacked PRs (base: #6621). Fixes the --json finding:

  • --json was accepted by every command (it was on the global allow list), so commands without JSON output took the flag and printed prose. Now a command supports --json by declaring a json arg, and every other command rejects it with exit 2 and the code json_unsupported.
  • Failures under --json came back as a bare string ({"ok":false,"error":"Command failed",...} from lint), a command-specific shape, or nothing on stdout at all (info, compositions, render, snapshot, cloud get in an empty directory). Every failure is now {"ok": false, "error": {"code", "message", "hint"?}}.
  • packages/cli/AGENTS.md (new) holds the CLI rules for agents: stdout is data, stderr is messages, never prompt without a TTY, exit 2 for usage errors, --json always returns an envelope, and define commands with the shared factory.

Why

An agent passing --json parses stdout. Empty stdout, a string where other commands put an object, or prose from a command that silently ignored the flag all break that parse, and none of them say which kind of failure happened.

Related work

Stack: #6616 → #6617 → #6621 → this PR.

How

  • failureMessage(error) returns an error's own message, or for the generic Command failed / Invalid command usage a presented failure carries, its cause or the title the command reported through errorBox. Every command catch that writes a --json failure uses it, so lint --json and check --json in an empty directory now say No composition found in <dir>.

  • Notices (telemetry disclosure, deprecation lines, diag progress) are never captured as the error message.

  • utils/jsonError.ts: jsonFailure(code, message, hint?) builds the envelope. The 25 places that printed {ok: false, error: "<text>"} now use it and keep their extra fields (lint findings and counts, validate errors, layout issues, auth status configured). Transcribe uses its own TranscribeError.code.

  • cli.ts: when --json is on the command line, watchOutput() notes whether anything reached stdout (stream and console, since Bun's console bypasses process.stdout.write) and keeps the tail of stderr. If the run fails and stdout is still empty, the boundary writes the envelope: a usage error carries its own code and hint (unknown_flag, unknown_command, json_unsupported, confirmation_required, prompt_unavailable, usage_error); anything else is runtime_error with the text the command printed on stderr as the message.

  • A refused timeline edit printed its JSON to stderr. It now prints the envelope to stdout, and keeps its reason and fix fields next to error.

  • reject-unknown-flags.ts: json left the global allow list.

Contract change for --json consumers: error is an object, not a string. Tests that read error as a string were updated (transcribe, models, capture, check, history, skills, timeline).

Test plan

  • Unit tests added/updated; contract test now checks, for every registered command, that --json plus a bad flag (where --json is supported) or bare --json (where it is not) exits 2 with an envelope on stdout; plus info --json failing in an empty directory and an unknown command under --json
  • CLI unit suite passes locally (422 files); typecheck, oxlint, oxfmt --check, fallow audit, comment ratchet pass
  • Manual testing performed (below)
  • Documentation updated: the error envelope in docs/packages/cli.mdx, plus packages/cli/AGENTS.md
  • Comments follow CONTRIBUTING.md "Comments"

Manual runs of the built CLI, CLAUDECODE=1, stdin from /dev/null:

$ hyperframes info --json </dev/null        # in a project
# exit 0, stdout is the info object with _meta, stderr empty (unchanged)

$ hyperframes info --json </dev/null        # in an empty directory
# before: exit 1, stdout empty
# after:  exit 1, stdout:
{"ok":false,"error":{"code":"runtime_error","message":"No composition found in <dir> No index.html file found. Run npx hyperframes init to create a new composition."}}

$ hyperframes rendr --json </dev/null
# after: exit 2, stdout:
{"ok":false,"error":{"code":"unknown_command","message":"Unknown command: rendr","hint":"Did you mean 'hyperframes render'?"}}

🤖 Generated with Claude Code

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: REQUEST CHANGES · 1 blocker · 0 important · 1 nit

The envelope, json_unsupported and the boundary fallback work. In an empty directory, info/render/snapshot/beats/cloud get --json now return a useful runtime_error, and an unknown command returns unknown_command with a hint. The blocker: lint --json and check --json still answer "message": "Command failed", which is the case this PR's description says it fixes (inline). The usage-errors-that-exit-1 cluster I flagged on #6616 also shows up here as runtime_error (e.g. capture --json "Missing URL").

— Genesis

Comment thread packages/cli/src/commands/lint.ts Resolved
Comment thread packages/cli/src/utils/jsonError.ts Resolved
@jrusso1020
jrusso1020 force-pushed the fix/cli-agent-mode-help branch from aeb5991 to b741bd5 Compare October 28, 2026 05:52
@jrusso1020
jrusso1020 force-pushed the fix/cli-agent-mode-json branch from 21814c3 to edc4df1 Compare October 28, 2026 05:52

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE — 2 resolved, 0 open, 0 new · 21814c3 → edc4df1. The manual ci.yml run on this head passed; Windows tests and Fallow were skipped in that dispatch.

— Genesis

heygengenesis[bot]
heygengenesis Bot previously approved these changes Oct 28, 2026

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE — 1 resolved, 0 open, 1 new · edc4df1 → 280f7dc

  • important — a --strict lint abort under --json says only "Command failed": with --json now quiet, presentRenderLintAbort and presentRenderLintFindings (render/execute.ts:299, :309) print nothing, then failCommand() throws. stderr is empty, so the boundary falls back to the generic text. To reproduce, run render q --strict --json on a project with lint errors (for example, a root without data-width). The result is {"code":"runtime_error","message":"Command failed"} with nothing on stderr. Writing the abort line to stderr when quiet, or failing with the reason, would give the agent something to act on.

This branch is dirty against fix/cli-agent-mode-help because #6621 was rebased under it, so it needs a rebase before merge. That push will clear this approval.

— Genesis

Base automatically changed from fix/cli-agent-mode-help to main October 28, 2026 10:02
@jrusso1020
jrusso1020 dismissed heygengenesis[bot]’s stale review October 28, 2026 10:02

The base branch was changed.

@mintlify

mintlify Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
hyperframes 🟢 Ready View Preview Oct 28, 2026, 10:15 AM

💡 Tip: Enable Automations to automatically generate PRs for you.

jrusso1020 and others added 5 commits October 28, 2026 10:03
…only where supported

Under --json, failures came back as a bare string (`"error": "Command failed"`
from lint), a command-specific shape, or nothing at all on stdout (info,
compositions, render, snapshot, cloud get). Now every failure is
`{"ok": false, "error": {"code", "message", "hint"?}}`:

- jsonFailure() in utils/jsonError.ts builds it. The commands that printed
  `{ok: false, error: "<text>"}` use it, keeping their extra fields (lint's
  findings, validate's errors). A refused timeline edit now writes it to stdout
  instead of stderr, and keeps its `reason` and `fix` fields.
- When a --json run fails without writing anything to stdout, cli.ts writes the
  envelope itself: usage errors carry their own code (unknown_flag,
  unknown_command, json_unsupported, confirmation_required), anything else is
  runtime_error with the message the command printed on stderr.

--json is no longer accepted by every command. A command supports it by
declaring a `json` arg; the others reject it with exit 2 and the code
json_unsupported, instead of accepting the flag and printing prose.

The contract test now checks every command's --json path, and packages/cli
gets an AGENTS.md with the rules: stdout is data, stderr is messages, never
prompt without a TTY, exit 2 for usage errors, --json always returns an
envelope.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…ric text or a notice

lint --json and check --json in a directory with no project answered
"message": "Command failed". The project check reports the problem, then
throws a presented "Command failed" with the real error as its cause, and the
command's own catch serialized that generic text. failureMessage() now returns
the cause, or else what the command reported through errorBox, and every
command catch that writes a --json failure uses it (lint, check, validate,
layout, snapshot, compare, grade-compare, media-treatment, tts,
remove-background, add, skills, lambda policies). compare, grade-compare and
add share failCaught() for that catch.

The boundary envelope follows the same order: the error's own message unless
it is the generic one, then its cause, then the errorBox title and hint, and
only then the stderr text. Notices (the telemetry disclosure, deprecation
lines, diag progress) are no longer captured, so validate, layout and inspect
no longer put "is deprecated" in the error message.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…swers with the envelope

Under --json a single render printed its header, lint findings and progress on
stdout, so a later failure such as `render --variables '{bad' --json` ended
with no JSON envelope. --json now silences that output the way --batch --json
already did, and --variables is parsed before the browser check so a bad value
fails without a Chrome download.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Under --json the render is quiet, so a --strict lint abort printed nothing and
the envelope said only "Command failed". The abort is now an error box on
stderr in every mode, naming the error and warning counts and the first
finding, and the --json envelope carries the same message. Lint now runs
before the browser check, so the abort never waits on a Chrome download.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The 1 s budget started when the command did. With lint now running before the
browser check, its subprocess (about 250 ms here) sat inside that budget, and
the test failed once under load. The budget now starts when the wrapper exits,
which is the latency the test is about.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@github-actions

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 271 (base branch 271), smooth 144 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

Unstable (1)

  • seqplainkeys-none-px-r0-root-z100: tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo false, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE — 1 resolved, 0 open, 0 new · 280f7dc → dab4b83. On a project with lint errors, render --strict --json now returns the reason and counts, and the hint names the first finding; stderr gets the same text. The three rebased commits match the ones I reviewed. CI is green.

— Genesis

@jrusso1020
jrusso1020 added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit 85efaec Oct 28, 2026
73 checks passed
@jrusso1020
jrusso1020 deleted the fix/cli-agent-mode-json branch October 28, 2026 11:05

This branch was successfully deployed

1 active deployment
staging - docs — dab4b834 Deployed Oct 28, 2026 by mintlify[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant