From 8724e6ba04ddf3be5dffac574e096987b8430389 Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Sun, 15 Feb 2026 18:17:52 +0530 Subject: [PATCH 1/3] Add AI agent detection to auto-switch to raw output Switch from JSON to raw error format for AI agents. The raw format outputs one error per line as file:line:message, which saves ~49% tokens compared to JSON for typical error output (and 100% for zero-error runs. When an agent is detected, progress bar output is fully suppressed (equivalent to --no-progress), eliminating redraw noise that wastes tokens in agent context windows. Token comparison (3 errors across 2 files): - JSON: ~185 tokens (678 chars) with structural overhead per error - Raw: ~95 tokens (295 chars) with just file:line:message per line Zero errors: - JSON: ~18 tokens for empty structure - Raw: 0 tokens (no output) Explicit --error-format=table or config errorFormat: table always takes priority over agent detection. --- src/Command/AnalyseCommand.php | 12 ++- .../AgentDetectedErrorFormatter.php | 51 +++++++++++ src/Command/ErrorsConsoleStyle.php | 33 +++++++- .../AgentDetectedErrorFormatterTest.php | 84 +++++++++++++++++++ 4 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 src/Command/ErrorFormatter/AgentDetectedErrorFormatter.php create mode 100644 tests/PHPStan/Command/ErrorFormatter/AgentDetectedErrorFormatterTest.php diff --git a/src/Command/AnalyseCommand.php b/src/Command/AnalyseCommand.php index ee40d2b0d7..0e56aa47a2 100644 --- a/src/Command/AnalyseCommand.php +++ b/src/Command/AnalyseCommand.php @@ -5,6 +5,7 @@ use OndraM\CiDetector\CiDetector; use Override; use PHPStan\Analyser\InternalError; +use PHPStan\Command\ErrorFormatter\AgentDetectedErrorFormatter; use PHPStan\Command\ErrorFormatter\BaselineNeonErrorFormatter; use PHPStan\Command\ErrorFormatter\BaselinePhpErrorFormatter; use PHPStan\Command\ErrorFormatter\ErrorFormatter; @@ -235,11 +236,20 @@ protected function execute(InputInterface $input, OutputInterface $output): int $errorFormat = $inceptionResult->getContainer()->getParameter('errorFormat'); } + $container = $inceptionResult->getContainer(); + + if ($errorFormat === null) { + /** @var AgentDetectedErrorFormatter $agentFormatter */ + $agentFormatter = $container->getByType(AgentDetectedErrorFormatter::class); + if ($agentFormatter->isAgentDetected()) { + $errorFormat = 'raw'; + } + } + if ($errorFormat === null) { $errorFormat = 'table'; } - $container = $inceptionResult->getContainer(); $errorFormatterServiceName = sprintf('errorFormatter.%s', $errorFormat); if (!$container->hasService($errorFormatterServiceName)) { $errorOutput->writeLineFormatted(sprintf( diff --git a/src/Command/ErrorFormatter/AgentDetectedErrorFormatter.php b/src/Command/ErrorFormatter/AgentDetectedErrorFormatter.php new file mode 100644 index 0000000000..5d42b4082e --- /dev/null +++ b/src/Command/ErrorFormatter/AgentDetectedErrorFormatter.php @@ -0,0 +1,51 @@ +rawErrorFormatter->formatErrors($analysisResult, $output); + } + +} diff --git a/src/Command/ErrorsConsoleStyle.php b/src/Command/ErrorsConsoleStyle.php index 18301f5ab8..c7ecda1de7 100644 --- a/src/Command/ErrorsConsoleStyle.php +++ b/src/Command/ErrorsConsoleStyle.php @@ -13,9 +13,12 @@ use Symfony\Component\Console\Terminal; use function array_unshift; use function explode; +use function getenv; use function implode; +use function is_string; use function sprintf; use function strlen; +use function trim; use const DIRECTORY_SEPARATOR; final class ErrorsConsoleStyle extends SymfonyStyle @@ -29,10 +32,13 @@ final class ErrorsConsoleStyle extends SymfonyStyle private ?bool $isCiDetected = null; + private ?bool $isAgentDetected = null; + public function __construct(InputInterface $input, OutputInterface $output) { parent::__construct($input, $output); - $this->showProgress = $input->hasOption(self::OPTION_NO_PROGRESS) && !(bool) $input->getOption(self::OPTION_NO_PROGRESS); + $showProgress = $input->hasOption(self::OPTION_NO_PROGRESS) && !(bool) $input->getOption(self::OPTION_NO_PROGRESS); + $this->showProgress = $showProgress && !$this->isAgentDetected(); } private function isCiDetected(): bool @@ -45,6 +51,26 @@ private function isCiDetected(): bool return $this->isCiDetected; } + private function isAgentDetected(): bool + { + if ($this->isAgentDetected === null) { + $aiAgent = getenv('AI_AGENT'); + $this->isAgentDetected = (is_string($aiAgent) && trim($aiAgent) !== '') + || getenv('CURSOR_TRACE_ID') !== false + || getenv('CURSOR_AGENT') !== false + || getenv('GEMINI_CLI') !== false + || getenv('CODEX_SANDBOX') !== false + || getenv('AUGMENT_AGENT') !== false + || getenv('OPENCODE_CLIENT') !== false + || getenv('OPENCODE') !== false + || getenv('CLAUDECODE') !== false + || getenv('CLAUDE_CODE') !== false + || getenv('REPL_ID') !== false; + } + + return $this->isAgentDetected; + } + /** * @param string[] $headers * @param string[][] $rows @@ -95,9 +121,10 @@ public function createProgressBar(int $max = 0): ProgressBar } $ci = $this->isCiDetected(); - $this->progressBar->setOverwrite(!$ci); + $agent = $this->isAgentDetected(); + $this->progressBar->setOverwrite(!$ci && !$agent); - if ($ci) { + if ($ci || $agent) { $this->progressBar->minSecondsBetweenRedraws(15); $this->progressBar->maxSecondsBetweenRedraws(30); } elseif (DIRECTORY_SEPARATOR === '\\') { diff --git a/tests/PHPStan/Command/ErrorFormatter/AgentDetectedErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/AgentDetectedErrorFormatterTest.php new file mode 100644 index 0000000000..8e23df3a9c --- /dev/null +++ b/tests/PHPStan/Command/ErrorFormatter/AgentDetectedErrorFormatterTest.php @@ -0,0 +1,84 @@ +assertFalse($formatter->isAgentDetected()); + } + + public function testIsAgentDetectedReturnsTrueWithAiAgent(): void + { + putenv('AI_AGENT=test'); + $formatter = new AgentDetectedErrorFormatter(new RawErrorFormatter()); + $this->assertTrue($formatter->isAgentDetected()); + } + + public function testIsAgentDetectedReturnsTrueWithClaudeCode(): void + { + putenv('CLAUDE_CODE=1'); + $formatter = new AgentDetectedErrorFormatter(new RawErrorFormatter()); + $this->assertTrue($formatter->isAgentDetected()); + } + + public function testFormatErrorsProducesRawOutput(): void + { + $formatter = new AgentDetectedErrorFormatter(new RawErrorFormatter()); + + $exitCode = $formatter->formatErrors( + $this->getAnalysisResult(1, 0), + $this->getOutput(), + ); + + $this->assertSame(1, $exitCode); + $this->assertSame( + '/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:4:Foo' . "\n", + $this->getOutputContent(), + ); + } + + public function testFormatErrorsNoErrors(): void + { + $formatter = new AgentDetectedErrorFormatter(new RawErrorFormatter()); + + $exitCode = $formatter->formatErrors( + $this->getAnalysisResult(0, 0), + $this->getOutput(), + ); + + $this->assertSame(0, $exitCode); + $this->assertSame('', $this->getOutputContent()); + } + +} From ee0abcecd83c088aba59f3698c51cf3ff2687003 Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Sun, 15 Feb 2026 22:20:46 +0530 Subject: [PATCH 2/3] Refactor agent detection into shared AgentDetector class - Extract duplicated detection logic from AgentDetectedErrorFormatter and ErrorsConsoleStyle into AgentDetector with static method - Drop unnecessary ErrorFormatter implementation and @api tag - Replace REPL_ID with more specific REPLIT_AGENT env var - Fix test tearDown to clean all env vars --- src/Command/AgentDetector.php | 31 +++++++ src/Command/AnalyseCommand.php | 9 +- .../AgentDetectedErrorFormatter.php | 51 ----------- src/Command/ErrorsConsoleStyle.php | 29 +------ tests/PHPStan/Command/AgentDetectorTest.php | 78 +++++++++++++++++ .../AgentDetectedErrorFormatterTest.php | 84 ------------------- 6 files changed, 113 insertions(+), 169 deletions(-) create mode 100644 src/Command/AgentDetector.php delete mode 100644 src/Command/ErrorFormatter/AgentDetectedErrorFormatter.php create mode 100644 tests/PHPStan/Command/AgentDetectorTest.php delete mode 100644 tests/PHPStan/Command/ErrorFormatter/AgentDetectedErrorFormatterTest.php diff --git a/src/Command/AgentDetector.php b/src/Command/AgentDetector.php new file mode 100644 index 0000000000..a9fec967cf --- /dev/null +++ b/src/Command/AgentDetector.php @@ -0,0 +1,31 @@ +getContainer(); - if ($errorFormat === null) { - /** @var AgentDetectedErrorFormatter $agentFormatter */ - $agentFormatter = $container->getByType(AgentDetectedErrorFormatter::class); - if ($agentFormatter->isAgentDetected()) { - $errorFormat = 'raw'; - } + if ($errorFormat === null && AgentDetector::isAgentDetected()) { + $errorFormat = 'raw'; } if ($errorFormat === null) { diff --git a/src/Command/ErrorFormatter/AgentDetectedErrorFormatter.php b/src/Command/ErrorFormatter/AgentDetectedErrorFormatter.php deleted file mode 100644 index 5d42b4082e..0000000000 --- a/src/Command/ErrorFormatter/AgentDetectedErrorFormatter.php +++ /dev/null @@ -1,51 +0,0 @@ -rawErrorFormatter->formatErrors($analysisResult, $output); - } - -} diff --git a/src/Command/ErrorsConsoleStyle.php b/src/Command/ErrorsConsoleStyle.php index c7ecda1de7..43149240d9 100644 --- a/src/Command/ErrorsConsoleStyle.php +++ b/src/Command/ErrorsConsoleStyle.php @@ -13,12 +13,9 @@ use Symfony\Component\Console\Terminal; use function array_unshift; use function explode; -use function getenv; use function implode; -use function is_string; use function sprintf; use function strlen; -use function trim; use const DIRECTORY_SEPARATOR; final class ErrorsConsoleStyle extends SymfonyStyle @@ -32,13 +29,11 @@ final class ErrorsConsoleStyle extends SymfonyStyle private ?bool $isCiDetected = null; - private ?bool $isAgentDetected = null; - public function __construct(InputInterface $input, OutputInterface $output) { parent::__construct($input, $output); $showProgress = $input->hasOption(self::OPTION_NO_PROGRESS) && !(bool) $input->getOption(self::OPTION_NO_PROGRESS); - $this->showProgress = $showProgress && !$this->isAgentDetected(); + $this->showProgress = $showProgress && !AgentDetector::isAgentDetected(); } private function isCiDetected(): bool @@ -51,26 +46,6 @@ private function isCiDetected(): bool return $this->isCiDetected; } - private function isAgentDetected(): bool - { - if ($this->isAgentDetected === null) { - $aiAgent = getenv('AI_AGENT'); - $this->isAgentDetected = (is_string($aiAgent) && trim($aiAgent) !== '') - || getenv('CURSOR_TRACE_ID') !== false - || getenv('CURSOR_AGENT') !== false - || getenv('GEMINI_CLI') !== false - || getenv('CODEX_SANDBOX') !== false - || getenv('AUGMENT_AGENT') !== false - || getenv('OPENCODE_CLIENT') !== false - || getenv('OPENCODE') !== false - || getenv('CLAUDECODE') !== false - || getenv('CLAUDE_CODE') !== false - || getenv('REPL_ID') !== false; - } - - return $this->isAgentDetected; - } - /** * @param string[] $headers * @param string[][] $rows @@ -121,7 +96,7 @@ public function createProgressBar(int $max = 0): ProgressBar } $ci = $this->isCiDetected(); - $agent = $this->isAgentDetected(); + $agent = AgentDetector::isAgentDetected(); $this->progressBar->setOverwrite(!$ci && !$agent); if ($ci || $agent) { diff --git a/tests/PHPStan/Command/AgentDetectorTest.php b/tests/PHPStan/Command/AgentDetectorTest.php new file mode 100644 index 0000000000..9718f6dd89 --- /dev/null +++ b/tests/PHPStan/Command/AgentDetectorTest.php @@ -0,0 +1,78 @@ + */ + private const ENV_VARS = [ + 'AI_AGENT', + 'CURSOR_TRACE_ID', + 'CURSOR_AGENT', + 'GEMINI_CLI', + 'CODEX_SANDBOX', + 'AUGMENT_AGENT', + 'OPENCODE_CLIENT', + 'OPENCODE', + 'CLAUDECODE', + 'CLAUDE_CODE', + 'REPLIT_AGENT', + ]; + + #[Override] + protected function setUp(): void + { + foreach (self::ENV_VARS as $var) { + putenv($var); + } + } + + #[Override] + protected function tearDown(): void + { + foreach (self::ENV_VARS as $var) { + putenv($var); + } + } + + public function testReturnsFalseWithNoEnvVars(): void + { + $this->assertFalse(AgentDetector::isAgentDetected()); + } + + public function testReturnsTrueWithAiAgent(): void + { + putenv('AI_AGENT=test'); + $this->assertTrue(AgentDetector::isAgentDetected()); + } + + public function testReturnsFalseWithEmptyAiAgent(): void + { + putenv('AI_AGENT='); + $this->assertFalse(AgentDetector::isAgentDetected()); + } + + public function testReturnsTrueWithClaudeCode(): void + { + putenv('CLAUDE_CODE=1'); + $this->assertTrue(AgentDetector::isAgentDetected()); + } + + public function testReturnsTrueWithCursorTraceId(): void + { + putenv('CURSOR_TRACE_ID=abc'); + $this->assertTrue(AgentDetector::isAgentDetected()); + } + + public function testReturnsTrueWithReplitAgent(): void + { + putenv('REPLIT_AGENT=1'); + $this->assertTrue(AgentDetector::isAgentDetected()); + } + +} diff --git a/tests/PHPStan/Command/ErrorFormatter/AgentDetectedErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/AgentDetectedErrorFormatterTest.php deleted file mode 100644 index 8e23df3a9c..0000000000 --- a/tests/PHPStan/Command/ErrorFormatter/AgentDetectedErrorFormatterTest.php +++ /dev/null @@ -1,84 +0,0 @@ -assertFalse($formatter->isAgentDetected()); - } - - public function testIsAgentDetectedReturnsTrueWithAiAgent(): void - { - putenv('AI_AGENT=test'); - $formatter = new AgentDetectedErrorFormatter(new RawErrorFormatter()); - $this->assertTrue($formatter->isAgentDetected()); - } - - public function testIsAgentDetectedReturnsTrueWithClaudeCode(): void - { - putenv('CLAUDE_CODE=1'); - $formatter = new AgentDetectedErrorFormatter(new RawErrorFormatter()); - $this->assertTrue($formatter->isAgentDetected()); - } - - public function testFormatErrorsProducesRawOutput(): void - { - $formatter = new AgentDetectedErrorFormatter(new RawErrorFormatter()); - - $exitCode = $formatter->formatErrors( - $this->getAnalysisResult(1, 0), - $this->getOutput(), - ); - - $this->assertSame(1, $exitCode); - $this->assertSame( - '/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:4:Foo' . "\n", - $this->getOutputContent(), - ); - } - - public function testFormatErrorsNoErrors(): void - { - $formatter = new AgentDetectedErrorFormatter(new RawErrorFormatter()); - - $exitCode = $formatter->formatErrors( - $this->getAnalysisResult(0, 0), - $this->getOutput(), - ); - - $this->assertSame(0, $exitCode); - $this->assertSame('', $this->getOutputContent()); - } - -} From 14b96098b61acee2cfa0327e58f4cae2afcca857 Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Sun, 15 Feb 2026 22:23:21 +0530 Subject: [PATCH 3/3] Remove useless doc comment on test constant --- tests/PHPStan/Command/AgentDetectorTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/PHPStan/Command/AgentDetectorTest.php b/tests/PHPStan/Command/AgentDetectorTest.php index 9718f6dd89..0db56d22ad 100644 --- a/tests/PHPStan/Command/AgentDetectorTest.php +++ b/tests/PHPStan/Command/AgentDetectorTest.php @@ -9,7 +9,6 @@ class AgentDetectorTest extends TestCase { - /** @var list */ private const ENV_VARS = [ 'AI_AGENT', 'CURSOR_TRACE_ID',