Skip to content

feat(cli): agent-friendly JSON, exit codes, and noun-verb commands - #633

Open
codyadam wants to merge 10 commits into
mainfrom
cursor/agent-friendly-cli-59d2
Open

codyadam wants to merge 10 commits into
mainfrom
cursor/agent-friendly-cli-59d2

Conversation

@codyadam

@codyadam codyadam commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

What changed

HUD CLI API commands now share one agent-facing I/O contract in hud/cli/utils/output.py:

  • --json / --output json on jobs, models, qa, trace, task, client, cancel, sync, eval, deploy, init, login, set, version, and auth. JSON goes to stdout; progress, warnings, and human error text go to stderr.
  • --quiet on list/get commands that return ids (jobs, models, qa, task).
  • --yes on cancel, eval, and sync. Non-TTY sessions require --yes (exit 2) instead of hanging on a prompt. --dry-run never prompts.
  • --dry-run on cancel, deploy, eval, init, models fork/head, qa run, and sync tasks.
  • Structured errors: {error, message, input, suggestion, transient, existing_id} with exit codes 0 success, 1 failure, 2 usage, 3 not found, 4 permission, 5 conflict. HTTP 404/401-403/409 map to 3/4/5; 429/5xx are marked transient.
  • Noun-verb verbs: hud jobs list|get|cancel, hud trace get, hud auth login|set. Existing invocations stay as aliases (hud jobs, hud jobs <id>, hud cancel, hud login, hud set).
  • Idempotency: hud models fork --if-not-exists; hud init on a non-empty directory exits 5 (conflict).
  • hud deploy --all --json emits one summary document (succeeded, failed, dry_run, environments). Per-env progress and error text go to stderr. Failed envs keep build_id / status / registry_id / name (and error when the env aborted). Per-env abort() cannot write extra JSON objects.

Human table output remains the default. Agents opt in with --json. Existing JSON payload shapes for jobs/models/qa/trace are unchanged. CLI reference in docs/v6/reference/cli.mdx is updated.

Why

AI agents consume CLIs through --help, stdout, and $?. The previous surface mixed tables with ad-hoc JSON, used 0/1 for almost everything, and could hang on confirmation prompts in non-TTY sessions. One shared contract is easier for agents than per-command parsing.

Impact

Scripts that already pass --json keep the same payload shapes. New flags are opt-in except:

  • Non-interactive cancel/eval/sync now require --yes (or they exit 2 instead of prompting). --dry-run skips confirmation.
  • hud init on a non-empty directory is now a conflict (exit 5) rather than a generic failure.
  • Missing API key on a single command exits 4 (permission_denied). hud deploy --all with no API key still writes one summary and exits 1 if any env failed.
  • hud deploy --all --json is a single JSON object, not concatenated per-env documents.

No server-side batch job delete was added; the API has no bulk selector. hud serve is long-running and does not take --json. hud login --quiet still means “don’t open a browser.”

Validation

Fail-to-pass: hud/cli/tests/test_agent_cli.py and hud/cli/utils/tests/test_output.py cover JSON-on-stdout vs messages-on-stderr, exit codes 2–5, --quiet ids, --dry-run, non-TTY --yes requirement, unknown-token dispatch (hud jobs <id>), and structured abort() errors.

  • Help assertions strip ANSI so they pass when CI forces color (Python 3.11 test job).
  • test_cancel_dry_run_json_skips_confirmation: non-TTY hud cancel --dry-run --json without --yes exits 0 with one plan JSON object.
  • Deploy --all --json tests assert a single json.loads document, failed-env details in environments, and a missing API key still one document with permission_denied per env.
  • require_api_key narrows settings.api_key to str after abort so ty accepts the return (invalid-return-type on hud/cli/utils/api.py).

Pass-to-pass: uv run pytest hud/cli -q (224 passed). uv run ruff format . --check and uv run ruff check . pass. uv run --extra dev --extra train --extra modal --extra daytona ty check --error-on-warning passes.

Open in Web Open in Cursor 

cursoragent and others added 2 commits August 31, 2026 14:00
Introduce a shared CLI I/O contract (JSON on stdout, structured errors,
exit codes 0-5, TTY confirmation policy) and apply it across API commands.
Add explicit jobs/trace verbs with backward-compatible aliases, --json/--quiet/
--yes/--dry-run where they apply, and update the v6 CLI reference.

Co-authored-by: cody <cody@hud.ai>
Typer treated `hud jobs list --json` as a job id plus a bogus command
because the group callback took a positional. Route unknown first tokens
to `get` instead. Remember `--json` in a contextvar so structured errors
still reach stdout when Click has torn down its context (CliRunner).

Co-authored-by: cody <cody@hud.ai>
@codyadam
codyadam marked this pull request as ready for review August 31, 2026 14:09

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Dry-run cancel still requires confirmation
    • Moved confirm_or_abort calls after the dry-run early return so --dry-run works without --yes in non-interactive environments.
  • ✅ Fixed: Deploy-all JSON writes multiple documents
    • Changed deploy_all to pass json_output=False to individual deploy_environment calls, emitting only the summary JSON document.

You can send follow-ups to the cloud agent here.

Comment thread hud/cli/cancel.py
Comment thread hud/cli/deploy.py Outdated
cursoragent and others added 4 commits August 31, 2026 14:19
…or deploy --all

- cancel: move confirm_or_abort after dry-run check so --dry-run works
  without --yes in non-interactive environments
- deploy: suppress per-environment JSON when deploy_all is invoked, only
  emit the summary JSON document to stdout
…eable

Strip ANSI from CLI help assertions so CI color output still matches
--json. Keep cancel --dry-run from prompting (already on this branch)
and cover it with a non-TTY test. Emit one deploy --all --json summary
that includes per-environment plans instead of concatenated documents.

Co-authored-by: cody <cody@hud.ai>
Passing json_output=False is not enough: wants_json still sees the
parent --json contextvar. deploy_all now sets emit_result=False so
stdout is one summary document.

Co-authored-by: cody <cody@hud.ai>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Failed deploy details are discarded
    • When emit_result=False, deploy_environment now returns the payload before checking result.success, allowing deploy_all to include full details (build_id, status, registry_id, name) in the environments list.
  • ✅ Fixed: Deploy-all JSON still duplicated
    • Moved require_api_key call to deploy_all before the loop and skipped it in deploy_environment when emit_result=False, ensuring only one JSON document is written.

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 2756fd6. Configure here.

Comment thread hud/cli/deploy.py
Comment thread hud/cli/deploy.py Outdated
cursoragent and others added 4 commits August 31, 2026 14:33
…deploy --all

Bug 1: When deploy_environment failed, it raised typer.Exit(1) before
returning the payload. Now when emit_result=False (called from deploy_all),
the payload is returned even on failure so deploy_all can include the
full details (build_id, status, registry_id, name) in the environments list.

Bug 2: require_api_key calls abort() which emits JSON based on the global
--json flag. When called from deploy_all, this caused duplicate JSON output.
Now deploy_all calls require_api_key once before the loop, and
deploy_environment skips the check when emit_result=False.
Return failed per-env payloads instead of Exit-and-catch so the
environments list keeps build_id, status, registry_id, and name.
Suppress per-env abort JSON (including a missing API key) while
aggregating so stdout is only the summary document.

Co-authored-by: cody <cody@hud.ai>
Check settings.api_key locally so ty can treat the return as str,
and keep missing_api_key_error as a CliError factory.

Co-authored-by: cody <cody@hud.ai>
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.

2 participants