From 778a04175f999f188156e07ec1d3b1db8a891e59 Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Sun, 15 Feb 2026 18:17:52 +0530 Subject: [PATCH] Add AI agent detection to auto-switch to JSON output --- src/Command/AnalyseCommand.php | 12 ++- .../AgentDetectedErrorFormatter.php | 53 +++++++++++ src/Command/ErrorsConsoleStyle.php | 32 ++++++- .../AgentDetectedErrorFormatterTest.php | 87 +++++++++++++++++++ 4 files changed, 181 insertions(+), 3 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..46aabc87cd 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 = 'json'; + } + } + 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..e7c03008e7 --- /dev/null +++ b/src/Command/ErrorFormatter/AgentDetectedErrorFormatter.php @@ -0,0 +1,53 @@ +jsonErrorFormatter->formatErrors($analysisResult, $output); + } + +} diff --git a/src/Command/ErrorsConsoleStyle.php b/src/Command/ErrorsConsoleStyle.php index 18301f5ab8..f4778a7fcc 100644 --- a/src/Command/ErrorsConsoleStyle.php +++ b/src/Command/ErrorsConsoleStyle.php @@ -15,7 +15,11 @@ use function explode; use function implode; use function sprintf; +use function file_exists; +use function getenv; +use function is_string; use function strlen; +use function trim; use const DIRECTORY_SEPARATOR; final class ErrorsConsoleStyle extends SymfonyStyle @@ -29,6 +33,8 @@ final class ErrorsConsoleStyle extends SymfonyStyle private ?bool $isCiDetected = null; + private ?bool $isAgentDetected = null; + public function __construct(InputInterface $input, OutputInterface $output) { parent::__construct($input, $output); @@ -45,6 +51,27 @@ 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 + || file_exists('/opt/.devin'); + } + + return $this->isAgentDetected; + } + /** * @param string[] $headers * @param string[][] $rows @@ -95,9 +122,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..ca9e5b4468 --- /dev/null +++ b/tests/PHPStan/Command/ErrorFormatter/AgentDetectedErrorFormatterTest.php @@ -0,0 +1,87 @@ +assertFalse($formatter->isAgentDetected()); + } + + public function testIsAgentDetectedReturnsTrueWithAiAgent(): void + { + putenv('AI_AGENT=test'); + $formatter = new AgentDetectedErrorFormatter(new JsonErrorFormatter(false)); + $this->assertTrue($formatter->isAgentDetected()); + } + + public function testIsAgentDetectedReturnsTrueWithClaudeCode(): void + { + putenv('CLAUDE_CODE=1'); + $formatter = new AgentDetectedErrorFormatter(new JsonErrorFormatter(false)); + $this->assertTrue($formatter->isAgentDetected()); + } + + public function testFormatErrorsProducesValidJson(): void + { + $formatter = new AgentDetectedErrorFormatter(new JsonErrorFormatter(false)); + + $exitCode = $formatter->formatErrors( + $this->getAnalysisResult(1, 0), + $this->getOutput(), + ); + + $this->assertSame(1, $exitCode); + $this->assertJsonStringEqualsJsonString( + '{"totals":{"errors":0,"file_errors":1},"files":{"/data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with \\"spaces\\" and unicode 😃.php":{"errors":1,"messages":[{"message":"Foo","line":4,"ignorable":true}]}},"errors":[]}', + $this->getOutputContent(), + ); + } + + public function testFormatErrorsNoErrors(): void + { + $formatter = new AgentDetectedErrorFormatter(new JsonErrorFormatter(false)); + + $exitCode = $formatter->formatErrors( + $this->getAnalysisResult(0, 0), + $this->getOutput(), + ); + + $this->assertSame(0, $exitCode); + $this->assertJsonStringEqualsJsonString( + '{"totals":{"errors":0,"file_errors":0},"files":{},"errors":[]}', + $this->getOutputContent(), + ); + } + +}