@@ -341,6 +341,242 @@ async function celebrateSuccess(costTracker, stats = {}) {
341341 }
342342}
343343
344+ /**
345+ * Analyze error to identify root cause and suggest fix strategies.
346+ */
347+ async function analyzeRootCause ( claudeCmd , error , context = { } ) {
348+ const ctx = { __proto__ : null , ...context }
349+ const errorHash = hashError ( error )
350+
351+ // Check cache first.
352+ const cachePath = path . join ( STORAGE_PATHS . cache , `analysis-${ errorHash } .json` )
353+ try {
354+ if ( existsSync ( cachePath ) ) {
355+ const cached = JSON . parse ( await fs . readFile ( cachePath , 'utf8' ) )
356+ const age = Date . now ( ) - cached . timestamp
357+ // Cache valid for 1 hour.
358+ if ( age < 60 * 60 * 1000 ) {
359+ log . substep ( colors . gray ( 'Using cached analysis' ) )
360+ return cached . analysis
361+ }
362+ }
363+ } catch {
364+ // Ignore cache errors.
365+ }
366+
367+ // Load error history for learning.
368+ const history = await loadErrorHistory ( )
369+ const similarErrors = findSimilarErrors ( errorHash , history )
370+
371+ log . progress ( 'Analyzing root cause with Claude' )
372+
373+ const prompt = `You are an expert software engineer analyzing a CI/test failure.
374+
375+ **Error Output:**
376+ \`\`\`
377+ ${ error }
378+ \`\`\`
379+
380+ **Context:**
381+ - Check name: ${ ctx . checkName || 'Unknown' }
382+ - Repository: ${ ctx . repoName || 'Unknown' }
383+ - Previous attempts: ${ ctx . attempts || 0 }
384+
385+ ${ similarErrors . length > 0 ? `**Similar Past Errors:**\n${ similarErrors . map ( e => `- ${ e . description } : ${ e . outcome } (${ e . strategy } )` ) . join ( '\n' ) } \n` : '' }
386+
387+ **Task:** Analyze this error and provide a structured diagnosis.
388+
389+ **Output Format (JSON):**
390+ {
391+ "rootCause": "Brief description of the actual problem (not symptoms)",
392+ "confidence": 85, // 0-100% how certain you are
393+ "category": "type-error|lint|test-failure|build-error|env-issue|other",
394+ "isEnvironmental": false, // true if likely GitHub runner/network/rate-limit issue
395+ "strategies": [
396+ {
397+ "name": "Fix type assertion",
398+ "probability": 90, // 0-100% estimated success probability
399+ "description": "Add type assertion to resolve type mismatch",
400+ "reasoning": "Error shows TypeScript expecting string but got number"
401+ },
402+ {
403+ "name": "Update import",
404+ "probability": 60,
405+ "description": "Update import path or module resolution",
406+ "reasoning": "Might be module resolution issue"
407+ }
408+ ],
409+ "environmentalFactors": [
410+ "Check if GitHub runner has sufficient memory",
411+ "Verify network connectivity for package downloads"
412+ ],
413+ "explanation": "Detailed explanation of what's happening and why"
414+ }
415+
416+ **Rules:**
417+ - Be specific about the root cause, not just symptoms
418+ - Rank strategies by success probability (highest first)
419+ - Include 1-3 strategies maximum
420+ - Mark as environmental if it's likely a runner/network/external issue
421+ - Use confidence scores honestly (50-70% = uncertain, 80-95% = confident, 95-100% = very confident)`
422+
423+ try {
424+ const result = await runCommandWithOutput (
425+ claudeCmd ,
426+ [
427+ 'code' ,
428+ '--non-interactive' ,
429+ '--output-format' ,
430+ 'text' ,
431+ '--prompt' ,
432+ prompt ,
433+ ] ,
434+ { cwd : rootPath } ,
435+ )
436+
437+ if ( result . exitCode !== 0 ) {
438+ log . warn ( 'Analysis failed, proceeding without root cause info' )
439+ return null
440+ }
441+
442+ // Parse JSON response.
443+ const jsonMatch = result . stdout . match ( / \{ [ \s \S ] * \} / )
444+ if ( ! jsonMatch ) {
445+ log . warn ( 'Could not parse analysis, proceeding without root cause info' )
446+ return null
447+ }
448+
449+ const analysis = JSON . parse ( jsonMatch [ 0 ] )
450+
451+ // Cache the analysis.
452+ try {
453+ await fs . writeFile (
454+ cachePath ,
455+ JSON . stringify (
456+ {
457+ analysis,
458+ errorHash,
459+ timestamp : Date . now ( ) ,
460+ } ,
461+ null ,
462+ 2 ,
463+ ) ,
464+ )
465+ } catch {
466+ // Ignore cache write errors.
467+ }
468+
469+ return analysis
470+ } catch ( e ) {
471+ log . warn ( `Analysis error: ${ e . message } ` )
472+ return null
473+ }
474+ }
475+
476+ /**
477+ * Load error history from storage.
478+ */
479+ async function loadErrorHistory ( ) {
480+ const historyPath = path . join ( CLAUDE_HOME , 'error-history.json' )
481+ try {
482+ if ( existsSync ( historyPath ) ) {
483+ const data = JSON . parse ( await fs . readFile ( historyPath , 'utf8' ) )
484+ // Only return recent history (last 100 errors).
485+ return data . errors . slice ( - 100 )
486+ }
487+ } catch {
488+ // Ignore errors.
489+ }
490+ return [ ]
491+ }
492+
493+ /**
494+ * Save error outcome to history for learning.
495+ */
496+ async function saveErrorHistory ( errorHash , outcome , strategy , description ) {
497+ const historyPath = path . join ( CLAUDE_HOME , 'error-history.json' )
498+ try {
499+ let data = { errors : [ ] }
500+ if ( existsSync ( historyPath ) ) {
501+ data = JSON . parse ( await fs . readFile ( historyPath , 'utf8' ) )
502+ }
503+
504+ // 'success' | 'failed'
505+ data . errors . push ( {
506+ errorHash,
507+ outcome,
508+ strategy,
509+ description,
510+ timestamp : Date . now ( ) ,
511+ } )
512+
513+ // Keep only last 200 errors.
514+ if ( data . errors . length > 200 ) {
515+ data . errors = data . errors . slice ( - 200 )
516+ }
517+
518+ await fs . writeFile ( historyPath , JSON . stringify ( data , null , 2 ) )
519+ } catch {
520+ // Ignore errors.
521+ }
522+ }
523+
524+ /**
525+ * Find similar errors from history.
526+ */
527+ function findSimilarErrors ( errorHash , history ) {
528+ return history
529+ . filter ( e => e . errorHash === errorHash && e . outcome === 'success' )
530+ . slice ( - 3 )
531+ }
532+
533+ /**
534+ * Display root cause analysis to user.
535+ */
536+ function displayAnalysis ( analysis ) {
537+ if ( ! analysis ) {
538+ return
539+ }
540+
541+ console . log ( colors . cyan ( '\n🔍 Root Cause Analysis:' ) )
542+ console . log (
543+ ` Cause: ${ analysis . rootCause } ${ colors . gray ( `(${ analysis . confidence } % confident)` ) } ` ,
544+ )
545+ console . log ( ` Category: ${ analysis . category } ` )
546+
547+ if ( analysis . isEnvironmental ) {
548+ console . log (
549+ colors . yellow (
550+ '\n ⚠ This appears to be an environmental issue (runner/network/external)' ,
551+ ) ,
552+ )
553+ if ( analysis . environmentalFactors . length > 0 ) {
554+ console . log ( colors . yellow ( ' Factors to check:' ) )
555+ analysis . environmentalFactors . forEach ( factor => {
556+ console . log ( colors . yellow ( ` - ${ factor } ` ) )
557+ } )
558+ }
559+ }
560+
561+ if ( analysis . strategies . length > 0 ) {
562+ console . log (
563+ colors . cyan ( '\n💡 Fix Strategies (ranked by success probability):' ) ,
564+ )
565+ analysis . strategies . forEach ( ( strategy , i ) => {
566+ console . log (
567+ ` ${ i + 1 } . ${ colors . bold ( strategy . name ) } ${ colors . gray ( `(${ strategy . probability } %)` ) } ` ,
568+ )
569+ console . log ( ` ${ strategy . description } ` )
570+ console . log ( colors . gray ( ` ${ strategy . reasoning } ` ) )
571+ } )
572+ }
573+
574+ if ( analysis . explanation ) {
575+ console . log ( colors . cyan ( '\n📖 Explanation:' ) )
576+ console . log ( colors . gray ( ` ${ analysis . explanation } ` ) )
577+ }
578+ }
579+
344580async function runCommand ( command , args = [ ] , options = { } ) {
345581 const opts = { __proto__ : null , ...options }
346582 return new Promise ( ( resolve , reject ) => {
@@ -3351,6 +3587,8 @@ async function runGreen(claudeCmd, options = {}) {
33513587 ]
33523588
33533589 let autoFixAttempts = 0
3590+ let lastAnalysis = null
3591+ let lastErrorHash = null
33543592
33553593 for ( const check of localChecks ) {
33563594 log . progress ( `[${ repoName } ] ${ check . name } ` )
@@ -3385,6 +3623,29 @@ async function runGreen(claudeCmd, options = {}) {
33853623 seenErrors . add ( errorHash )
33863624 autoFixAttempts ++
33873625
3626+ // Analyze root cause before attempting fix.
3627+ const analysis = await analyzeRootCause ( claudeCmd , errorOutput , {
3628+ checkName : check . name ,
3629+ repoName,
3630+ attempts : autoFixAttempts ,
3631+ } )
3632+
3633+ // Save for history tracking.
3634+ lastAnalysis = analysis
3635+ lastErrorHash = errorHash
3636+
3637+ // Display analysis to user.
3638+ if ( analysis ) {
3639+ displayAnalysis ( analysis )
3640+
3641+ // Warn if environmental issue.
3642+ if ( analysis . isEnvironmental && analysis . confidence > 70 ) {
3643+ log . warn (
3644+ 'This looks like an environmental issue - fix may not help. Consider checking runner status.' ,
3645+ )
3646+ }
3647+ }
3648+
33883649 // Decide whether to auto-fix or go interactive
33893650 const isAutoMode = autoFixAttempts <= MAX_AUTO_FIX_ATTEMPTS
33903651
@@ -3394,11 +3655,32 @@ async function runGreen(claudeCmd, options = {}) {
33943655 `[${ repoName } ] Auto-fix attempt ${ autoFixAttempts } /${ MAX_AUTO_FIX_ATTEMPTS } ` ,
33953656 )
33963657
3658+ // Build fix prompt with analysis if available.
33973659 const fixPrompt = `You are fixing a CI/build issue automatically. The command "${ check . cmd } ${ check . args . join ( ' ' ) } " failed in the ${ path . basename ( rootPath ) } project.
33983660
33993661Error output:
34003662${ errorOutput }
34013663
3664+ ${
3665+ analysis
3666+ ? `
3667+ Root Cause Analysis:
3668+ - Problem: ${ analysis . rootCause }
3669+ - Confidence: ${ analysis . confidence } %
3670+ - Category: ${ analysis . category }
3671+
3672+ Recommended Fix Strategy:
3673+ ${
3674+ analysis . strategies [ 0 ]
3675+ ? `- ${ analysis . strategies [ 0 ] . name } (${ analysis . strategies [ 0 ] . probability } % success probability)
3676+ ${ analysis . strategies [ 0 ] . description }
3677+ Reasoning: ${ analysis . strategies [ 0 ] . reasoning } `
3678+ : 'No specific strategy recommended'
3679+ }
3680+ `
3681+ : ''
3682+ }
3683+
34023684Your task:
340336851. Analyze the error
340436862. Provide the exact fix needed
@@ -3476,6 +3758,16 @@ Fix this issue now by making the necessary changes.`
34763758 } )
34773759
34783760 if ( retryResult . exitCode !== 0 ) {
3761+ // Auto-fix didn't work - save failure to history.
3762+ if ( lastAnalysis ) {
3763+ await saveErrorHistory (
3764+ lastErrorHash ,
3765+ 'failed' ,
3766+ lastAnalysis . strategies [ 0 ] ?. name || 'auto-fix' ,
3767+ lastAnalysis . rootCause ,
3768+ )
3769+ }
3770+
34793771 // Auto-fix didn't work
34803772 if ( autoFixAttempts >= MAX_AUTO_FIX_ATTEMPTS ) {
34813773 // Switch to interactive mode
@@ -3536,6 +3828,16 @@ Let's work through this together to get CI passing.`
35363828 }
35373829 }
35383830
3831+ // Fix succeeded - save success to history.
3832+ if ( lastAnalysis ) {
3833+ await saveErrorHistory (
3834+ lastErrorHash ,
3835+ 'success' ,
3836+ lastAnalysis . strategies [ 0 ] ?. name || 'auto-fix' ,
3837+ lastAnalysis . rootCause ,
3838+ )
3839+ }
3840+
35393841 log . done ( `${ check . name } passed` )
35403842 }
35413843
0 commit comments