Summary
Several gbrain subcommands gate their primary output on process.stdout.isTTY. When the CLI is invoked from any non-interactive context — an exec/subprocess capture, a sub-agent, a cron job, or a plain pipe — these commands silently switch to a different (or empty) output path instead of printing the human-readable result. Callers that expect the normal stdout get back nothing (the (no output) symptom), which looks like the CLI is "eating stdout."
We hit this repeatedly driving gbrain from an automation harness (no TTY): commands that work perfectly when run by hand in a terminal return empty when captured.
Affected (isTTY-gated output paths)
From src/ on gbrain 0.42.8.0 (HEAD 0bfe0d0c):
src/commands/eval-cross-modal.ts:254-255,345 — isTTY() flips default cycles (3 in TTY, 1 otherwise) and gates progress output.
src/commands/eval-takes-quality.ts:141,211 — process.stdout.isTTY ? 3 : 1 cycle default; non-TTY path emits differently.
src/commands/reindex-code.ts:457-458 — const isTTY = ...; if (!isTTY || json) { ... } switches the entire output branch.
src/commands/jobs-watch.ts:201-202,209 — const json = opts.json || !isTTY; forces JSON-only when not a TTY, suppressing the human view.
Repro
# Terminal (TTY): prints the expected human output
gbrain jobs watch
# Non-TTY (the automation case): output silently changes / disappears
gbrain jobs watch < /dev/null | cat # forced into json/empty branch
gbrain reindex-code 2>/dev/null | cat # !isTTY branch, different output
The core read commands (get/list/search/query) are fine — they emit through formatResult + console.log unconditionally. The bug is specifically the isTTY-gated commands.
Expected
A CLI's stdout should not depend on whether stdout is a terminal. TTY detection is fine for cosmetics (spinners, ANSI colors, live-refresh redraws, interactive cycle defaults) but must not change what data is printed. Non-interactive callers should get the same result payload (or a documented, stable --json), never an empty/altered stream by default.
Suggested fix
- Decouple "is this a TTY?" (controls spinner/ANSI/live-redraw only) from "what do we print?" (always emit the result).
- For commands where non-TTY currently implies JSON (
jobs-watch), still emit a final human-readable summary to stdout unless --json is explicitly passed, rather than inferring json = !isTTY.
- Make interactive-only defaults (e.g.
cycles = isTTY ? 3 : 1) explicit via flags so non-TTY runs are not silently degraded.
- Add a non-TTY smoke test (
command < /dev/null | cat asserts non-empty stdout) for each affected command.
Env
- gbrain 0.42.8.0, HEAD 0bfe0d0
- bun runtime, Linux, invoked via subprocess (no controlling TTY)
Summary
Several
gbrainsubcommands gate their primary output onprocess.stdout.isTTY. When the CLI is invoked from any non-interactive context — an exec/subprocess capture, a sub-agent, a cron job, or a plain pipe — these commands silently switch to a different (or empty) output path instead of printing the human-readable result. Callers that expect the normal stdout get back nothing (the(no output)symptom), which looks like the CLI is "eating stdout."We hit this repeatedly driving gbrain from an automation harness (no TTY): commands that work perfectly when run by hand in a terminal return empty when captured.
Affected (isTTY-gated output paths)
From
src/ongbrain 0.42.8.0(HEAD0bfe0d0c):src/commands/eval-cross-modal.ts:254-255,345—isTTY()flips defaultcycles(3 in TTY, 1 otherwise) and gates progress output.src/commands/eval-takes-quality.ts:141,211—process.stdout.isTTY ? 3 : 1cycle default; non-TTY path emits differently.src/commands/reindex-code.ts:457-458—const isTTY = ...; if (!isTTY || json) { ... }switches the entire output branch.src/commands/jobs-watch.ts:201-202,209—const json = opts.json || !isTTY;forces JSON-only when not a TTY, suppressing the human view.Repro
The core read commands (
get/list/search/query) are fine — they emit throughformatResult+console.logunconditionally. The bug is specifically the isTTY-gated commands.Expected
A CLI's stdout should not depend on whether stdout is a terminal. TTY detection is fine for cosmetics (spinners, ANSI colors, live-refresh redraws, interactive cycle defaults) but must not change what data is printed. Non-interactive callers should get the same result payload (or a documented, stable
--json), never an empty/altered stream by default.Suggested fix
jobs-watch), still emit a final human-readable summary to stdout unless--jsonis explicitly passed, rather than inferringjson = !isTTY.cycles = isTTY ? 3 : 1) explicit via flags so non-TTY runs are not silently degraded.command < /dev/null | catasserts non-empty stdout) for each affected command.Env