← back to Letsbegin

app/api/ralph/continue/route.ts

154 lines

import { NextRequest, NextResponse } from 'next/server'
import { spawn } from 'child_process'
import * as fs from 'fs'
import * as path from 'path'

// Continue Ralph from where it left off - only runs PENDING stories

export async function POST(request: NextRequest) {
  try {
    const { projectPath } = await request.json()

    if (!projectPath) {
      return NextResponse.json({ error: 'Missing projectPath' }, { status: 400 })
    }

    const ralphDir = path.join(projectPath, 'scripts', 'ralph')
    const prdPath = path.join(ralphDir, 'prd.json')
    const progressPath = path.join(ralphDir, 'progress.txt')

    // Check if PRD exists
    if (!fs.existsSync(prdPath)) {
      return NextResponse.json({ error: 'No prd.json found. Run Start first.' }, { status: 400 })
    }

    // Check if progress exists
    if (!fs.existsSync(progressPath)) {
      return NextResponse.json({ error: 'No progress.txt found. Run Start first.' }, { status: 400 })
    }

    // Read PRD
    const ralphData = JSON.parse(fs.readFileSync(prdPath, 'utf-8'))

    // Read progress to find PENDING stories
    const progressContent = fs.readFileSync(progressPath, 'utf-8')
    const pendingStories: string[] = []
    const lines = progressContent.split('\n')

    for (const line of lines) {
      const match = line.match(/^(US-\d+): (PENDING|RUNNING)/)
      if (match) {
        pendingStories.push(match[1])
      }
    }

    if (pendingStories.length === 0) {
      return NextResponse.json({
        success: true,
        message: 'All stories already completed!',
        completed: true
      })
    }

    // Filter userStories to only include pending ones
    const storiesToRun = ralphData.userStories.filter((s: any) => pendingStories.includes(s.id))

    const logPath = '/tmp/ralph-output.log'
    const scriptPath = '/tmp/ralph-continue.sh'

    // Ensure ralph user can write to the project and log file
    const { execSync } = require('child_process')
    try {
      execSync(`chown -R ralph:ralph "${projectPath}"`, { stdio: 'pipe' })
      execSync(`touch ${logPath} && chmod 666 ${logPath}`, { stdio: 'pipe' })
    } catch {}

    // Create script that only runs pending stories
    const script = `#!/bin/bash
set -e

cd "${projectPath}"
export HOME="/home/ralph"
export NVM_DIR="/home/ralph/.nvm"
export PATH="/home/ralph/.nvm/versions/node/v22.19.0/bin:$PATH"
export ANTHROPIC_API_KEY="${process.env.ANTHROPIC_API_KEY || ''}"

PROGRESS_FILE="${progressPath}"
LOG_FILE="${logPath}"

# Append to log file (don't clear)
echo "" >> "$LOG_FILE"
echo "=== RALPH CONTINUING ===" >> "$LOG_FILE"
echo "Project: ${projectPath}" >> "$LOG_FILE"
echo "Pending stories: ${pendingStories.join(', ')}" >> "$LOG_FILE"
echo "Running as: $(whoami)" >> "$LOG_FILE"
echo "" >> "$LOG_FILE"

${storiesToRun.map((story: any) => `
# Story ${story.id}
echo "=== Starting ${story.id}: ${story.title.replace(/"/g, '\\"')} ===" >> "$LOG_FILE"
sed -i 's/${story.id}: PENDING/${story.id}: RUNNING/' "$PROGRESS_FILE"
sed -i 's/${story.id}: RUNNING/${story.id}: RUNNING/' "$PROGRESS_FILE"

cat > /tmp/ralph-story-${story.id}.txt << 'STORYPROMPT'
You are Ralphy Boy. Implement this ONE story for project ${ralphData.project}:

STORY: ${story.id} - ${story.title}
DESCRIPTION: ${story.description}

ACCEPTANCE CRITERIA:
${story.acceptanceCriteria.map((c: string) => `- ${c}`).join('\n')}

INSTRUCTIONS:
1. Implement only this story
2. Run typecheck: npm run build (or tsc --noEmit)
3. Commit: git add . && git commit -m "feat(${story.id}): ${story.title.replace(/"/g, '\\"')}"
4. Announce when done: "Completed ${story.id}"

Start implementation now.
STORYPROMPT

echo "Running Gemini for ${story.id}..." >> "$LOG_FILE"
if /home/ralph/.nvm/versions/node/v22.19.0/bin/node ${process.cwd()}/scripts/ralph-llm-runner.js --prompt-file /tmp/ralph-story-${story.id}.txt --project "${projectPath}" >> "$LOG_FILE" 2>&1; then
  echo "Completed ${story.id}" >> "$LOG_FILE"
  sed -i 's/${story.id}: RUNNING/${story.id}: DONE/' "$PROGRESS_FILE"
else
  echo "Failed ${story.id}" >> "$LOG_FILE"
  sed -i 's/${story.id}: RUNNING/${story.id}: FAIL/' "$PROGRESS_FILE"
fi
echo "" >> "$LOG_FILE"
`).join('\n')}

echo "=== RALPH_COMPLETE ===" >> "$LOG_FILE"
`

    fs.writeFileSync(scriptPath, script)
    fs.chmodSync(scriptPath, '755')

    // Spawn the process as ralph user
    const proc = spawn('sudo', ['-u', 'ralph', 'bash', scriptPath], {
      detached: true,
      stdio: 'ignore',
      cwd: projectPath,
      env: {
        ...process.env,
        HOME: '/home/ralph',
        PATH: '/home/ralph/.nvm/versions/node/v22.19.0/bin:' + process.env.PATH,
        ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || ''
      }
    })
    proc.unref()

    return NextResponse.json({
      success: true,
      message: `Continuing Ralph with ${storiesToRun.length} pending stories`,
      pendingStories,
      pid: proc.pid
    })

  } catch (error: any) {
    console.error('Ralph continue error:', error)
    return NextResponse.json({ error: error.message }, { status: 500 })
  }
}