← 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 })
  }
}