4040$ running = true ;
4141$ logFile = null ;
4242$ optind = 0 ;
43- $ args = getopt ('vhwb:a: ' , ['verbose ' , 'help ' , 'wait-if-empty ' , 'review-bots ' , 'show-agent ' , 'show-all-issues ' , 'log:: ' ], $ optind );
43+ $ args = getopt ('vhwb:a: ' , ['verbose ' , 'help ' , 'wait-if-empty ' , 'review-bots ' , 'show-agent ' , 'show-all-issues ' , 'timeout: ' , ' log:: ' ], $ optind );
4444if (isset ($ args ['h ' ]) || isset ($ args ['help ' ])) {
45- fwrite (STDOUT , "Usage: php scripts/github-code-review.php [-v|--verbose]... [-w|--wait-if-empty] [-b|--review-bots] [-a|--show-agent] [--show-all-issues] [--log=FILE] \n" );
45+ fwrite (STDOUT , "Usage: php scripts/github-code-review.php [-v|--verbose]... [-w|--wait-if-empty] [-b|--review-bots] [-a|--show-agent] [--show-all-issues] [--timeout=30m] [-- log=FILE] \n" );
4646 fwrite (STDOUT , " -v, --verbose Increase verbosity (can stack: -vv, -vvv, -vvvv) \n" );
4747 fwrite (STDOUT , " -v [INFO] Essential progress (job start/complete/errors) \n" );
4848 fwrite (STDOUT , " -vv [DEBUG] Detailed progress (checkpoints, function entry) \n" );
5252 fwrite (STDOUT , " -b, --review-bots Also review PRs from bots like dependabot (default: skip) \n" );
5353 fwrite (STDOUT , " -a, --show-agent Stream opencode agent messages in real-time (colorized at -vvvv) \n" );
5454 fwrite (STDOUT , " --show-all-issues Include issues without file/line in report (default: skip summary sections) \n" );
55+ fwrite (STDOUT , " --timeout=N Timeout for opencode analysis (default: 30m). Examples: 30, 30s, 30m \n" );
5556 fwrite (STDOUT , " --log=FILE Append all log output to FILE (in addition to stderr) \n" );
5657 fwrite (STDOUT , " -h, --help Show this help message \n" );
5758 fwrite (STDOUT , "\n Press Ctrl-C to exit gracefully at any time. \n" );
6768$ reviewBots = isset ($ args ['b ' ]) || isset ($ args ['review-bots ' ]);
6869$ showAgent = isset ($ args ['a ' ]) || isset ($ args ['show-agent ' ]);
6970$ showAllIssues = isset ($ args ['show-all-issues ' ]);
71+
72+ // Parse timeout: bare number = seconds, number + s = seconds, number + m = minutes
73+ $ opencodeTimeout = 30 * 60 ; // default: 30 minutes in seconds
74+ if (isset ($ args ['timeout ' ])) {
75+ $ raw = is_array ($ args ['timeout ' ]) ? $ args ['timeout ' ][0 ] : $ args ['timeout ' ];
76+ if ($ raw !== false && $ raw !== '' ) {
77+ $ raw = trim ($ raw );
78+ if (preg_match ('/^(\d+)([smh])?$/i ' , $ raw , $ m )) {
79+ $ val = (int )$ m [1 ];
80+ $ unit = strtolower ($ m [2 ] ?? '' );
81+ if ($ unit === 'm ' ) {
82+ $ opencodeTimeout = $ val * 60 ;
83+ } elseif ($ unit === 'h ' ) {
84+ $ opencodeTimeout = $ val * 3600 ;
85+ } else {
86+ $ opencodeTimeout = $ val ; // bare number = seconds
87+ }
88+ } else {
89+ fwrite (STDERR , "github-code-review: invalid --timeout value ' {$ raw }', using default 30m \n" );
90+ }
91+ }
92+ }
7093if (isset ($ args ['log ' ])) {
7194 $ logFile = $ args ['log ' ] !== false ? $ args ['log ' ] : 'github-code-review.log ' ;
7295}
@@ -250,10 +273,11 @@ function parseOpencodeEventLine(string $line, int $verbose = 4): ?string
250273 * @param string|null $logFile Optional log file path
251274 * @return string Accumulated raw output
252275 */
253- function streamOpencodeOutput ($ stdout , int $ verbose , ?string $ logFile = null ): string
276+ function streamOpencodeOutput ($ stdout , int $ verbose , ?string $ logFile = null , int $ timeout = 0 ): string
254277{
255278 $ accumulated = '' ;
256279 $ renderer = null ;
280+ $ startTime = microtime (true );
257281
258282 if ($ verbose >= 4 ) {
259283 try {
@@ -263,7 +287,30 @@ function streamOpencodeOutput($stdout, int $verbose, ?string $logFile = null): s
263287 }
264288 }
265289
290+ $ timeoutSecs = $ timeout > 0 ? $ timeout : 86400 ; // default 24h if no timeout
291+ $ deadline = $ startTime + $ timeoutSecs ;
292+
266293 while (!feof ($ stdout )) {
294+ // Calculate remaining time for this select call
295+ $ remaining = max (1 , (int )($ deadline - microtime (true )));
296+ $ read = [$ stdout ];
297+ $ write = null ;
298+ $ except = null ;
299+ $ changed = @stream_select ($ read , $ write , $ except , 1 , 0 ); // 1 second timeout + microseconds
300+
301+ if ($ changed === false ) {
302+ break ; // error on stream
303+ }
304+
305+ if ($ changed === 0 ) {
306+ // Timeout on select - check if overall deadline exceeded
307+ if (microtime (true ) >= $ deadline ) {
308+ verbose_log ("streamOpencodeOutput: overall timeout reached ( {$ timeoutSecs }s), stopping " , 1 );
309+ break ;
310+ }
311+ continue ; // No data ready yet, loop and try again
312+ }
313+
267314 $ line = fgets ($ stdout );
268315 if ($ line === false ) {
269316 break ;
@@ -629,7 +676,7 @@ function main(): void
629676 */
630677function processJob (array $ job ): string
631678{
632- global $ githubToken , $ checkoutRoot , $ opencodeAnalyzeCmd , $ opencodeImproveCmd , $ verbose , $ reviewBots , $ running , $ showAgent , $ showAllIssues ;
679+ global $ githubToken , $ checkoutRoot , $ opencodeAnalyzeCmd , $ opencodeImproveCmd , $ verbose , $ reviewBots , $ running , $ showAgent , $ showAllIssues, $ opencodeTimeout ;
633680
634681 // Check if shutdown was requested before starting work
635682 if (!$ running ) {
@@ -719,8 +766,8 @@ function processJob(array $job): string
719766 // Step 3: Initialize git repo and run opencode analysis on the modified working dir
720767 initGitRepo ($ checkoutPath );
721768
722- verbose_log ("starting opencode analysis for {$ repo }# {$ prNumber } in {$ checkoutPath }" , 2 );
723- $ analysisOutput = runOpencodeAnalysis ($ checkoutPath , $ jobId , $ opencodeAnalyzeCmd , $ showAgent );
769+ verbose_log ("starting opencode analysis for {$ repo }# {$ prNumber } in {$ checkoutPath }" . ( $ opencodeTimeout > 0 ? " (timeout: " . ( $ opencodeTimeout >= 3600 ? round ( $ opencodeTimeout / 3600 , 1 ). " h " : round ( $ opencodeTimeout / 60 , 1 ). " m " ) . " ) " : "" ) , 2 );
770+ $ analysisOutput = runOpencodeAnalysis ($ checkoutPath , $ jobId , $ opencodeAnalyzeCmd , $ showAgent, $ opencodeTimeout );
724771 verbose_log ("analysis command completed " , 3 );
725772
726773 $ issues = parseAnalysisOutput ($ analysisOutput , $ showAllIssues );
@@ -1363,8 +1410,9 @@ function checkoutBranch(string $repo, string $branch, string $checkoutPath): str
13631410 * @param string $cmdTemplate Unused, kept for signature compatibility
13641411 * @return string Raw opencode output
13651412 */
1366- function runOpencodeAnalysis (string $ dir , string $ jobId , string $ cmdTemplate , bool $ showAgent = false ): string
1413+ function runOpencodeAnalysis (string $ dir , string $ jobId , string $ cmdTemplate , bool $ showAgent = false , int $ timeout = 1800 ): string
13671414{
1415+ global $ running ;
13681416 // Load review prompt from file (allows easy tweaking without code changes)
13691417 static $ reviewPrompt = null ;
13701418 if ($ reviewPrompt === null ) {
@@ -1390,8 +1438,9 @@ function runOpencodeAnalysis(string $dir, string $jobId, string $cmdTemplate, bo
13901438 file_put_contents ($ promptFile , $ reviewPrompt );
13911439
13921440 // Use bash -c with $(cat ...) to read prompt from file - avoids all escaping issues
1441+ // --format default enables streaming-compatible output with the subagent-reporter plugin
13931442 $ opencodeCmd = sprintf (
1394- 'cd %s && git diff --name-only && git diff && opencode run "$(cat %s)" --format json ' ,
1443+ 'cd %s && git diff --name-only && git diff && opencode run "$(cat %s)" --format default ' ,
13951444 escapeshellarg ($ dir ),
13961445 escapeshellarg ($ promptFile )
13971446 );
@@ -1418,7 +1467,25 @@ function runOpencodeAnalysis(string $dir, string $jobId, string $cmdTemplate, bo
14181467 // Set stdout to non-blocking for streaming reads
14191468 stream_set_blocking ($ pipes [1 ], false );
14201469
1421- $ result = streamOpencodeOutput ($ pipes [1 ], $ verbose , $ logFile );
1470+ // --- Timeout enforcement via SIGALRM ---
1471+ $ timedOut = false ;
1472+ $ alarmHandler = function (int $ signo ) use ($ proc , &$ timedOut , $ timeout ): void {
1473+ if ($ timedOut ) { return ; }
1474+ $ timedOut = true ;
1475+ verbose_log ("runOpencodeAnalysis: timeout reached ( {$ timeout }s), terminating opencode process " , 1 );
1476+ proc_terminate ($ proc , SIGTERM );
1477+ };
1478+ pcntl_async_signals (true );
1479+ pcntl_signal (SIGALRM , $ alarmHandler );
1480+ if ($ timeout > 0 ) {
1481+ pcntl_alarm ($ timeout );
1482+ }
1483+
1484+ $ result = streamOpencodeOutput ($ pipes [1 ], $ verbose , $ logFile , $ timeout );
1485+
1486+ // Cancel the alarm (process finished before timeout)
1487+ pcntl_alarm (0 );
1488+ pcntl_signal (SIGALRM , SIG_DFL );
14221489
14231490 // Read any remaining stderr
14241491 $ stderr = '' ;
0 commit comments