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