Skip to content

Commit 2141ac0

Browse files
committed
Add root cause analysis with confidence scores
Implement intelligent error analysis before fix attempts: - analyzeRootCause() uses Claude to diagnose errors - Returns confidence scores, categories, and ranked fix strategies - Displays analysis with color-coded output - Caches analysis for 1 hour to save costs - Learns from past fixes via error history - Identifies environmental issues (runner/network) - Suggests 1-3 strategies ranked by success probability - Saves outcomes to history for continuous learning - Shows similar past errors and their solutions
1 parent 910f5cf commit 2141ac0

File tree

1 file changed

+302
-0
lines changed

1 file changed

+302
-0
lines changed

scripts/claude.mjs

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
344580
async 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
33993661
Error 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+
34023684
Your task:
34033685
1. Analyze the error
34043686
2. 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

Comments
 (0)