Skip to content

fix(cli): name the missing flag instead of printing a Zod dump - #648

Open
filip131311 wants to merge 1 commit into
mainfrom
filip/run-cli-validation-messages
Open

fix(cli): name the missing flag instead of printing a Zod dump#648
filip131311 wants to merge 1 commit into
mainfrom
filip/run-cli-validation-messages

Conversation

@filip131311

Copy link
Copy Markdown
Collaborator

Fixes #645.

Before

$ argent run gesture-tap
[
  { "expected": "string", "code": "invalid_type", "path": ["udid"],
    "message": "Invalid input: expected string, received undefined" },
  ... two more
]

After

$ argent run gesture-tap
Error: missing required flags --udid, --x, --y

argent run gesture-tap [flags]
...full per-flag help...

Scope

The issue names the missing-flag case. Reproducing it showed the same raw dump has a second
trigger: any constraint the tool itself rejects, which the CLI cannot know in advance.

invocation before after
gesture-tap (no flags) raw issue array missing required flags --udid, --x, --y
screenshot --scale 99 raw issue array --scale Too big: expected number to be <=1
gesture-scroll --udid X --x .5 --y .5 (zero deltas) raw issue array Pass a non-zero deltaX and/or deltaY …
run-sequence --udid X raw issue array missing required flag --steps-json
gesture-tap --x abc already good unchanged
runtime failure plain message, exit 1 unchanged

Fixing only the first row would have left the CLI's error voice less predictable than before, so
both go through the same renderer.

How it works

Two sources, one output. Missing flags are found locally from the tool's published schema — a
presence check only, never a type or range judgement, which stays exclusively the server's.
That is sound because the schema generator excludes defaulted fields from required
(debugger-connect requires device_id, not port), so it cannot reject a call the server would
have accepted. Everything else is read off the rejection the tool-server already returns.

Both produce one report rendered by one function and reported through one call site, so the
wording, the output channel and the exit code cannot drift between them.

Recognition of a rejection is structural — the shape of the issue list plus the tool's own
schema — never the wording of a message, which is not ours to depend on and changes between Zod
releases. Four gates must all hold, and any failure leaves the existing handling untouched.

Details worth a reviewer's attention:

  • A rule spanning the whole payload reports no field. gesture-scroll and gesture-rotate
    validate at the object level, and screenshot-diff is .strict(). Treating those as
    field-scoped would print Error: --undefined …; they are stated on their own instead.
  • A rule firing on an optional field is not called a missing required flag.
    await-ui-element refines on expectedText, which it does not require — reporting that as
    missing would contradict the help block printed directly underneath, where the field carries no
    (required) marker.
  • The check runs after --args is merged, so argent run gesture-tap --args '{...}' is not
    falsely reported as missing everything. Verified live: it reaches the device and taps.
  • Presence uses own-key semantics, so a required field named e.g. constructor is not treated as
    supplied by the prototype chain.

Behaviour changes a reviewer should weigh

  • A tool-rejected value now exits 2 instead of 1. run.ts already uses 2 for bad input and 1
    for a failed run; a Zod 400 means the tool never ran. No in-repo consumer tests for a specific
    code — scripts/e2e-full only checks zero/non-zero. Note this does not make 2 universal:
    an unknown tool name still exits 1, which is untouched here.
  • --json gains a defined failure shape (it had none — the blob printed regardless). One
    object on stderr, nothing on stdout, so --json | jq on a failed run reads an empty stream and
    a non-zero status:
    { "error": "missing required flags --udid, --x, --y",
      "missing": ["--udid", "--x", "--y"],
      "issues": [] }
    issues carries the tool's list verbatim, which is the raw data the issue asked to keep.
  • One adjacent fix, called out because it is not the reported bug: the global-option parse ran
    outside the error handler, so argent run screenshot --out printed a raw stack trace. Same
    bad-input-to-ugly-output defect, one line above the fix.

Not done

  • The unknown-flag hole (argent run gesture-tap --bogusflag 1 is silently accepted and the tool
    runs). The pass-through is deliberate — see the comment in flag-parser.ts — and belongs with
    Boolean flags silently ignore a space-separated value (--includeChildren false stays true) #586, the same CLI-argument family.
  • argent flow builds its own payload and has its own required check; it cannot reach this path.
  • The server still returns the issue list verbatim, so MCP and other clients are unaffected. All
    prettifying is CLI-side.

Verification

Live against the built CLI for every row of the table above, plus: debugger-connect asks only
for --device_id and never --port; --args '{...}' reaches the device and taps; a thrown
runtime error still prints its bare message with no Error: prefix, no help block, exit 1.

43 new tests. run-telemetry.test.ts passes unedited — it already asserted the non-Zod
passthrough, which makes it the regression guard for the untouched path. Full CLI suite 320
passed, registry 87 passed.

🤖 Generated with Claude Code

…od dump

`argent run <tool>` with a required flag omitted answered with the raw validation
issue list — a JSON array of `invalid_type` objects. The information needed for a
useful message was already there: the same schema that renders `--help` says which
fields are required and what each flag is called.

Missing flags are now named, in the voice the CLI already uses for a bad value:

    Error: missing required flags --udid, --x, --y

    argent run gesture-tap [flags]
    ...

The check runs against the merged payload, so a field supplied through `--args` or
`--<field>-json` counts, and it runs before the call goes out — which also spares a
tool that takes file inputs from uploading them only to be told a flag was missing.

A value the tool itself rejects is reported the same way rather than dumped, since
constraints and rules spanning several fields are only knowable from the response:

    Error: --scale Too big: expected number to be <=1

A rule that spans the whole payload reports no field, so it is stated on its own
rather than attributed to an invented flag. A rule that fires on an optional field
is not called a missing required flag, which would contradict the help printed
directly underneath it.

Recognising a rejection is structural — the shape of the issue list and the tool's
own schema decide it, never the wording of a message. Anything that is not provably
this tool's own input validation keeps its existing handling exactly: a runtime
failure still prints its bare message and exits 1.

Both paths render through one function and report through one call site, so the
message, the channel and the exit code cannot drift apart between them. `--json`
gets one object on stderr carrying that same message, the missing flag names, and
the tool's issue list verbatim.

Also guards the global-option parse, which ran outside the error handler: `--out`
with no value surfaced as a raw stack trace.

Fixes #645
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.

argent run prints a raw Zod JSON blob when a required flag is missing

1 participant