← back to Letsbegin
app/api/ralph/status/route.ts
186 lines
import { NextResponse } from 'next/server'
import * as fs from 'fs'
const STATE_FILE = '/tmp/ralph-state.json'
const LOG_FILE = '/tmp/ralph-output.log'
interface RalphState {
running: boolean
projectPath: string
branchName: string
startTime: number
currentStoryIndex: number
logs: string[]
storyResults: Record<string, 'pending' | 'running' | 'pass' | 'fail'>
storyTimes: Record<string, number>
storyNotes: Record<string, string>
userStories: any[]
completed: boolean
pid?: number
}
function getState(): RalphState {
if (fs.existsSync(STATE_FILE)) {
return JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'))
}
return {
running: false,
projectPath: '',
branchName: '',
startTime: 0,
currentStoryIndex: 0,
logs: [],
storyResults: {},
storyTimes: {},
storyNotes: {},
userStories: [],
completed: false
}
}
function saveState(state: RalphState) {
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2))
}
export async function GET() {
try {
const state = getState()
// Ensure storyResults is initialized
if (!state.storyResults) {
state.storyResults = {}
}
if (!state.storyTimes) {
state.storyTimes = {}
}
if (!state.storyNotes) {
state.storyNotes = {}
}
if (!state.logs) {
state.logs = []
}
// Check if process is still running
if (state.pid) {
try {
process.kill(state.pid, 0)
} catch {
state.running = false
}
}
// Read latest output from log file
if (fs.existsSync(LOG_FILE)) {
const logContent = fs.readFileSync(LOG_FILE, 'utf-8')
const lines = logContent.split('\n').slice(-100)
for (const line of lines) {
const trimmedLine = line.trim()
if (!trimmedLine) continue
// Check for story start
const startMatch = trimmedLine.match(/(?:Starting|Working on)\s+(US-\d+)/i)
if (startMatch) {
const storyId = startMatch[1]
const idx = state.userStories.findIndex((s: any) => s.id === storyId)
if (idx !== -1) {
state.currentStoryIndex = idx
state.storyResults[storyId] = 'running'
const msg = '> Starting: ' + storyId
if (!state.logs.includes(msg)) state.logs.push(msg)
}
}
// Check for story completion - only accept valid story IDs
const doneMatch = trimmedLine.match(/(?:Completed|Done|DONE)\s+(US-\d+)/i)
if (doneMatch) {
const storyId = doneMatch[1]
// Validate story exists in current PRD
const storyExists = state.userStories.some((s: any) => s.id === storyId)
if (storyExists) {
state.storyResults[storyId] = 'pass'
state.storyTimes[storyId] = Math.floor((Date.now() - state.startTime) / 1000)
const msg = '> Completed: ' + storyId
if (!state.logs.includes(msg)) state.logs.push(msg)
}
// Ignore invalid story IDs (e.g., AI hallucinations)
}
// Check for story failure - only accept valid story IDs
const failMatch = trimmedLine.match(/(?:FAIL|Failed)\s+(US-\d+)/i)
if (failMatch) {
const storyId = failMatch[1]
// Validate story exists in current PRD
const storyExists = state.userStories.some((s: any) => s.id === storyId)
if (storyExists) {
state.storyResults[storyId] = 'fail'
const msg = '> Failed: ' + storyId
if (!state.logs.includes(msg)) state.logs.push(msg)
}
// Ignore invalid story IDs
}
}
// Check if completed
if (logContent.includes('RALPH_COMPLETE')) {
state.completed = true
state.running = false
if (!state.logs.includes('> Ralph completed')) {
state.logs.push('> Ralph completed')
}
}
if (state.logs.length > 200) {
state.logs = state.logs.slice(-200)
}
saveState(state)
}
// Check progress.txt - read directly regardless of userStories
if (state.projectPath) {
const progressPath = state.projectPath + '/scripts/ralph/progress.txt'
if (fs.existsSync(progressPath)) {
const progress = fs.readFileSync(progressPath, 'utf-8')
const lines = progress.split('\n')
for (const line of lines) {
// Parse lines like "US-001: DONE - Title"
const match = line.match(/^(US-\d+):\s*(DONE|PASS|FAIL|RUNNING|PENDING)/)
if (match) {
const storyId = match[1]
const status = match[2]
if (status === 'DONE' || status === 'PASS') {
state.storyResults[storyId] = 'pass'
} else if (status === 'FAIL') {
state.storyResults[storyId] = 'fail'
} else if (status === 'RUNNING') {
state.storyResults[storyId] = 'running'
} else if (status === 'PENDING') {
if (!state.storyResults[storyId]) {
state.storyResults[storyId] = 'pending'
}
}
}
}
}
}
return NextResponse.json({
running: state.running,
completed: state.completed,
currentStoryIndex: state.currentStoryIndex,
currentStory: state.userStories[state.currentStoryIndex]?.id,
logs: state.logs,
storyResults: state.storyResults,
storyTimes: state.storyTimes,
storyNotes: state.storyNotes,
elapsed: state.startTime ? Math.floor((Date.now() - state.startTime) / 1000) : 0
})
} catch (error: any) {
console.error('Ralph status error:', error)
return NextResponse.json({ error: error.message }, { status: 500 })
}
}