Skip to content

feat(cli): define every command through one factory and test them all as an agent - #6616

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

jrusso1020 merged 3 commits into
mainfrom
fix/cli-agent-mode

Conversation

@jrusso1020

@jrusso1020 jrusso1020 commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

What

First of four stacked PRs that make the CLI safe and quiet when a coding agent or CI runs it. This one adds the shared pieces the rest build on:

  • defineCommand in packages/cli/src/utils/defineCommand.ts, the one way to define a command. Every command file now imports it instead of citty's. A lint rule bans importing defineCommand from citty anywhere else.
  • isUnattended() in packages/cli/src/utils/attendedTerminal.ts: true when stdin or stdout is not a TTY, under CI, when isAgentDriven() (new, next to detectAgentRuntime in telemetry/agent_runtime.ts) finds an agent running the shell, or with HYPERFRAMES_AGENT=1. Color turns off when it is true.
  • Usage errors exit 2: unknown flags, extra positionals, citty's own parse errors (missing argument, bad enum value, unknown or missing subcommand), and the hand-written argument checks in lambda, cloudrun, lambda policies, capture, remove-background, init --resolution, tts --lang and cloud render. An unknown flag now also prints Run 'hyperframes <cmd> --help' to see its flags.
  • An agent-mode contract test (packages/cli/src/cli.agentMode.test.ts).

Why

Agents run the CLI without a terminal. Today some commands prompt anyway, nags print on every run, and usage errors exit 1 like real failures, so an agent cannot tell "I typed it wrong" from "it broke". Fixing those one command at a time does not stay fixed; a factory plus a test that walks every registered command does.

Related work

Stack (each PR targets the one before it):

  1. this PR: factory, isUnattended(), exit 2 for usage errors, contract test
  2. publish/upgrade prompts and background auto-install
  3. help [cmd], suggestions, nag throttle
  4. JSON error envelope, --json only where supported, contributor rules

How

  • isAgentDriven() uses the same vendor rules as telemetry, but only the markers an agent sets on the shells it runs. TERM_PROGRAM=cursor, TERM_PROGRAM=windsurf and Replit's REPL_ID/REPLIT_USER also mark a person typing in that editor's terminal or workspace, so they do not count; CURSOR_AGENT does.

  • defineCommand wraps citty's and records each command it builds in a WeakSet. The runtime guards stay where they were (the registry wrapper in command-failure-tracking.ts), because they need the command path (cloud render) that only the registry knows.

  • The contract test reads commandLoaders from cli.ts (the same way cli.commands.test.ts does), imports each module, and walks nested subCommands. A new command is covered as soon as it is registered. For each of the 130 command paths it checks the command came from the factory, then spawns the CLI like an agent (no TTY, stdin closed, CI=1, CLAUDECODE=1, an empty HOME, a 15 s timeout) for --help and for an unknown flag. It asserts: no hang, no ANSI escapes, no prompt text, no update or skills nag lines, help exits 0, the unknown flag exits 2 with nothing on stdout.

  • CliUsageError now defaults to exit code 2 and can carry a hint. When a usage error has a hint, the boundary prints the message and the hint instead of the full usage text.

From source, preview will not load until the studio packages are built, and the CLI test jobs do not build them, so the test spawns preview only when they are built; it is still checked for coming from the factory.

Behavior change for people at a terminal: usage errors exit 2 instead of 1. Two existing tests that pinned exit 1 for a usage error were updated.

Test plan

  • Unit tests added/updated: cli.agentMode.test.ts (262 cases), attendedTerminal.test.ts
  • bun run --filter @hyperframes/cli test (unit project): 421 files pass
  • typecheck, oxlint, oxfmt --check, fallow audit, comment ratchet and citation checks pass locally
  • Manual testing performed (below)
  • Documentation updated: exit codes in docs/packages/cli.mdx
  • Comments follow CONTRIBUTING.md "Comments"

Manual run of the built CLI from an empty directory, CLAUDECODE=1, stdin from /dev/null:

$ hyperframes render --not-a-flag </dev/null
# before: exit 1, stderr "Unknown flag: --not-a-flag"
# after:  exit 2, stdout empty, stderr:
Unknown flag: --not-a-flag
Run 'hyperframes render --help' to see its flags.

🤖 Generated with Claude Code

… as an agent

