Skip to content

[6.x] Quiet CLI output when running under an AI agent#11812

Open
alies-dev wants to merge 1 commit intovimeo:6.xfrom
alies-dev:feature/ai-friendly-output
Open

[6.x] Quiet CLI output when running under an AI agent#11812
alies-dev wants to merge 1 commit intovimeo:6.xfrom
alies-dev:feature/ai-friendly-output

Conversation

@alies-dev
Copy link
Copy Markdown
Contributor

@alies-dev alies-dev commented Apr 16, 2026

Why

AI coding agents (Claude Code, Cursor, Gemini CLI, Codex, Goose, Amp, etc.) run psalm and feed the combined output back into a language model. Every token spent on progress bars, ANSI colors, and \r-based scanning updates is wasted budget.

Related reading:

What changes

Three conservative defaults that kick in only when the existing flags are not set:

  1. Under an AI coding agent: default to VoidProgress and disable ANSI colors.
    • Explicit --long-progress still wins (escape hatch).
    • CI takes precedence for progress: persistent CI logs still want per-phase breadcrumbs for humans reviewing the build. If both CI=true and an AI marker are set, the run uses LongProgress. Colors still drop because most CI renderers treat ANSI inconsistently and agents reading the CI log still benefit.
  2. Non-TTY stderr (and not in CI): switch the default from DefaultProgress to the CI-flavored LongProgress. Previously, piping stderr to a file captured hundreds of \r-based "N / M..." scanning updates into one giant line. Now the CI path kicks in and we only emit per-phase output plus the compact /E analysis dots.
  3. NO_COLOR (https://no-color.org): disables ANSI colors on the stdout report when set to a non-empty value. Convention already respected by git, clang, cargo, etc.

No existing flag changes meaning. -m, --no-progress, and --long-progress all still win when set explicitly.

Agent detection

Each agent exports a presence marker into every spawned subprocess. The list mirrors the coverage of @vercel/detect-agent and shipfastlabs/agent-detector:

Variable Agent
CLAUDECODE, CLAUDE_CODE Claude Code (Anthropic)
CURSOR_AGENT, CURSOR_TRACE_ID Cursor Composer
GEMINI_CLI Gemini CLI (Google)
CODEX_SANDBOX, CODEX_THREAD_ID OpenAI Codex CLI
AUGMENT_AGENT Augment
CLINE_ACTIVE Cline
OPENCODE_CLIENT, OPENCODE OpenCode (sst/opencode)
AMP_CURRENT_THREAD_ID Amp (Sourcegraph)
TRAE_AI_SHELL_ID TRAE AI
COPILOT_CLI GitHub Copilot CLI
ANTIGRAVITY_AGENT Google Antigravity
PI_CODING_AGENT Pi coding agent
REPL_ID Replit
AI_AGENT Vendor-neutral convention
AGENT=goose Goose (value-gated to dodge collisions with unrelated AGENT usage)
/opt/.devin Devin (Cognition) filesystem sandbox marker

Using getenv() rather than $_SERVER matches the dominant pattern in the CLI code and survives strict variables_order settings. Help text speaks of "an AI coding agent" in the abstract rather than naming vendors, so adding the next marker is a one-line change.

Measurement

Ran on filament/4.x (real Laravel codebase, ~4.8K files, ~10.4K errors, cache cleared between runs):

Scenario stdout stderr total tokens (chars/4)
Baseline (main 6.x) 4,188,848 10,508 4,199,356 1,049,839
After, under AI agent 3,937,281 0 3,937,281 984,320
After, under AI agent in CI 3,937,281 2,818 3,940,099 985,024
After, piped (no AI env) 4,188,848 3,104 4,191,952 1,047,988
  • Under an AI agent: -262 KB / ~65K tokens (6.2%) per run, mostly from ANSI color removal on ~10K errors.
  • AI + CI: same stdout savings, plus ~3 KB of phase breadcrumbs restored on stderr for human log readers.
  • Piped run (non-AI): stderr shrinks 70% (10.5 KB -> 3.1 KB), driven entirely by dropping the \r-based scanning flood.
  • Output is otherwise byte-identical (same errors, same summaries, same paths, same snippets).

Files

  • src/Psalm/Internal/CliUtils.php: three static helpers (runningUnderAiAgent, streamIsInteractive, noColorRequested) plus a small envVarLooksTruthy internal.
  • src/Psalm/Internal/Cli/Psalm.php: main initProgress and initStdoutReportOptions pick quieter defaults. --help text updated.
  • src/Psalm/Internal/Cli/Psalter.php: same defaults for --alter, including the piped-stderr fallback.
  • src/Psalm/Internal/Cli/Refactor.php: VoidProgress and color-off under AI agent.
  • tests/EndToEnd/PsalmRunnerTrait.php: the Symfony Process constructor now receives an agentEnvVarsToUnset() map so the E2E harness doesn't silently swap to VoidProgress when an agent-using dev runs composer phpunit.

Behavior matrix

Context Progress Color
Interactive TTY, no env DefaultProgress (unchanged) on (unchanged)
Piped stderr, not CI LongProgress (was DefaultProgress) on
CI LongProgress (unchanged) on
AI agent detected (no CI) VoidProgress off
AI agent detected in CI LongProgress (CI wins) off
NO_COLOR=<non-empty> default off
--long-progress LongProgress (escape hatch honored) per other flags
--no-progress VoidProgress per other flags

BC note

Non-AI users who pipe stderr to a file will see the cleaner LongProgress format instead of the interleaved \r-based DefaultProgress. The old format was effectively unreadable off-TTY anyway, so this is meant as an improvement, but it is a visible change for anyone reading saved logs.


Note

Medium Risk
Moderate risk because it changes default CLI progress/color behavior based on environment detection (AI markers, NO_COLOR, non-TTY stderr), which could surprise users relying on previous log formats.

Overview
Makes Psalm/psalter/refactor CLI output quieter by default when it would otherwise be token/log noise: auto-disables ANSI color when NO_COLOR is set or when CliUtils::runningUnderAiAgent() detects an AI agent, and auto-disables progress under AI agents unless CI/--long-progress is in effect.

Improves non-interactive logging by switching to LongProgress when STDERR is not a TTY (and in CI), avoiding \r-based progress-bar flooding; help text is updated to document these auto-defaults. Adds CliUtils helpers (runningUnderAiAgent, streamIsInteractive, noColorRequested) and updates E2E test harness to unset agent-related env vars to keep output deterministic.

Reviewed by Cursor Bugbot for commit b4c7d74. Bugbot is set up for automated code reviews on this repo. Configure here.

@alies-dev alies-dev force-pushed the feature/ai-friendly-output branch from af92e18 to d0dfc6f Compare April 17, 2026 07:36
@alies-dev alies-dev changed the title Quiet CLI output when running under an AI agent (draft) [6.x] Quiet CLI output when running under an AI agent (draft) Apr 17, 2026
@alies-dev alies-dev force-pushed the feature/ai-friendly-output branch from d0dfc6f to f74c310 Compare April 17, 2026 07:45
@alies-dev alies-dev marked this pull request as ready for review April 17, 2026 07:54
@alies-dev alies-dev changed the title [6.x] Quiet CLI output when running under an AI agent (draft) [6.x] Quiet CLI output when running under an AI agent Apr 17, 2026
// which emits one line per phase transition.
$quiet_progress = $in_ci || !CliUtils::streamIsInteractive(STDERR);
if (isset($options['long-progress']) || $quiet_progress) {
$progress = new LongProgress($show_errors, $show_info, $quiet_progress);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI progress silently switches from DefaultProgress to LongProgress

Low Severity

The $quiet_progress variable conflates two concerns: choosing the progress class and setting its constructor parameter. When $in_ci is true, $quiet_progress becomes true, causing the condition isset($options['long-progress']) || $quiet_progress to select LongProgress instead of the previous DefaultProgress. For projects with >1500 files in CI (without explicit --long-progress), the analysis output changes from DefaultProgress's \r-based progress bar to LongProgress's /E dot output. The PR behavior matrix states "CI: LongProgress (unchanged)" but the old default was DefaultProgress.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f74c310. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive. Old 6.x behavior in CI was already LongProgress, not DefaultProgress — see Psalm.php:280-282 on 6.x which sets $options["long-progress"] = true unconditionally when $in_ci, so initProgress took the LongProgress branch (with in_ci=true, producing the /E dot output). My new code preserves that: $quiet_progress = $in_ci || !streamIsInteractive(STDERR) is true in CI, and the combined condition still selects LongProgress($show_errors, $show_info, true) — identical to before. No behavior change for projects >1500 files in CI.

@alies-dev alies-dev force-pushed the feature/ai-friendly-output branch 2 times, most recently from b7cc051 to 18843d1 Compare April 17, 2026 10:43
Comment thread src/Psalm/Internal/Cli/Refactor.php Outdated
Add auto-detection for AI-agent invocations, non-TTY stderr, and the
NO_COLOR convention so piped output is no longer dominated by progress
tokens the agent will never render.

- Under an AI agent: default to VoidProgress and drop ANSI colors.
  Detected via per-vendor presence markers for Claude Code, Cursor,
  Gemini CLI, OpenAI Codex, Augment, Cline, OpenCode, Amp, TRAE AI,
  GitHub Copilot CLI, Google Antigravity, Pi, Replit; the Vercel
  AI_AGENT vendor-neutral convention; a value-gated AGENT=goose
  check; and the /opt/.devin sandbox marker used by Devin.
- On non-TTY stderr (and not in CI), switch DefaultProgress to the
  CI-flavored LongProgress so we stop flushing \r-based 'N / M...'
  scanning updates into log files.
- Respect NO_COLOR (https://no-color.org) for the stdout report.

Explicit flags (--no-progress, --long-progress, -m) still win.

Measured on filament/4.x (clean cache, ~10.4K errors):
- Output under an AI agent: 4.20 MB -> 3.94 MB (saved ~260 KB /
  ~65K tokens), stderr drops from 10.5 KB to 0.
- Non-AI piped run: stderr drops from 10.5 KB to 3.1 KB.
@alies-dev alies-dev force-pushed the feature/ai-friendly-output branch from 18843d1 to b4c7d74 Compare April 17, 2026 11:25
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Reviewed by Cursor Bugbot for commit b4c7d74. Configure here.

: new DefaultProgress();
// CI takes precedence over AI detection, matching Psalm.php / Psalter.php.
$no_progress = isset($options['no-progress'])
|| (!$in_ci && CliUtils::runningUnderAiAgent());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--no-progress not in Refactor's valid options list

Medium Severity

The new code checks isset($options['no-progress']) but 'no-progress' was never added to $valid_long_options in Refactor.php. Since getopt() won't parse an unlisted option, this check is always false — dead code. Worse, if a user actually passes --no-progress as an escape hatch (as the PR intends), the argument validator earlier in the function rejects it as an "Unrecognised argument" and exits with an error. The option string 'no-progress' needs to be added to $valid_long_options for the escape hatch to function.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b4c7d74. Configure here.

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