Repository navigation
feat(cli): define every command through one factory and test them all as an agent - #6616
Conversation
… 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]>
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Automations to automatically generate PRs for you. |
Edit accuracy: accurate 271 (base branch 271), smooth 126 of thoseThe gate passes. 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]>
There was a problem hiding this comment.
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
| `--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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
…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]>
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:
defineCommandinpackages/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 importingdefineCommandfrom citty anywhere else.isUnattended()inpackages/cli/src/utils/attendedTerminal.ts: true when stdin or stdout is not a TTY, under CI, whenisAgentDriven()(new, next todetectAgentRuntimeintelemetry/agent_runtime.ts) finds an agent running the shell, or withHYPERFRAMES_AGENT=1. Color turns off when it is true.lambda,cloudrun,lambda policies,capture,remove-background,init --resolution,tts --langandcloud render. An unknown flag now also printsRun 'hyperframes <cmd> --help' to see its flags.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):
isUnattended(), exit 2 for usage errors, contract testhelp [cmd], suggestions, nag throttle--jsononly where supported, contributor rulesHow
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=windsurfand Replit'sREPL_ID/REPLIT_USERalso mark a person typing in that editor's terminal or workspace, so they do not count;CURSOR_AGENTdoes.defineCommandwraps citty's and records each command it builds in aWeakSet. The runtime guards stay where they were (the registry wrapper incommand-failure-tracking.ts), because they need the command path (cloud render) that only the registry knows.The contract test reads
commandLoadersfromcli.ts(the same waycli.commands.test.tsdoes), imports each module, and walks nestedsubCommands. 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--helpand 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.CliUsageErrornow defaults to exit code 2 and can carry ahint. When a usage error has a hint, the boundary prints the message and the hint instead of the full usage text.From source,
previewwill not load until the studio packages are built, and the CLI test jobs do not build them, so the test spawnspreviewonly 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
cli.agentMode.test.ts(262 cases),attendedTerminal.test.tsbun run --filter @hyperframes/cli test(unit project): 421 files passtypecheck,oxlint,oxfmt --check,fallow audit, comment ratchet and citation checks pass locallydocs/packages/cli.mdxManual run of the built CLI from an empty directory,
CLAUDECODE=1, stdin from/dev/null:🤖 Generated with Claude Code