Every command now comes from defineCommand in utils/defineCommand.ts. A lint
rule bans importing defineCommand from citty anywhere else, and a new contract
test walks the command registry in cli.ts, checks each command was built by the
factory, and runs it the way a coding agent does: no terminal, stdin closed,
CI=1, CLAUDECODE=1 and a home folder of its own.

Usage errors now exit 2: unknown flags, extra positionals, and citty's own
parse errors (missing argument, bad value, unknown or missing subcommand).
An unknown flag names the help command instead of printing nothing else.

isUnattended() in utils/attendedTerminal.ts is the one answer to "can a person
respond here". It is true without a TTY on stdin or stdout, under CI (any value
except "false" or "0"), when the existing agent detection finds an agent, or
with HYPERFRAMES_AGENT=1. Color is off when it is true.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@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, 4:22 AM

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

@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 271 (base branch 271), smooth 126 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)

…packages are built

From source, preview refuses to load until the studio workspace packages are
built, and the CLI test jobs do not build them. preview is still checked for
being defined with the shared factory.

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

@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 · 1 important · 0 nits

The factory, the exit-2 boundary and the contract test look right. The agent-mode suite plus attendedTerminal, ui/, autoUpdate and updateCheck pass locally at the top of the stack (480 tests). The blocker is how isUnattended() decides someone is an agent (inline). It only costs color here, but #6617 builds its prompt refusals on it.

— Genesis

Comment thread docs/packages/cli.mdx
`--non-interactive`, and `catalog --human-friendly` opens a picker. Nothing else
prompts, apart from confirmations you can skip with `--yes` or `--no-confirm`.

**Exit codes.** `0` means the command succeeded, `1` means it ran and failed, and

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.

important: This promises exit 2 for "a missing or invalid value", but the hand-written argument checks still failCommand() (exit 1). Under --json, #6622 then codes them runtime_error. Verified: hyperframes lambda progress → exit 1 with [lambda progress] usage: …, and capture --json → exit 1 with runtime_error "Missing URL…". The same pattern appears in lambda.ts (5 usage: sites), cloudrun.ts (4), lambda/policies.ts:266, remove-background.ts:100-110 (missing input or --output) and capture.ts:166 (--local-root requires --local-file). An agent still can't tell "I typed it wrong" from "it broke" for these. Throw CliUsageError at those sites, or narrow the doc to parser-level errors.

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.

Partly fixed in d02c0e7. The 23 sites you converted exit 2, which I checked for lambda progress, capture and remove-background. Four missing-argument checks still failCommand(). hyperframes lambda render ./x exits 1, and its --json output is runtime_error "--width and --height are required." The same check is at lambda.ts:378 (render-batch) and cloudrun.ts:546 and :652. The invalid-value checks the doc line also covers still exit 1 too: the --fps must be one of checks (lambda.ts:327/:384, cloudrun.ts:529), parsePositiveInt in both files (it throws a plain Error), capture.ts:26, feedback --rating 99 and benchmark --runs. Not blocking. Either convert them or narrow the doc to missing arguments.

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.

Follow-up in #6639: every site listed here now exits 2 with usage_error (checked as an agent at 25132f4). I'll resolve this thread once that PR lands.

…issing arguments

isUnattended() used the telemetry fingerprint detectAgentRuntime(), which also
matches a person typing in Cursor's or Windsurf's terminal (TERM_PROGRAM) and
any Replit shell (REPL_ID, REPLIT_USER). It now uses isAgentDriven(), which
keeps only markers an agent sets on the shells it runs: CURSOR_AGENT counts,
TERM_PROGRAM=cursor, TERM_PROGRAM=windsurf and the Replit variables do not.

The hand-written argument checks in lambda, cloudrun, lambda policies,
capture, remove-background, init (--resolution), tts (--lang) and cloud render
(--idempotency-key, --dry-run) exited 1. They are usage errors and now exit 2.

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

@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, 1 open (important, not blocking), 0 new · 625bd9c → d02c0e7

— Genesis

@jrusso1020
jrusso1020 added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit 22e6da0 Oct 28, 2026
118 of 119 checks passed
@jrusso1020
jrusso1020 deleted the fix/cli-agent-mode branch October 28, 2026 07:08
@heygengenesis heygengenesis Bot mentioned this pull request Oct 28, 2026
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