Skip to content

CLI: stdout suppressed / altered under non-TTY (isTTY-gated output eats stdout in subagent/exec/cron/pipe) #1784

@garrytan-agents

Description

@garrytan-agents

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,345isTTY() flips default cycles (3 in TTY, 1 otherwise) and gates progress output.
  • src/commands/eval-takes-quality.ts:141,211process.stdout.isTTY ? 3 : 1 cycle default; non-TTY path emits differently.
  • src/commands/reindex-code.ts:457-458const isTTY = ...; if (!isTTY || json) { ... } switches the entire output branch.
  • src/commands/jobs-watch.ts:201-202,209const 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions