fabis-ralph-loop 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/run.mjs
CHANGED
|
@@ -112,7 +112,7 @@ var StreamProgressParser = class {
|
|
|
112
112
|
case "result":
|
|
113
113
|
this.resultText = message.result || "";
|
|
114
114
|
this.cost = message.total_cost_usd ?? null;
|
|
115
|
-
consola.
|
|
115
|
+
if (this.resultText) consola.box(this.resultText);
|
|
116
116
|
consola.info(`Completed in ${this.turns} turns | Cost: $${this.cost ?? "?"}`);
|
|
117
117
|
break;
|
|
118
118
|
default:
|
package/dist/run.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.mjs","names":[],"sources":["../src/container/exec.ts","../src/loop/progress.ts","../src/loop/archive.ts","../src/loop/runner.ts","../src/commands/run.ts"],"sourcesContent":["import { execa } from 'execa'\n\ninterface AgentExecOptions {\n command: string\n args: string[]\n input?: string\n onData?: (chunk: Buffer) => void\n onStderr?: (chunk: Buffer) => void\n signal?: AbortSignal\n}\n\ninterface AgentExecResult {\n stdout: string\n stderr: string\n exitCode: number\n aborted: boolean\n}\n\n/**\n * Execute a command directly as a child process.\n * Returns stdout, stderr, and exit code. If onData/onStderr are provided,\n * streams chunks in real-time. Supports AbortSignal for clean cancellation.\n */\nexport async function execAgent(options: AgentExecOptions): Promise<AgentExecResult> {\n const { command, args, input, onData, onStderr, signal } = options\n\n const proc = execa(command, args, { input, reject: false })\n\n if (onData && proc.stdout) {\n proc.stdout.on('data', onData)\n }\n if (onStderr && proc.stderr) {\n proc.stderr.on('data', onStderr)\n }\n\n let aborted = false\n if (signal) {\n const onAbort = () => {\n aborted = true\n proc.kill('SIGTERM')\n }\n if (signal.aborted) {\n onAbort()\n } else {\n signal.addEventListener('abort', onAbort, { once: true })\n }\n }\n\n const result = await proc\n return {\n stdout: result.stdout,\n stderr: result.stderr,\n exitCode: result.exitCode ?? 1,\n aborted,\n }\n}\n","import { consola } from 'consola'\n\ninterface StreamMessage {\n type: string\n message?: {\n content?: Array<{\n type: string\n name?: string\n text?: string\n input?: Record<string, unknown>\n }>\n }\n result?: string\n total_cost_usd?: number\n}\n\ninterface ProgressResult {\n output: string\n turns: number\n cost: number | null\n}\n\n/**\n * Streaming progress parser for Claude stream-json output.\n * Processes lines incrementally as they arrive, logging progress to stderr.\n */\nexport class StreamProgressParser {\n private turns = 0\n private cost: number | null = null\n private resultText = ''\n private buffer = ''\n\n constructor(private iteration: number) {}\n\n /**\n * Feed a raw chunk of data. Internally buffers and processes complete lines.\n */\n processChunk(chunk: Buffer | string): void {\n this.buffer += chunk.toString()\n const lines = this.buffer.split('\\n')\n this.buffer = lines.pop() ?? '' // keep incomplete line in buffer\n\n for (const line of lines) {\n if (line.trim()) this.processLine(line)\n }\n }\n\n /**\n * Flush any remaining buffered data. Call after the process exits.\n */\n flush(): void {\n if (this.buffer.trim()) {\n this.processLine(this.buffer)\n this.buffer = ''\n }\n }\n\n getResult(): ProgressResult {\n return { output: this.resultText, turns: this.turns, cost: this.cost }\n }\n\n private processLine(line: string): void {\n let message: StreamMessage\n try {\n message = JSON.parse(line) as StreamMessage\n } catch {\n return\n }\n\n switch (message.type) {\n case 'system':\n case 'user':\n break\n\n case 'assistant': {\n this.turns++\n consola.info(`--- Iteration ${this.iteration} | Turn ${this.turns} ---`)\n\n if (message.message?.content) {\n const toolDetails = message.message.content\n .filter((c) => c.type === 'tool_use')\n .map((c) => {\n const name = c.name || ''\n const input = (c.input || {}) as Record<string, string>\n\n if (['Read', 'Write', 'Edit'].includes(name)) {\n const filePath = input.file_path || ''\n return `${name} ${filePath.split('/').pop()}`\n }\n if (name === 'Glob') return `${name} ${input.pattern || ''}`\n if (name === 'Grep') return `${name} ${input.pattern || ''}`\n if (name === 'Bash') return `${name} ${(input.command || '').slice(0, 80)}`\n return name\n })\n\n if (toolDetails.length > 0) {\n consola.info(` ${toolDetails.join('\\n ')}`)\n }\n\n const text = message.message.content\n .filter((c) => c.type === 'text')\n .map((c) => c.text || '')\n .join('')\n\n if (text) {\n consola.info(text)\n }\n }\n break\n }\n\n case 'result': {\n this.resultText = message.result || ''\n this.cost = message.total_cost_usd ?? null\n consola.info('---')\n consola.info(`Completed in ${this.turns} turns | Cost: $${this.cost ?? '?'}`)\n break\n }\n\n default: {\n if (message.type) {\n consola.debug(`[debug] unrecognized type: ${message.type}`)\n }\n break\n }\n }\n }\n}\n\n/**\n * Parse Claude stream-json output into human-readable progress.\n * Returns the final result text and metadata.\n */\nexport function parseStreamOutput(rawOutput: string, iteration: number): ProgressResult {\n const parser = new StreamProgressParser(iteration)\n parser.processChunk(rawOutput)\n parser.flush()\n return parser.getResult()\n}\n","import { readFile, writeFile, mkdir, cp } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { consola } from 'consola'\n\nconst RALPH_DIR = '.ralph'\nconst PRD_FILE = join(RALPH_DIR, 'prd.json')\nconst PROGRESS_FILE = join(RALPH_DIR, 'progress.txt')\nconst LAST_BRANCH_FILE = join(RALPH_DIR, '.last-branch')\nconst ARCHIVE_DIR = join(RALPH_DIR, 'archive')\n\nexport async function archiveIfBranchChanged(): Promise<void> {\n if (!existsSync(PRD_FILE) || !existsSync(LAST_BRANCH_FILE)) {\n await trackCurrentBranch()\n return\n }\n\n let currentBranch: string\n try {\n const prd = JSON.parse(await readFile(PRD_FILE, 'utf8'))\n currentBranch = prd.branchName || ''\n } catch {\n return\n }\n\n let lastBranch: string\n try {\n lastBranch = (await readFile(LAST_BRANCH_FILE, 'utf8')).trim()\n } catch {\n lastBranch = ''\n }\n\n if (!currentBranch || !lastBranch || currentBranch === lastBranch) {\n await trackCurrentBranch()\n return\n }\n\n // Archive the previous run\n const date = new Date().toISOString().split('T')[0]\n const folderName = lastBranch.replace(/^ralph\\//, '')\n const archiveFolder = join(ARCHIVE_DIR, `${date}-${folderName}`)\n\n consola.info(`Archiving previous run: ${lastBranch}`)\n await mkdir(archiveFolder, { recursive: true })\n\n if (existsSync(PRD_FILE)) {\n await cp(PRD_FILE, join(archiveFolder, 'prd.json'))\n }\n if (existsSync(PROGRESS_FILE)) {\n await cp(PROGRESS_FILE, join(archiveFolder, 'progress.txt'))\n }\n consola.info(`Archived to: ${archiveFolder}`)\n\n // Reset progress file\n await writeFile(\n PROGRESS_FILE,\n `# Ralph Progress Log\\nStarted: ${new Date().toISOString()}\\n---\\n`,\n )\n\n await trackCurrentBranch()\n}\n\nasync function trackCurrentBranch(): Promise<void> {\n if (!existsSync(PRD_FILE)) return\n\n try {\n const prd = JSON.parse(await readFile(PRD_FILE, 'utf8'))\n const branch = prd.branchName\n if (branch) {\n await mkdir(RALPH_DIR, { recursive: true })\n await writeFile(LAST_BRANCH_FILE, branch + '\\n')\n }\n } catch {\n // PRD doesn't exist or is invalid, skip\n }\n}\n\nexport async function ensureProgressFile(): Promise<void> {\n await mkdir(RALPH_DIR, { recursive: true })\n if (!existsSync(PROGRESS_FILE)) {\n await writeFile(\n PROGRESS_FILE,\n `# Ralph Progress Log\\nStarted: ${new Date().toISOString()}\\n---\\n`,\n )\n }\n}\n","import { existsSync } from 'node:fs'\nimport { readFile } from 'node:fs/promises'\nimport { consola } from 'consola'\nimport { execAgent } from '../container/exec.js'\nimport { StreamProgressParser } from './progress.js'\nimport { archiveIfBranchChanged, ensureProgressFile } from './archive.js'\nimport type { ResolvedConfig } from '../config/schema.js'\n\ninterface RunOptions {\n iterations: number\n model?: string\n verbose?: boolean\n}\n\ninterface IterationStats {\n iteration: number\n turns: number\n cost: number | null\n}\n\nfunction buildAgentArgs(agent: string, options: { model: string; verbose: boolean }): string[] {\n if (agent === 'claude') {\n const args = ['--dangerously-skip-permissions', '--model', options.model, '--print']\n if (options.verbose) {\n args.push('--verbose', '--output-format', 'stream-json')\n }\n return args\n }\n // Future: support other agents\n return []\n}\n\nfunction formatCost(cost: number | null): string {\n return cost != null ? `$${cost.toFixed(4)}` : '$?'\n}\n\nfunction logSummaryTable(stats: IterationStats[]): void {\n const totalTurns = stats.reduce((sum, s) => sum + s.turns, 0)\n const costs = stats.map((s) => s.cost)\n const totalCost = costs.every((c): c is number => c != null)\n ? costs.reduce((sum, c) => sum + c, 0)\n : null\n\n const rows = stats.map((s) => [String(s.iteration), String(s.turns), formatCost(s.cost)])\n const totalRow = ['Total', String(totalTurns), formatCost(totalCost)]\n\n const headers = ['Iteration', 'Turns', 'Cost']\n const allRows = [...rows, totalRow]\n const widths = headers.map((h, col) => {\n const cellLengths = allRows.map((r) => (r[col] ?? '').length)\n return Math.max(h.length, ...cellLengths)\n })\n\n const formatRow = (row: string[]) => row.map((cell, i) => cell.padEnd(widths[i] ?? 0)).join(' ')\n const separator = widths.map((w) => '-'.repeat(w)).join(' ')\n\n const lines = [\n '',\n formatRow(headers),\n separator,\n ...rows.map(formatRow),\n separator,\n formatRow(totalRow),\n ]\n\n consola.info(lines.join('\\n'))\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n if (signal?.aborted) {\n resolve()\n return\n }\n const timer = setTimeout(resolve, ms)\n signal?.addEventListener(\n 'abort',\n () => {\n clearTimeout(timer)\n resolve()\n },\n { once: true },\n )\n })\n}\n\nexport async function runLoop(config: ResolvedConfig, options: RunOptions): Promise<void> {\n const model = options.model || config.defaults.model\n const verbose = options.verbose ?? config.defaults.verbose\n const { sleepBetweenMs, completionSignal } = config.defaults\n\n if (!existsSync('/.dockerenv')) {\n consola.warn(\n 'It looks like you are running outside a Docker container. The loop is designed to run inside the Ralph container (use `ralph-loop start` to launch it).',\n )\n }\n\n await ensureProgressFile()\n\n const abortController = new AbortController()\n const { signal } = abortController\n\n // First Ctrl+C: graceful cleanup (abort agent + container process).\n // Second Ctrl+C: force exit (process.once removes the handler after first call,\n // so the default Node.js SIGINT behavior kicks in).\n const handleSignal = () => {\n consola.warn('\\nInterrupted. Cleaning up...')\n abortController.abort()\n }\n process.once('SIGINT', handleSignal)\n process.once('SIGTERM', handleSignal)\n\n consola.info(\n [\n `Starting Ralph`,\n `Agent: ${config.defaults.agent}`,\n `Model: ${model}`,\n `Verbose: ${verbose}`,\n `Iterations: ${options.iterations}`,\n ].join(' | '),\n )\n\n const iterationStats: IterationStats[] = []\n\n try {\n for (let i = 1; i <= options.iterations; i++) {\n if (signal.aborted) break\n\n consola.box(`Ralph Iteration ${i} of ${options.iterations} (${config.defaults.agent})`)\n\n await archiveIfBranchChanged()\n\n const promptContent = await readFile('.ralph-container/ralph-prompt.md', 'utf8')\n const agentArgs = buildAgentArgs(config.defaults.agent, { model, verbose })\n\n let finalOutput: string\n let turnsUsed = '?'\n\n try {\n if (verbose) {\n // Verbose: parse stream-json lines in real-time, show progress on stderr\n const parser = new StreamProgressParser(i)\n const result = await execAgent({\n command: config.defaults.agent,\n args: agentArgs,\n input: promptContent,\n onData: (chunk) => parser.processChunk(chunk),\n onStderr: (chunk) => process.stderr.write(chunk),\n signal,\n })\n\n if (result.aborted) break\n\n parser.flush()\n const progress = parser.getResult()\n finalOutput = progress.output\n turnsUsed = String(progress.turns)\n iterationStats.push({ iteration: i, turns: progress.turns, cost: progress.cost })\n\n if (result.exitCode !== 0) {\n consola.warn(`Agent exited with code ${result.exitCode}`)\n }\n } else {\n // Non-verbose: stream both stdout and stderr to stderr in real-time\n const result = await execAgent({\n command: config.defaults.agent,\n args: agentArgs,\n input: promptContent,\n onData: (chunk) => process.stderr.write(chunk),\n onStderr: (chunk) => process.stderr.write(chunk),\n signal,\n })\n\n if (result.aborted) break\n\n finalOutput = result.stdout\n }\n } catch (error) {\n consola.error(`Agent execution failed: ${error instanceof Error ? error.message : error}`)\n finalOutput = ''\n }\n\n if (finalOutput.includes(completionSignal)) {\n consola.success(\n `All stories complete! Completed at iteration ${i} of ${options.iterations}`,\n )\n return\n }\n\n if (i < options.iterations) {\n consola.info(\n `Iteration ${i} complete (${turnsUsed} turns). Sleeping ${sleepBetweenMs}ms...`,\n )\n await sleep(sleepBetweenMs, signal)\n }\n }\n\n if (signal.aborted) {\n consola.info('Stopped.')\n process.exitCode = 130\n return\n }\n\n consola.warn(`Reached max iterations (${options.iterations}) without completing all tasks.`)\n consola.warn('Check .ralph/progress.txt for status.')\n process.exitCode = 1\n } finally {\n if (verbose && iterationStats.length > 0) {\n logSummaryTable(iterationStats)\n }\n process.removeListener('SIGINT', handleSignal)\n process.removeListener('SIGTERM', handleSignal)\n }\n}\n","import { defineCommand } from 'citty'\nimport { loadRalphConfig } from '../config/loader.js'\nimport { runLoop } from '../loop/runner.js'\n\nexport default defineCommand({\n meta: {\n name: 'run',\n description: 'Execute the ralph iteration loop',\n },\n args: {\n iterations: {\n type: 'positional',\n description: 'Number of iterations to run (required)',\n required: true,\n },\n model: {\n type: 'string',\n description: 'Override default model',\n },\n verbose: {\n type: 'boolean',\n description: 'Enable verbose stream-json progress output',\n },\n },\n async run({ args }) {\n const config = await loadRalphConfig()\n const iterations = Number.parseInt(args.iterations as string, 10)\n\n if (Number.isNaN(iterations) || iterations < 1) {\n throw new Error('iterations must be a positive integer')\n }\n\n await runLoop(config, {\n iterations,\n model: args.model,\n verbose: args.verbose,\n })\n },\n})\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"run.mjs","names":[],"sources":["../src/container/exec.ts","../src/loop/progress.ts","../src/loop/archive.ts","../src/loop/runner.ts","../src/commands/run.ts"],"sourcesContent":["import { execa } from 'execa'\n\ninterface AgentExecOptions {\n command: string\n args: string[]\n input?: string\n onData?: (chunk: Buffer) => void\n onStderr?: (chunk: Buffer) => void\n signal?: AbortSignal\n}\n\ninterface AgentExecResult {\n stdout: string\n stderr: string\n exitCode: number\n aborted: boolean\n}\n\n/**\n * Execute a command directly as a child process.\n * Returns stdout, stderr, and exit code. If onData/onStderr are provided,\n * streams chunks in real-time. Supports AbortSignal for clean cancellation.\n */\nexport async function execAgent(options: AgentExecOptions): Promise<AgentExecResult> {\n const { command, args, input, onData, onStderr, signal } = options\n\n const proc = execa(command, args, { input, reject: false })\n\n if (onData && proc.stdout) {\n proc.stdout.on('data', onData)\n }\n if (onStderr && proc.stderr) {\n proc.stderr.on('data', onStderr)\n }\n\n let aborted = false\n if (signal) {\n const onAbort = () => {\n aborted = true\n proc.kill('SIGTERM')\n }\n if (signal.aborted) {\n onAbort()\n } else {\n signal.addEventListener('abort', onAbort, { once: true })\n }\n }\n\n const result = await proc\n return {\n stdout: result.stdout,\n stderr: result.stderr,\n exitCode: result.exitCode ?? 1,\n aborted,\n }\n}\n","import { consola } from 'consola'\n\ninterface StreamMessage {\n type: string\n message?: {\n content?: Array<{\n type: string\n name?: string\n text?: string\n input?: Record<string, unknown>\n }>\n }\n result?: string\n total_cost_usd?: number\n}\n\ninterface ProgressResult {\n output: string\n turns: number\n cost: number | null\n}\n\n/**\n * Streaming progress parser for Claude stream-json output.\n * Processes lines incrementally as they arrive, logging progress to stderr.\n */\nexport class StreamProgressParser {\n private turns = 0\n private cost: number | null = null\n private resultText = ''\n private buffer = ''\n\n constructor(private iteration: number) {}\n\n /**\n * Feed a raw chunk of data. Internally buffers and processes complete lines.\n */\n processChunk(chunk: Buffer | string): void {\n this.buffer += chunk.toString()\n const lines = this.buffer.split('\\n')\n this.buffer = lines.pop() ?? '' // keep incomplete line in buffer\n\n for (const line of lines) {\n if (line.trim()) this.processLine(line)\n }\n }\n\n /**\n * Flush any remaining buffered data. Call after the process exits.\n */\n flush(): void {\n if (this.buffer.trim()) {\n this.processLine(this.buffer)\n this.buffer = ''\n }\n }\n\n getResult(): ProgressResult {\n return { output: this.resultText, turns: this.turns, cost: this.cost }\n }\n\n private processLine(line: string): void {\n let message: StreamMessage\n try {\n message = JSON.parse(line) as StreamMessage\n } catch {\n return\n }\n\n switch (message.type) {\n case 'system':\n case 'user':\n break\n\n case 'assistant': {\n this.turns++\n consola.info(`--- Iteration ${this.iteration} | Turn ${this.turns} ---`)\n\n if (message.message?.content) {\n const toolDetails = message.message.content\n .filter((c) => c.type === 'tool_use')\n .map((c) => {\n const name = c.name || ''\n const input = (c.input || {}) as Record<string, string>\n\n if (['Read', 'Write', 'Edit'].includes(name)) {\n const filePath = input.file_path || ''\n return `${name} ${filePath.split('/').pop()}`\n }\n if (name === 'Glob') return `${name} ${input.pattern || ''}`\n if (name === 'Grep') return `${name} ${input.pattern || ''}`\n if (name === 'Bash') return `${name} ${(input.command || '').slice(0, 80)}`\n return name\n })\n\n if (toolDetails.length > 0) {\n consola.info(` ${toolDetails.join('\\n ')}`)\n }\n\n const text = message.message.content\n .filter((c) => c.type === 'text')\n .map((c) => c.text || '')\n .join('')\n\n if (text) {\n consola.info(text)\n }\n }\n break\n }\n\n case 'result': {\n this.resultText = message.result || ''\n this.cost = message.total_cost_usd ?? null\n if (this.resultText) {\n consola.box(this.resultText)\n }\n consola.info(`Completed in ${this.turns} turns | Cost: $${this.cost ?? '?'}`)\n break\n }\n\n default: {\n if (message.type) {\n consola.debug(`[debug] unrecognized type: ${message.type}`)\n }\n break\n }\n }\n }\n}\n\n/**\n * Parse Claude stream-json output into human-readable progress.\n * Returns the final result text and metadata.\n */\nexport function parseStreamOutput(rawOutput: string, iteration: number): ProgressResult {\n const parser = new StreamProgressParser(iteration)\n parser.processChunk(rawOutput)\n parser.flush()\n return parser.getResult()\n}\n","import { readFile, writeFile, mkdir, cp } from 'node:fs/promises'\nimport { existsSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { consola } from 'consola'\n\nconst RALPH_DIR = '.ralph'\nconst PRD_FILE = join(RALPH_DIR, 'prd.json')\nconst PROGRESS_FILE = join(RALPH_DIR, 'progress.txt')\nconst LAST_BRANCH_FILE = join(RALPH_DIR, '.last-branch')\nconst ARCHIVE_DIR = join(RALPH_DIR, 'archive')\n\nexport async function archiveIfBranchChanged(): Promise<void> {\n if (!existsSync(PRD_FILE) || !existsSync(LAST_BRANCH_FILE)) {\n await trackCurrentBranch()\n return\n }\n\n let currentBranch: string\n try {\n const prd = JSON.parse(await readFile(PRD_FILE, 'utf8'))\n currentBranch = prd.branchName || ''\n } catch {\n return\n }\n\n let lastBranch: string\n try {\n lastBranch = (await readFile(LAST_BRANCH_FILE, 'utf8')).trim()\n } catch {\n lastBranch = ''\n }\n\n if (!currentBranch || !lastBranch || currentBranch === lastBranch) {\n await trackCurrentBranch()\n return\n }\n\n // Archive the previous run\n const date = new Date().toISOString().split('T')[0]\n const folderName = lastBranch.replace(/^ralph\\//, '')\n const archiveFolder = join(ARCHIVE_DIR, `${date}-${folderName}`)\n\n consola.info(`Archiving previous run: ${lastBranch}`)\n await mkdir(archiveFolder, { recursive: true })\n\n if (existsSync(PRD_FILE)) {\n await cp(PRD_FILE, join(archiveFolder, 'prd.json'))\n }\n if (existsSync(PROGRESS_FILE)) {\n await cp(PROGRESS_FILE, join(archiveFolder, 'progress.txt'))\n }\n consola.info(`Archived to: ${archiveFolder}`)\n\n // Reset progress file\n await writeFile(\n PROGRESS_FILE,\n `# Ralph Progress Log\\nStarted: ${new Date().toISOString()}\\n---\\n`,\n )\n\n await trackCurrentBranch()\n}\n\nasync function trackCurrentBranch(): Promise<void> {\n if (!existsSync(PRD_FILE)) return\n\n try {\n const prd = JSON.parse(await readFile(PRD_FILE, 'utf8'))\n const branch = prd.branchName\n if (branch) {\n await mkdir(RALPH_DIR, { recursive: true })\n await writeFile(LAST_BRANCH_FILE, branch + '\\n')\n }\n } catch {\n // PRD doesn't exist or is invalid, skip\n }\n}\n\nexport async function ensureProgressFile(): Promise<void> {\n await mkdir(RALPH_DIR, { recursive: true })\n if (!existsSync(PROGRESS_FILE)) {\n await writeFile(\n PROGRESS_FILE,\n `# Ralph Progress Log\\nStarted: ${new Date().toISOString()}\\n---\\n`,\n )\n }\n}\n","import { existsSync } from 'node:fs'\nimport { readFile } from 'node:fs/promises'\nimport { consola } from 'consola'\nimport { execAgent } from '../container/exec.js'\nimport { StreamProgressParser } from './progress.js'\nimport { archiveIfBranchChanged, ensureProgressFile } from './archive.js'\nimport type { ResolvedConfig } from '../config/schema.js'\n\ninterface RunOptions {\n iterations: number\n model?: string\n verbose?: boolean\n}\n\ninterface IterationStats {\n iteration: number\n turns: number\n cost: number | null\n}\n\nfunction buildAgentArgs(agent: string, options: { model: string; verbose: boolean }): string[] {\n if (agent === 'claude') {\n const args = ['--dangerously-skip-permissions', '--model', options.model, '--print']\n if (options.verbose) {\n args.push('--verbose', '--output-format', 'stream-json')\n }\n return args\n }\n // Future: support other agents\n return []\n}\n\nfunction formatCost(cost: number | null): string {\n return cost != null ? `$${cost.toFixed(4)}` : '$?'\n}\n\nfunction logSummaryTable(stats: IterationStats[]): void {\n const totalTurns = stats.reduce((sum, s) => sum + s.turns, 0)\n const costs = stats.map((s) => s.cost)\n const totalCost = costs.every((c): c is number => c != null)\n ? costs.reduce((sum, c) => sum + c, 0)\n : null\n\n const rows = stats.map((s) => [String(s.iteration), String(s.turns), formatCost(s.cost)])\n const totalRow = ['Total', String(totalTurns), formatCost(totalCost)]\n\n const headers = ['Iteration', 'Turns', 'Cost']\n const allRows = [...rows, totalRow]\n const widths = headers.map((h, col) => {\n const cellLengths = allRows.map((r) => (r[col] ?? '').length)\n return Math.max(h.length, ...cellLengths)\n })\n\n const formatRow = (row: string[]) => row.map((cell, i) => cell.padEnd(widths[i] ?? 0)).join(' ')\n const separator = widths.map((w) => '-'.repeat(w)).join(' ')\n\n const lines = [\n '',\n formatRow(headers),\n separator,\n ...rows.map(formatRow),\n separator,\n formatRow(totalRow),\n ]\n\n consola.info(lines.join('\\n'))\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n if (signal?.aborted) {\n resolve()\n return\n }\n const timer = setTimeout(resolve, ms)\n signal?.addEventListener(\n 'abort',\n () => {\n clearTimeout(timer)\n resolve()\n },\n { once: true },\n )\n })\n}\n\nexport async function runLoop(config: ResolvedConfig, options: RunOptions): Promise<void> {\n const model = options.model || config.defaults.model\n const verbose = options.verbose ?? config.defaults.verbose\n const { sleepBetweenMs, completionSignal } = config.defaults\n\n if (!existsSync('/.dockerenv')) {\n consola.warn(\n 'It looks like you are running outside a Docker container. The loop is designed to run inside the Ralph container (use `ralph-loop start` to launch it).',\n )\n }\n\n await ensureProgressFile()\n\n const abortController = new AbortController()\n const { signal } = abortController\n\n // First Ctrl+C: graceful cleanup (abort agent + container process).\n // Second Ctrl+C: force exit (process.once removes the handler after first call,\n // so the default Node.js SIGINT behavior kicks in).\n const handleSignal = () => {\n consola.warn('\\nInterrupted. Cleaning up...')\n abortController.abort()\n }\n process.once('SIGINT', handleSignal)\n process.once('SIGTERM', handleSignal)\n\n consola.info(\n [\n `Starting Ralph`,\n `Agent: ${config.defaults.agent}`,\n `Model: ${model}`,\n `Verbose: ${verbose}`,\n `Iterations: ${options.iterations}`,\n ].join(' | '),\n )\n\n const iterationStats: IterationStats[] = []\n\n try {\n for (let i = 1; i <= options.iterations; i++) {\n if (signal.aborted) break\n\n consola.box(`Ralph Iteration ${i} of ${options.iterations} (${config.defaults.agent})`)\n\n await archiveIfBranchChanged()\n\n const promptContent = await readFile('.ralph-container/ralph-prompt.md', 'utf8')\n const agentArgs = buildAgentArgs(config.defaults.agent, { model, verbose })\n\n let finalOutput: string\n let turnsUsed = '?'\n\n try {\n if (verbose) {\n // Verbose: parse stream-json lines in real-time, show progress on stderr\n const parser = new StreamProgressParser(i)\n const result = await execAgent({\n command: config.defaults.agent,\n args: agentArgs,\n input: promptContent,\n onData: (chunk) => parser.processChunk(chunk),\n onStderr: (chunk) => process.stderr.write(chunk),\n signal,\n })\n\n if (result.aborted) break\n\n parser.flush()\n const progress = parser.getResult()\n finalOutput = progress.output\n turnsUsed = String(progress.turns)\n iterationStats.push({ iteration: i, turns: progress.turns, cost: progress.cost })\n\n if (result.exitCode !== 0) {\n consola.warn(`Agent exited with code ${result.exitCode}`)\n }\n } else {\n // Non-verbose: stream both stdout and stderr to stderr in real-time\n const result = await execAgent({\n command: config.defaults.agent,\n args: agentArgs,\n input: promptContent,\n onData: (chunk) => process.stderr.write(chunk),\n onStderr: (chunk) => process.stderr.write(chunk),\n signal,\n })\n\n if (result.aborted) break\n\n finalOutput = result.stdout\n }\n } catch (error) {\n consola.error(`Agent execution failed: ${error instanceof Error ? error.message : error}`)\n finalOutput = ''\n }\n\n if (finalOutput.includes(completionSignal)) {\n consola.success(\n `All stories complete! Completed at iteration ${i} of ${options.iterations}`,\n )\n return\n }\n\n if (i < options.iterations) {\n consola.info(\n `Iteration ${i} complete (${turnsUsed} turns). Sleeping ${sleepBetweenMs}ms...`,\n )\n await sleep(sleepBetweenMs, signal)\n }\n }\n\n if (signal.aborted) {\n consola.info('Stopped.')\n process.exitCode = 130\n return\n }\n\n consola.warn(`Reached max iterations (${options.iterations}) without completing all tasks.`)\n consola.warn('Check .ralph/progress.txt for status.')\n process.exitCode = 1\n } finally {\n if (verbose && iterationStats.length > 0) {\n logSummaryTable(iterationStats)\n }\n process.removeListener('SIGINT', handleSignal)\n process.removeListener('SIGTERM', handleSignal)\n }\n}\n","import { defineCommand } from 'citty'\nimport { loadRalphConfig } from '../config/loader.js'\nimport { runLoop } from '../loop/runner.js'\n\nexport default defineCommand({\n meta: {\n name: 'run',\n description: 'Execute the ralph iteration loop',\n },\n args: {\n iterations: {\n type: 'positional',\n description: 'Number of iterations to run (required)',\n required: true,\n },\n model: {\n type: 'string',\n description: 'Override default model',\n },\n verbose: {\n type: 'boolean',\n description: 'Enable verbose stream-json progress output',\n },\n },\n async run({ args }) {\n const config = await loadRalphConfig()\n const iterations = Number.parseInt(args.iterations as string, 10)\n\n if (Number.isNaN(iterations) || iterations < 1) {\n throw new Error('iterations must be a positive integer')\n }\n\n await runLoop(config, {\n iterations,\n model: args.model,\n verbose: args.verbose,\n })\n },\n})\n"],"mappings":"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"}
|
|
@@ -89,8 +89,9 @@ RUN mkdir -p <%= homeDir %>/.claude <%= homeDir %>/.local/share/direnv/allow \
|
|
|
89
89
|
ENV PATH="<%= homeDir %>/.local/bin:${PATH}"
|
|
90
90
|
|
|
91
91
|
<% if (playwright) { %>
|
|
92
|
-
# === Playwright MCP pre-install ===
|
|
93
|
-
RUN
|
|
92
|
+
# === Playwright CLI & MCP pre-install ===
|
|
93
|
+
RUN npm install -g @playwright/cli@latest \
|
|
94
|
+
&& npx -y @playwright/mcp@latest --help 2>/dev/null || true
|
|
94
95
|
<% } %>
|
|
95
96
|
# === Environment ===
|
|
96
97
|
ENV IS_SANDBOX=1
|
|
@@ -107,7 +107,7 @@ After completing one story (commit done, prd.json updated, progress appended):
|
|
|
107
107
|
|
|
108
108
|
## Rules
|
|
109
109
|
|
|
110
|
-
- Do not EVER under ANY circumstances output the completion signal `<%= completionSignal %>` until you have fully completed the full plan (ALL stories now have `passes: true`)
|
|
110
|
+
- Do not EVER under ANY circumstances output the completion signal `<%= completionSignal %>` until you have fully completed the full plan (ALL stories now have `passes: true`). You should not even mention it in your thinking & partial progress reports. If you have to refer to it just refer to it as "the completion signal" without writing the actual string.
|
|
111
111
|
- Implement exactly ONE story, then stop
|
|
112
112
|
- Commit frequently
|
|
113
113
|
- Keep CI green
|