Skip to content

fix(cli): keep a rejected --concurrency value off stdout - #201

Open
ethanj wants to merge 1 commit into
mainfrom
fix/quickstart-json-stdout
Open

fix(cli): keep a rejected --concurrency value off stdout#201
ethanj wants to merge 1 commit into
mainfrom
fix/quickstart-json-stdout

Conversation

@ethanj

@ethanj ethanj commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Fixes #191, reported by @graysoncooper with a diagnosis that pointed straight at the cause.

What

quickstart --json promises a single parseable envelope on stdout. It did not deliver one when --concurrency was invalid:

$ llmwiki quickstart src.md --json --no-open --concurrency eight
! --concurrency value "eight" is not a positive integer; ignoring it.
{
  "version": 1,
  ...

Exit code is 0, so an automated caller sees success and then fails to parse.

Why it happened

The value is parsed while the CLI assembles the command's arguments, at cli.ts:389, and quiet mode is enabled later inside the command body at quickstart.ts:147. Argument expressions are evaluated before the callee runs, so quiet mode was never early enough to suppress it. Moving the setQuiet call earlier cannot fix this, because there is no point inside the command that precedes its own arguments.

The channel was the real problem. stdout is the data channel, and a complaint about invalid input belongs on stderr. output.note already exists for exactly that and has eighteen callers, so the fix is to use it.

That also fixes compile, refresh and watch, which take the same flag, and it keeps the warning visible rather than trading a corrupt envelope for a silent one.

Tests

  • test/quickstart-json-stdout.test.ts drives the real CLI through dist/ and asserts both halves of the contract: stdout parses whole, and stderr still names the rejected value. No credentials needed, since the provider failure is reported inside the envelope, which is the shape a caller has to read anyway.
  • The existing unit test asserted the rejection appeared on stdout, so it encoded the defect. It now pins the channel split instead.

Both tests were mutation-tested against the committed fix. Restoring the stdout write turns both red, and deleting the diagnostic altogether also turns both red, so a fix that simply silenced the warning would not pass.

The two other warnings in concurrency.ts are left alone deliberately: they run inside the command, where quiet mode governs them correctly.

`quickstart --json` promises a single parseable envelope on stdout. It did not
deliver one when `--concurrency` was invalid: the value is parsed while the CLI
assembles the command's arguments, so the rejection was written before
`quickstartCommand` could enable quiet mode, landing ahead of the envelope. The
command still exited 0, so an automated caller read a successful-looking stream
that failed to parse.

Quiet mode could never have suppressed this. Argument expressions are evaluated
before the callee's body runs, so nothing the command does on entry is early
enough. The channel is the problem, not the timing: stdout is the data channel,
and a diagnostic about invalid input belongs on stderr. `output.note` already
serves exactly that purpose and has eighteen callers.

Routing it there fixes every command that takes the flag, not just quickstart,
and keeps the warning visible rather than trading a corrupt envelope for a
silent one.

The existing unit test asserted the rejection appeared on stdout, so it encoded
the defect and had to change. It now pins both halves of the contract: stderr
carries the message, stdout stays empty.
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.

quickstart --json writes a warning to stdout before the JSON envelope on invalid --concurrency

1 participant