@@ -499,6 +499,60 @@ async function ensureGitHubAuthenticated() {
499499 return false
500500}
501501
502+ /**
503+ * Check if a commit SHA is part of a pull request.
504+ * @param {string } sha - The commit SHA to check
505+ * @param {string } owner - The repository owner
506+ * @param {string } repo - The repository name
507+ * @returns {Promise<{isPR: boolean, prNumber?: number, prTitle?: string}> }
508+ */
509+ async function checkIfCommitIsPartOfPR ( sha , owner , repo ) {
510+ try {
511+ const result = await runCommandWithOutput ( 'gh' , [
512+ 'pr' ,
513+ 'list' ,
514+ '--repo' ,
515+ `${ owner } /${ repo } ` ,
516+ '--state' ,
517+ 'all' ,
518+ '--search' ,
519+ sha ,
520+ '--json' ,
521+ 'number,title,state' ,
522+ '--limit' ,
523+ '1' ,
524+ ] )
525+
526+ if ( result . exitCode === 0 && result . stdout ) {
527+ const prs = JSON . parse ( result . stdout )
528+ if ( prs . length > 0 ) {
529+ const pr = prs [ 0 ]
530+ return {
531+ isPR : true ,
532+ prNumber : pr . number ,
533+ prTitle : pr . title ,
534+ prState : pr . state ,
535+ }
536+ }
537+ }
538+ } catch ( e ) {
539+ log . warn ( `Failed to check if commit is part of PR: ${ e . message } ` )
540+ }
541+
542+ return { isPR : false }
543+ }
544+
545+ /**
546+ * Create a hash of error output for tracking duplicate errors.
547+ * @param {string } errorOutput - The error output to hash
548+ * @returns {string } A simple hash of the error
549+ */
550+ function hashError ( errorOutput ) {
551+ // Simple hash: take first 200 chars of error, normalize whitespace
552+ const normalized = errorOutput . trim ( ) . slice ( 0 , 200 ) . replace ( / \s + / g, ' ' )
553+ return normalized
554+ }
555+
502556/**
503557 * Model strategy for intelligent Pinky/Brain switching.
504558 * "Gee, Brain, what do you want to do tonight?"
@@ -2678,9 +2732,15 @@ async function runGreen(claudeCmd, options = {}) {
26782732 opts [ 'max-auto-fixes' ] || '10' ,
26792733 10 ,
26802734 )
2735+ const useNoVerify = opts [ 'no-verify' ] === true
26812736
26822737 printHeader ( 'Green CI Pipeline' )
26832738
2739+ // Track errors to avoid checking same error repeatedly
2740+ const seenErrors = new Set ( )
2741+ // Track CI errors by run ID
2742+ const ciErrorHistory = new Map ( )
2743+
26842744 // Step 1: Run local checks
26852745 const repoName = path . basename ( rootPath )
26862746 log . step ( `Running local checks in ${ colors . cyan ( repoName ) } ` )
@@ -2711,12 +2771,24 @@ async function runGreen(claudeCmd, options = {}) {
27112771
27122772 if ( result . exitCode !== 0 ) {
27132773 log . failed ( `${ check . name } failed` )
2774+
2775+ // Track error to avoid repeated attempts on same error
2776+ const errorOutput =
2777+ result . stderr || result . stdout || 'No error output available'
2778+ const errorHash = hashError ( errorOutput )
2779+
2780+ if ( seenErrors . has ( errorHash ) ) {
2781+ log . error ( `Detected same error again for "${ check . name } "` )
2782+ log . substep ( 'Skipping auto-fix to avoid infinite loop' )
2783+ log . substep ( 'Error appears unchanged from previous attempt' )
2784+ return false
2785+ }
2786+
2787+ seenErrors . add ( errorHash )
27142788 autoFixAttempts ++
27152789
27162790 // Decide whether to auto-fix or go interactive
27172791 const isAutoMode = autoFixAttempts <= MAX_AUTO_FIX_ATTEMPTS
2718- const errorOutput =
2719- result . stderr || result . stdout || 'No error output available'
27202792
27212793 if ( isAutoMode ) {
27222794 // Attempt automatic fix
@@ -2890,9 +2962,13 @@ Let's work through this together to get CI passing.`
28902962 // Stage all changes
28912963 await runCommand ( 'git' , [ 'add' , '.' ] , { cwd : rootPath } )
28922964
2893- // Commit
2894- const commitMessage = 'Fix CI issues and update tests'
2895- await runCommand ( 'git' , [ 'commit' , '-m' , commitMessage , '--no-verify' ] , {
2965+ // Commit with descriptive message (no AI attribution per CLAUDE.md)
2966+ const commitMessage = 'Fix local checks and update tests'
2967+ const commitArgs = [ 'commit' , '-m' , commitMessage ]
2968+ if ( useNoVerify ) {
2969+ commitArgs . push ( '--no-verify' )
2970+ }
2971+ await runCommand ( 'git' , commitArgs , {
28962972 cwd : rootPath ,
28972973 } )
28982974
@@ -2969,6 +3045,17 @@ Let's work through this together to get CI passing.`
29693045 const [ , owner , repoNameMatch ] = repoMatch
29703046 const repo = repoNameMatch . replace ( '.git' , '' )
29713047
3048+ // Check if commit is part of a PR
3049+ const prInfo = await checkIfCommitIsPartOfPR ( currentSha , owner , repo )
3050+ if ( prInfo . isPR ) {
3051+ log . info (
3052+ `Commit is part of PR #${ prInfo . prNumber } : ${ colors . cyan ( prInfo . prTitle ) } ` ,
3053+ )
3054+ log . substep ( `PR state: ${ prInfo . prState } ` )
3055+ } else {
3056+ log . info ( 'Commit is a direct push (not part of a PR)' )
3057+ }
3058+
29723059 // Monitor workflow with retries
29733060 let retryCount = 0
29743061 let lastRunId = null
@@ -3118,6 +3205,29 @@ Let's work through this together to get CI passing.`
31183205 } ,
31193206 )
31203207
3208+ // Check if we've seen this CI error before
3209+ const ciErrorOutput = logsResult . stdout || 'No logs available'
3210+ const ciErrorHash = hashError ( ciErrorOutput )
3211+
3212+ if ( ciErrorHistory . has ( lastRunId ) ) {
3213+ log . error ( `Already attempted fix for run ${ lastRunId } ` )
3214+ log . substep ( 'Skipping to avoid repeated attempts on same CI run' )
3215+ retryCount ++
3216+ continue
3217+ }
3218+
3219+ if ( seenErrors . has ( ciErrorHash ) ) {
3220+ log . error ( 'Detected same CI error pattern as previous attempt' )
3221+ log . substep ( 'Error appears unchanged after push' )
3222+ log . substep (
3223+ `View run at: https://github.com/${ owner } /${ repo } /actions/runs/${ lastRunId } ` ,
3224+ )
3225+ return false
3226+ }
3227+
3228+ ciErrorHistory . set ( lastRunId , ciErrorHash )
3229+ seenErrors . add ( ciErrorHash )
3230+
31213231 // Analyze and fix with Claude
31223232 log . progress ( 'Analyzing CI failure with Claude' )
31233233 const fixPrompt = `You are automatically fixing CI failures. The CI workflow failed for commit ${ currentSha } in ${ owner } /${ repo } .
@@ -3207,17 +3317,16 @@ Fix all CI failures now by making the necessary changes.`
32073317 if ( fixStatusResult . stdout . trim ( ) ) {
32083318 log . progress ( 'Committing CI fixes' )
32093319 await runCommand ( 'git' , [ 'add' , '.' ] , { cwd : rootPath } )
3210- await runCommand (
3211- 'git' ,
3212- [
3213- 'commit' ,
3214- '-m' ,
3215- `Fix CI failures (attempt ${ retryCount + 1 } )` ,
3216- '--no-verify' ,
3217- ] ,
3218- { cwd : rootPath } ,
3219- )
3320+
3321+ // Commit with descriptive message (no AI attribution per CLAUDE.md)
3322+ const ciFixMessage = `Fix CI failures from run ${ lastRunId } `
3323+ const ciCommitArgs = [ 'commit' , '-m' , ciFixMessage ]
3324+ if ( useNoVerify ) {
3325+ ciCommitArgs . push ( '--no-verify' )
3326+ }
3327+ await runCommand ( 'git' , ciCommitArgs , { cwd : rootPath } )
32203328 await runCommand ( 'git' , [ 'push' ] , { cwd : rootPath } )
3329+ log . done ( `Pushed fix commit: ${ ciFixMessage } ` )
32213330
32223331 // Update SHA for next check
32233332 const newShaResult = await runCommandWithOutput (
0 commit comments