Skip to content

fix: Send CLI errors to stderr and respect the --json flag on the - #2

Merged
andrei-hasna merged 1 commit into
mainfrom
factory/8543be03-8947-4e22-a8a3-18bade59-3acc5ff5
Jul 31, 2026
Merged

fix: Send CLI errors to stderr and respect the --json flag on the#2
andrei-hasna merged 1 commit into
mainfrom
factory/8543be03-8947-4e22-a8a3-18bade59-3acc5ff5

Conversation

@andrei-hasna

@andrei-hasna andrei-hasna commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

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.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.logs 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.

Verification

  • policy source: base bc2e9a0 (immutable commit — agent-proof)
  • ⚠️ GATE-INTEGRITY: agent touched verify-bearing config — REVIEW (src/cli/index.tsx)
  • containment: env — allowlist env, non-login shell, run-scoped HOME (registry auth seeded for install)
  • install: pass
  • typecheck: pass
  • build: pass
  • test: pass
  • doctor (ci): ok — 11 checks passed (1 advisory)

Run run_e95c5f31f339 · backend codewith · task 8543be03-8947-4e22-a8a3-18bade59491c
🏭 Generated by @hasnaxyz/factory


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

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
andrei-hasna merged commit ff2c207 into main Jul 31, 2026
3 checks passed
@andrei-hasna
andrei-hasna deleted the factory/8543be03-8947-4e22-a8a3-18bade59-3acc5ff5 branch July 31, 2026 09:37
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