fix: Send CLI errors to stderr and respect the --json flag on the - #2
Merged
andrei-hasna merged 1 commit intoJul 31, 2026
Merged
Conversation
Send CLI errors to stderr and respect the --json flag on the failure path
The CLI in @hasna/workforce writes command failures to standard output instead of standard error, and always formats them as JSON even when the user did not ask for JSON.
Problem, in full. `src/cli/namespaces.ts` routes every CLI action through one wrapper:
function handle(json: boolean, fn: () => unknown): void {
try {
emit(fn(), json);
} catch (error) {
const env = toErrorEnvelope(error);
console.log(JSON.stringify({ ...env, error: env.message }));
process.exitCode = 1;
}
}
The success path honours the `json` flag through `emit(...)`. The failure path ignores it entirely and unconditionally `console.log`s a JSON envelope. Two things are wrong.
First, the error goes to stdout. `workforce member get does-not-exist > members.json` writes the error envelope into `members.json` and leaves stderr empty, so a shell pipeline or a calling agent that reads stdout receives a document that is neither the requested data nor visibly an error unless it inspects the keys. Every other diagnostic convention — and the reason stderr exists — is that failure text does not contaminate the data stream. The exit code is set correctly, so the information is available; it is simply written to the wrong stream.
Second, a human running `workforce member get bad-id` with no `--json` flag gets a raw JSON blob rather than a readable message, because the `json` parameter that `handle` already receives is not consulted in the catch branch.
The same top-level handler in `src/cli/index.tsx` has the identical shape: `main().catch((error) => { console.log(JSON.stringify({ code: "INTERNAL_ERROR", ... })); process.exit(1); })` — also stdout, also unconditionally JSON.
Expected behaviour after the fix. Error output goes to stderr in every case. When the JSON flag is set, stderr receives the JSON error envelope exactly as it is shaped today, so machine callers keep a parseable error. When the JSON flag is not set, stderr receives a readable one-line message (the envelope's message, and its suggestion when present). Standard output stays empty on failure. The exit code stays non-zero, unchanged. Success output on both paths is untouched.
Reproduce. Build the CLI and run a command that fails, redirecting the two streams separately: `workforce member get no-such-id > out.txt 2> err.txt`. Observe `out.txt` contains the JSON error envelope and `err.txt` is empty. Run the same command without `--json` and observe a JSON blob rather than a human message.
Acceptance criterion. A regression test, written first and confirmed failing against the current code, that invokes a failing CLI action with captured streams and asserts stdout is empty while stderr carries the error; a second case asserting the JSON flag yields a parseable envelope on stderr; a third asserting the non-JSON case yields a plain readable message; and a case asserting the exit code remains non-zero. Then make it pass.
Keep the change minimal. Change the failure path in `handle` and the top-level catch in `src/cli/index.tsx` only. Do not alter the success path, the error envelope's field names, `toErrorEnvelope`, or any service code.
X-Factory-Run: run_e95c5f31f339
X-Factory-Task: 8543be03-8947-4e22-a8a3-18bade59491c
andrei-hasna
deleted the
factory/8543be03-8947-4e22-a8a3-18bade59-3acc5ff5
branch
July 31, 2026 09:37
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Objective
Send CLI errors to stderr and respect the --json flag on the failure path
The CLI in @hasna/workforce writes command failures to standard output instead of standard error, and always formats them as JSON even when the user did not ask for JSON.
Problem, in full.
src/cli/namespaces.tsroutes every CLI action through one wrapper:The success path honours the
jsonflag throughemit(...). The failure path ignores it entirely and unconditionallyconsole.logs a JSON envelope. Two things are wrong.First, the error goes to stdout.
workforce member get does-not-exist > members.jsonwrites the error envelope intomembers.jsonand leaves stderr empty, so a shell pipeline or a calling agent that reads stdout receives a document that is neither the requested data nor visibly an error unless it inspects the keys. Every other diagnostic convention — and the reason stderr exists — is that failure text does not contaminate the data stream. The exit code is set correctly, so the information is available; it is simply written to the wrong stream.Second, a human running
workforce member get bad-idwith no--jsonflag gets a raw JSON blob rather than a readable message, because thejsonparameter thathandlealready receives is not consulted in the catch branch.The same top-level handler in
src/cli/index.tsxhas the identical shape:main().catch((error) => { console.log(JSON.stringify({ code: "INTERNAL_ERROR", ... })); process.exit(1); })— also stdout, also unconditionally JSON.Expected behaviour after the fix. Error output goes to stderr in every case. When the JSON flag is set, stderr receives the JSON error envelope exactly as it is shaped today, so machine callers keep a parseable error. When the JSON flag is not set, stderr receives a readable one-line message (the envelope's message, and its suggestion when present). Standard output stays empty on failure. The exit code stays non-zero, unchanged. Success output on both paths is untouched.
Reproduce. Build the CLI and run a command that fails, redirecting the two streams separately:
workforce member get no-such-id > out.txt 2> err.txt. Observeout.txtcontains the JSON error envelope anderr.txtis empty. Run the same command without--jsonand observe a JSON blob rather than a human message.Acceptance criterion. A regression test, written first and confirmed failing against the current code, that invokes a failing CLI action with captured streams and asserts stdout is empty while stderr carries the error; a second case asserting the JSON flag yields a parseable envelope on stderr; a third asserting the non-JSON case yields a plain readable message; and a case asserting the exit code remains non-zero. Then make it pass.
Keep the change minimal. Change the failure path in
handleand the top-level catch insrc/cli/index.tsxonly. Do not alter the success path, the error envelope's field names,toErrorEnvelope, or any service code.Verification
Run
run_e95c5f31f339· backendcodewith· task8543be03-8947-4e22-a8a3-18bade59491c🏭 Generated by @hasnaxyz/factory
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.