Repository navigation
fix(cli): every --json failure answers with one envelope, and --json only where supported - #6622
Conversation
9534986 to
21814c3
Compare
There was a problem hiding this comment.
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
aeb5991 to
b741bd5
Compare
21814c3 to
edc4df1
Compare
b741bd5 to
75e80e4
Compare
There was a problem hiding this comment.
Verdict: APPROVE — 1 resolved, 0 open, 1 new · edc4df1 → 280f7dc
- important — a
--strictlint abort under--jsonsays only "Command failed": with--jsonnow quiet,presentRenderLintAbortandpresentRenderLintFindings(render/execute.ts:299,:309) print nothing, thenfailCommand()throws. stderr is empty, so the boundary falls back to the generic text. To reproduce, runrender q --strict --jsonon a project with lint errors (for example, a root withoutdata-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
The base branch was changed.
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Automations to automatically generate PRs for you. |
…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]>
280f7dc to
dab4b83
Compare
Edit accuracy: accurate 271 (base branch 271), smooth 144 of thoseThe gate passes. Quarantined, measured but not gated (0) Unstable (1)
|
What
Last of four stacked PRs (base: #6621). Fixes the
--jsonfinding:--jsonwas 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--jsonby declaring ajsonarg, and every other command rejects it with exit 2 and the codejson_unsupported.--jsoncame back as a bare string ({"ok":false,"error":"Command failed",...}fromlint), a command-specific shape, or nothing on stdout at all (info,compositions,render,snapshot,cloud getin 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,--jsonalways returns an envelope, and define commands with the shared factory.Why
An agent passing
--jsonparses 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 genericCommand failed/Invalid command usagea presented failure carries, itscauseor the title the command reported througherrorBox. Every command catch that writes a--jsonfailure uses it, solint --jsonandcheck --jsonin an empty directory now sayNo composition found in <dir>.Notices (telemetry disclosure, deprecation lines,
diagprogress) 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 (lintfindings and counts,validateerrors,layoutissues,auth statusconfigured). Transcribe uses its ownTranscribeError.code.cli.ts: when--jsonis on the command line,watchOutput()notes whether anything reached stdout (stream and console, since Bun's console bypassesprocess.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 isruntime_errorwith the text the command printed on stderr as the message.A refused
timelineedit printed its JSON to stderr. It now prints the envelope to stdout, and keeps itsreasonandfixfields next toerror.reject-unknown-flags.ts:jsonleft the global allow list.Contract change for
--jsonconsumers:erroris an object, not a string. Tests that readerroras a string were updated (transcribe, models, capture, check, history, skills, timeline).Test plan
--jsonplus a bad flag (where--jsonis supported) or bare--json(where it is not) exits 2 with an envelope on stdout; plusinfo --jsonfailing in an empty directory and an unknown command under--jsontypecheck,oxlint,oxfmt --check,fallow audit, comment ratchet passdocs/packages/cli.mdx, pluspackages/cli/AGENTS.mdManual runs of the built CLI,
CLAUDECODE=1, stdin from/dev/null:🤖 Generated with Claude Code