← back to Letsbegin
app/api/processes/fix/route.ts
288 lines
import { NextRequest, NextResponse } from 'next/server'
import { logGemini } from '@/lib/cost-track'
import * as fs from 'fs'
import * as path from 'path'
import { execSync, spawn } from 'child_process'
// Gemini API configuration (FREE - no credits needed!)
const GEMINI_API_KEY = process.env.GEMINI_API_KEY
if (!GEMINI_API_KEY) {
throw new Error('GEMINI_API_KEY environment variable is required')
}
const GEMINI_ENDPOINT = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent'
async function callGemini(prompt: string): Promise<string> {
const response = await fetch(`${GEMINI_ENDPOINT}?key=${GEMINI_API_KEY}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }],
generationConfig: { temperature: 0.3, maxOutputTokens: 2000 }
})
})
if (!response.ok) {
const error = await response.text()
throw new Error(`Gemini API error: ${error}`)
}
const data = await response.json()
logGemini(data, { note: 'processes-fix' })
return data.candidates?.[0]?.content?.parts?.[0]?.text || ''
}
const STATE_FILE = '/tmp/ralph-state.json'
export async function POST(request: NextRequest) {
try {
let body: { projectPath?: string; project?: string; analysis?: string; attempt?: number }
try {
body = await request.json()
} catch {
return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 })
}
const { projectPath, project, analysis, attempt = 1 } = body
if (!projectPath || !project) {
return NextResponse.json({ error: 'Missing projectPath or project' }, { status: 400 })
}
const logs: string[] = [`=== FIX ATTEMPT ${attempt} ===`]
logs.push(`Project: ${project}`)
logs.push(`Time: ${new Date().toISOString()}`)
logs.push('')
// Collect error context
let errorContext = analysis || ''
// Read progress.txt for current state
const progressFile = path.join(projectPath, 'scripts', 'ralph', 'progress.txt')
if (fs.existsSync(progressFile)) {
errorContext += '\n\n=== PROGRESS FILE ===\n' + fs.readFileSync(progressFile, 'utf-8')
}
// Read any error logs
const errorLogPaths = [
path.join(projectPath, 'ralph-error.log'),
path.join(projectPath, 'scripts', 'ralph', 'error.log'),
]
for (const logPath of errorLogPaths) {
if (fs.existsSync(logPath)) {
const content = fs.readFileSync(logPath, 'utf-8').split('\n').slice(-50).join('\n')
errorContext += `\n\n=== ${path.basename(logPath)} ===\n${content}`
}
}
// Read prd.json to understand the task
const prdFile = path.join(projectPath, 'scripts', 'ralph', 'prd.json')
let prdData: any = null
if (fs.existsSync(prdFile)) {
prdData = JSON.parse(fs.readFileSync(prdFile, 'utf-8'))
errorContext += `\n\n=== PRD SUMMARY ===\nProject: ${prdData.project}\nStories: ${prdData.userStories?.length || 0}`
}
// Use Gemini to analyze and generate fix commands
logs.push('Analyzing errors with Gemini...')
const aiPrompt = `You are a debugging expert. Analyze these errors from a Ralph (automated code generator) process and provide SPECIFIC fix commands.
PROJECT PATH: ${projectPath}
PROJECT: ${project}
ATTEMPT: ${attempt}
ERROR CONTEXT:
${errorContext}
Respond with a JSON object containing:
1. "diagnosis": Brief explanation of the root cause
2. "fixes": Array of fix objects, each with:
- "type": "command" | "file_edit" | "file_create" | "file_delete"
- "description": What this fix does
- For "command": "command": the bash command to run
- For "file_edit": "path", "find", "replace"
- For "file_create": "path", "content"
- For "file_delete": "path"
3. "should_restart": boolean - whether to restart Ralph after fixes
4. "confidence": "high" | "medium" | "low"
Common fixes:
- Missing dependencies: npm install
- Type errors: Fix the specific type issue
- Missing files: Create them
- Permission issues: chmod
- Port conflicts: Kill process on port
- Stalled stories: Reset story status in progress.txt
IMPORTANT: Only suggest safe, reversible fixes. Never suggest destructive commands.
Respond ONLY with valid JSON, no markdown.`
const aiResponse = await callGemini(aiPrompt)
logs.push('Claude response received')
let fixPlan: any
try {
// Clean up response - remove any markdown formatting
const cleanJson = aiResponse.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim()
fixPlan = JSON.parse(cleanJson)
} catch (e) {
logs.push(`Failed to parse fix plan: ${e}`)
logs.push(`Raw response: ${aiResponse.substring(0, 500)}`)
return NextResponse.json({
success: false,
logs,
error: 'Failed to parse AI fix suggestions'
})
}
logs.push('')
logs.push(`DIAGNOSIS: ${fixPlan.diagnosis}`)
logs.push(`CONFIDENCE: ${fixPlan.confidence}`)
logs.push(`FIXES TO APPLY: ${fixPlan.fixes?.length || 0}`)
logs.push('')
// Apply fixes
const appliedFixes: string[] = []
const failedFixes: string[] = []
for (const fix of (fixPlan.fixes || [])) {
logs.push(`Applying: ${fix.description}`)
try {
switch (fix.type) {
case 'command':
if (fix.command && !fix.command.includes('rm -rf') && !fix.command.includes('sudo rm')) {
execSync(fix.command, {
cwd: projectPath,
encoding: 'utf-8',
timeout: 30000
})
appliedFixes.push(fix.description)
logs.push(` ✓ Command executed`)
} else {
logs.push(` ✗ Skipped unsafe command`)
failedFixes.push(`Unsafe: ${fix.description}`)
}
break
case 'file_edit':
if (fix.path && fix.find && fix.replace !== undefined) {
const filePath = path.isAbsolute(fix.path) ? fix.path : path.join(projectPath, fix.path)
if (fs.existsSync(filePath)) {
let content = fs.readFileSync(filePath, 'utf-8')
if (content.includes(fix.find)) {
content = content.replace(fix.find, fix.replace)
fs.writeFileSync(filePath, content)
appliedFixes.push(fix.description)
logs.push(` ✓ File edited`)
} else {
logs.push(` ✗ Pattern not found in file`)
failedFixes.push(`Pattern not found: ${fix.description}`)
}
} else {
logs.push(` ✗ File not found: ${filePath}`)
failedFixes.push(`File not found: ${fix.description}`)
}
}
break
case 'file_create':
if (fix.path && fix.content !== undefined) {
const filePath = path.isAbsolute(fix.path) ? fix.path : path.join(projectPath, fix.path)
const dir = path.dirname(filePath)
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true })
}
fs.writeFileSync(filePath, fix.content)
appliedFixes.push(fix.description)
logs.push(` ✓ File created`)
}
break
case 'file_delete':
// Skip file deletions for safety
logs.push(` ✗ Skipped file deletion for safety`)
failedFixes.push(`Skipped deletion: ${fix.description}`)
break
default:
logs.push(` ✗ Unknown fix type: ${fix.type}`)
}
} catch (e: any) {
logs.push(` ✗ Error: ${e.message}`)
failedFixes.push(`Error: ${fix.description} - ${e.message}`)
}
}
logs.push('')
logs.push(`Applied: ${appliedFixes.length} fixes`)
logs.push(`Failed: ${failedFixes.length} fixes`)
// Restart if suggested
let restarted = false
if (fixPlan.should_restart && appliedFixes.length > 0) {
logs.push('')
logs.push('Restarting Ralph process...')
// Reset failed stories to PENDING in progress.txt
if (fs.existsSync(progressFile)) {
let content = fs.readFileSync(progressFile, 'utf-8')
content = content.replace(/: FAIL/g, ': PENDING')
content = content.replace(/: STOPPED/g, ': PENDING')
fs.writeFileSync(progressFile, content)
logs.push('Reset failed stories to PENDING')
}
// Update state file
if (fs.existsSync(STATE_FILE)) {
try {
const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'))
state.running = true
state.logs = state.logs || []
state.logs.push(`> Auto-fix attempt ${attempt} - restarting`)
// Reset failed story results
if (state.storyResults) {
for (const [id, status] of Object.entries(state.storyResults)) {
if (status === 'FAIL' || status === 'fail' || status === 'STOPPED') {
state.storyResults[id] = 'PENDING'
}
}
}
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2))
} catch {}
}
// Try to start the ralph runner
const runnerScript = path.join(projectPath, 'scripts', 'ralph', 'ralph-runner.sh')
if (fs.existsSync(runnerScript)) {
const proc = spawn('bash', [runnerScript], {
cwd: projectPath,
detached: true,
stdio: 'ignore'
})
proc.unref()
restarted = true
logs.push(`Ralph restarted with PID ${proc.pid}`)
} else {
logs.push('No ralph-runner.sh found - manual restart required')
}
}
return NextResponse.json({
success: appliedFixes.length > 0,
logs,
diagnosis: fixPlan.diagnosis,
confidence: fixPlan.confidence,
appliedFixes,
failedFixes,
restarted,
attempt
})
} catch (error: any) {
console.error('Fix process error:', error)
return NextResponse.json({ error: error.message }, { status: 500 })
}
}