reference updates

This commit is contained in:
2026-05-30 23:54:11 -04:00
parent 923f174f3b
commit ead5d9be3a
12 changed files with 1119 additions and 1154 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
node_modules node_modules
dist dist
.pi-lens .pi-lens
package-lock.json

View File

@@ -2,7 +2,7 @@
## What this is ## What this is
A Pi coding agent extension that registers the `/ralph` slash command. Not a standalone app — it runs inside Pi's extension host. A Pi coding agent extension that registers the `/ralpi` slash command. Not a standalone app — it runs inside Pi's extension host.
## Build ## Build
@@ -32,22 +32,22 @@ The only real npm dependency is `yaml` (^2.4.0).
- `parser.ts` — task file parsing (Fio, checkbox, YAML formats) - `parser.ts` — task file parsing (Fio, checkbox, YAML formats)
- `dag.ts` — Kahn's algorithm dependency resolution, batch planning - `dag.ts` — Kahn's algorithm dependency resolution, batch planning
- `executor.ts` — task execution, retry, parallel/sequential modes - `executor.ts` — task execution, retry, parallel/sequential modes
- `progress.ts``.ralph/progress.json` state management - `progress.ts``.ralpi/progress.json` state management
- `prompts.ts` — prompt generation for spawned agent sessions - `prompts.ts` — prompt generation for spawned agent sessions
- `reflection.ts` — reflection extraction from agent output - `reflection.ts` — reflection extraction from agent output
- `utils.ts` — config loading, progress discovery, `runAgentSession()` - `utils.ts` — config loading, progress discovery, `runAgentSession()`
- `types.ts` — all interfaces and `DEFAULT_CONFIG` - `types.ts` — all interfaces and `DEFAULT_CONFIG`
- `widget-batcher.ts` — debounced widget updates for parallel tasks - `widget-batcher.ts` — debounced widget updates for parallel tasks
- `skills/ralph-task/SKILL.md` — Pi skill definition for task execution - `skills/ralpi-use.md` — Pi skill definition for task execution
- `tasks/` — example ralph task files (self-modification history) - `tasks/` — example ralpi task files (self-modification history)
## Runtime state ## Runtime state
All runtime state lives in `.ralph/` (gitignored): All runtime state lives in `.ralpi/` (gitignored):
- `.ralph/progress.json` — execution progress, supports multiple PRDs - `.ralpi/progress.json` — execution progress, supports multiple PRDs
- `.ralph/reflections/` — per-task reflection JSON files - `.ralpi/reflections/` — per-task reflection JSON files
- `.ralph/prompts/` — generated prompts (timestamped, for debugging) - `.ralpi/prompts/` — generated prompts (timestamped, for debugging)
- `.ralph/sessions/` — full session transcripts - `.ralpi/sessions/` — full session transcripts
## Task ID convention ## Task ID convention
@@ -55,8 +55,8 @@ Task IDs are zero-padded strings (`"01"`, `"02"`, etc.). The parser prepends `0`
## Command routing ## Command routing
`/ralph` with no args → plan. First token looks like a path (`@path`, `./path`, `.md`, etc.) → run. Otherwise dispatches to subcommand (`run`, `plan`, `status`, `resume`, `next`, `reset`). `/ralpi` with no args → plan. First token looks like a path (`@path`, `./path`, `.md`, etc.) → run. Otherwise dispatches to subcommand (`run`, `plan`, `status`, `resume`, `next`, `reset`).
## Config ## Config
Read from `.ralph/config.yaml` in project directory. Falls back to `DEFAULT_CONFIG` in `src/types.ts` when file is missing. Config is loaded at `projectDir` level, not extension level. Read from `.ralpi/config.yaml` in project directory. Falls back to `DEFAULT_CONFIG` in `src/types.ts` when file is missing. Config is loaded at `projectDir` level, not extension level.

View File

@@ -6,7 +6,7 @@ Execute tasks from task files using DAG-based dependency resolution with persist
- **DAG-based execution**: Tasks are ordered by dependencies using Kahn's algorithm - **DAG-based execution**: Tasks are ordered by dependencies using Kahn's algorithm
- **Parallel batching**: Independent tasks in each batch can run concurrently - **Parallel batching**: Independent tasks in each batch can run concurrently
- **Persistent progress**: Execution state saved to `.ralph/progress.json` - **Persistent progress**: Execution state saved to `.ralpi/progress.json`
- **Reflection system**: Each task produces a reflection for downstream tasks - **Reflection system**: Each task produces a reflection for downstream tasks
- **Retry with backoff**: Failed tasks retry with exponential backoff - **Retry with backoff**: Failed tasks retry with exponential backoff
- **Multiple formats**: Supports Fio README, simple checkboxes, and YAML - **Multiple formats**: Supports Fio README, simple checkboxes, and YAML
@@ -21,12 +21,12 @@ Execute tasks from task files using DAG-based dependency resolution with persist
## Usage ## Usage
``` ```
/ralph plan [task-file] # Show execution plan /ralpi plan [task-file] # Show execution plan
/ralph run [task-file] # Execute all tasks /ralpi run [task-file] # Execute all tasks
/ralph status [task-file] # Show current progress /ralpi status [task-file] # Show current progress
/ralph resume [task-file] # Resume paused execution /ralpi resume [task-file] # Resume paused execution
/ralph next [task-file] # Execute next batch only /ralpi next [task-file] # Execute next batch only
/ralph reset [task-file] # Reset all progress /ralpi reset [task-file] # Reset all progress
``` ```
## Task File Formats ## Task File Formats
@@ -98,7 +98,7 @@ tasks:
## Configuration ## Configuration
Create `.ralph/config.yaml`: Create `.ralpi/config.yaml`:
```yaml ```yaml
maxRetries: 3 maxRetries: 3
@@ -121,7 +121,7 @@ Supported formats: `10m` (minutes), `600s` (seconds), `3600000` (milliseconds)
## State Files ## State Files
- `.ralph/progress.json` - Execution progress - `.ralpi/progress.json` - Execution progress
- `.ralph/reflections/` - Per-task reflections - `.ralpi/reflections/` - Per-task reflections
- `.ralph/prompts/` - Generated prompts - `.ralpi/prompts/` - Generated prompts
- `.ralph/sessions/` - Full task output for review - `.ralpi/sessions/` - Full task output for review

972
index.ts

File diff suppressed because it is too large Load Diff

65
package-lock.json generated
View File

@@ -1,65 +0,0 @@
{
"name": "ralph-loop",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ralph-loop",
"version": "1.0.0",
"dependencies": {
"yaml": "^2.4.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.3.0"
}
},
"node_modules/@types/node": {
"version": "20.19.41",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz",
"integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/yaml": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
},
"funding": {
"url": "https://github.com/sponsors/eemeli"
}
}
}
}

View File

@@ -1,5 +1,5 @@
{ {
"name": "ralph-loop", "name": "ralpi-loop",
"version": "1.0.0", "version": "1.0.0",
"description": "Execute tasks from task files using DAG-based dependency resolution with persistent progress tracking", "description": "Execute tasks from task files using DAG-based dependency resolution with persistent progress tracking",
"main": "dist/index.js", "main": "dist/index.js",
@@ -8,7 +8,9 @@
"pi-extension", "pi-extension",
"task-runner", "task-runner",
"dag", "dag",
"task-manager" "task-manager",
"ralpi-loop",
"prd"
], ],
"author": "", "author": "",
"license": "MIT", "license": "MIT",
@@ -24,9 +26,9 @@
"prepublishOnly": "npm run build" "prepublishOnly": "npm run build"
}, },
"pi": { "pi": {
"extensions": ["./dist/index.js"], "extensions": [
"skills": ["./skills"], "./dist/index.js"
"prompts": ["./prompts"] ]
}, },
"dependencies": { "dependencies": {
"yaml": "^2.4.0" "yaml": "^2.4.0"

View File

@@ -1,6 +1,10 @@
# ralph-task ---
description: Executes individual tasks from ralpi task files using DAG-based dependency resolution, with progress tracking and reflection support
---
Execute a single task from a ralph task file. # ralpi-task
Execute a single task from a ralpi task file.
## When to Use ## When to Use
@@ -11,9 +15,9 @@ Execute a single task from a ralph task file.
## Usage ## Usage
``` ```
/ralph run [task-file] # Run all tasks /ralpi run [task-file] # Run all tasks
/ralph next [task-file] # Run next batch /ralpi next [task-file] # Run next batch
/ralph status [task-file] # Check progress /ralpi status [task-file] # Check progress
``` ```
## Task File Location ## Task File Location

View File

@@ -1,19 +1,25 @@
import type { RalphConfig } from "./types";
import { DEFAULT_CONFIG } from "./types"; import { DEFAULT_CONFIG } from "./types";
export { DEFAULT_CONFIG }; export { DEFAULT_CONFIG };
// CLI // CLI
export const SLASH_COMMAND = "/ralph"; export const SLASH_COMMAND = "/ralpi";
export const COMMANDS = ["run", "plan", "status", "resume", "next", "reset"] as const; export const COMMANDS = [
"run",
"plan",
"status",
"resume",
"next",
"reset",
] as const;
// Task file detection // Task file detection
export const TASK_FILE_NAMES = [ export const TASK_FILE_NAMES = [
"README.md", "README.md",
"PRD.md", "PRD.md",
"tasks.md", "tasks.md",
"tasks.yaml", "tasks.yaml",
"tasks.yml", "tasks.yml",
] as const; ] as const;
// Reflection parsing // Reflection parsing

View File

@@ -1,29 +1,29 @@
import * as path from "node:path"; import * as path from "node:path";
import type { Task, Project, Reflection, ToolUsage } from "./types"; import type { Task, Project, Reflection, ToolUsage } from "./types";
import type { RalphConfig } from "./types"; import type { RalpiConfig } from "./types";
import type { ProgressTracker } from "./progress"; import type { ProgressTracker } from "./progress";
import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
import { buildTaskPrompt } from "./prompts"; import { buildTaskPrompt } from "./prompts";
import { extractReflection } from "./reflection"; import { extractReflection } from "./reflection";
import { WidgetBatcher } from "./widget-batcher"; import { WidgetBatcher } from "./widget-batcher";
import { import {
runAgentSession, runAgentSession,
writeFileSafe, writeFileSafe,
ensureDir, ensureDir,
captureGitCommits, captureGitCommits,
formatDuration, formatDuration,
} from "./utils"; } from "./utils";
/** Optional callback to post a progress message into the chat history. */ /** Optional callback to post a progress message into the chat history. */
export type SendChatMessage = ( export type SendChatMessage = (
content: string, content: string,
/** Extra data passed to the message renderer for the expanded view. */ /** Extra data passed to the message renderer for the expanded view. */
meta?: { toolCalls?: ToolCallEntry[] }, meta?: { toolCalls?: ToolCallEntry[] },
) => void; ) => void;
export interface ToolCallEntry { export interface ToolCallEntry {
name: string; name: string;
label: string; label: string;
} }
// ─── Run Single Task ──────────────────────────────────────────────────────── // ─── Run Single Task ────────────────────────────────────────────────────────
@@ -33,176 +33,176 @@ export interface ToolCallEntry {
* Non-blocking — the TUI remains responsive throughout. * Non-blocking — the TUI remains responsive throughout.
*/ */
export async function runTask( export async function runTask(
task: Task, task: Task,
project: Project, project: Project,
config: RalphConfig, config: RalpiConfig,
depReflections: Reflection[], depReflections: Reflection[],
ctx: ExtensionContext, ctx: ExtensionContext,
sendChatMessage?: SendChatMessage, sendChatMessage?: SendChatMessage,
projectDir: string = project.sourceDir, projectDir: string = project.sourceDir,
batcher?: WidgetBatcher, batcher?: WidgetBatcher,
): Promise<{ ): Promise<{
success: boolean; success: boolean;
reflection?: Reflection; reflection?: Reflection;
error?: string; error?: string;
durationMs: number; durationMs: number;
toolUsage?: ToolUsage; toolUsage?: ToolUsage;
outputPreview?: string; outputPreview?: string;
sessionFile?: string; sessionFile?: string;
commitMessages?: string[]; commitMessages?: string[];
commitSummary?: string; commitSummary?: string;
}> { }> {
const startMs = Date.now(); const startMs = Date.now();
// Build prompt // Build prompt
const prompt = buildTaskPrompt( const prompt = buildTaskPrompt(
task, task,
project, project,
depReflections, depReflections,
config.prompts.projectContext, config.prompts.projectContext,
); );
// Write prompt to .ralph/ with timestamp (for debugging) // Write prompt to .ralpi/ with timestamp (for debugging)
const ralphDir = path.join(projectDir, ".ralph"); const ralpiDir = path.join(projectDir, ".ralpi");
ensureDir(ralphDir); ensureDir(ralpiDir);
const promptFile = path.join(ralphDir, `prompt-${startMs}.md`); const promptFile = path.join(ralpiDir, `prompt-${startMs}.md`);
writeFileSafe(promptFile, prompt); writeFileSafe(promptFile, prompt);
// Footer shows just the task title (no batch prefix) // Footer shows just the task title (no batch prefix)
ctx.ui.setStatus("ralph", task.title); ctx.ui.setStatus("ralpi", task.title);
const taskHeader = `${task.id} · ${task.title}`; const taskHeader = `${task.id} · ${task.title}`;
// Live progress widget above the editor — animated spinner + tool call tree // Live progress widget above the editor — animated spinner + tool call tree
// Using setWidget instead of setWorkingMessage because the working message area // Using setWidget instead of setWorkingMessage because the working message area
// is only visible during parent agent streaming, not during extension command execution. // is only visible during parent agent streaming, not during extension command execution.
// Widget key is unique per task so parallel tasks each get their own widget. // Widget key is unique per task so parallel tasks each get their own widget.
const widgetKey = `ralph-task-${task.id}`; const widgetKey = `ralpi-task-${task.id}`;
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
let frameIndex = 0; let frameIndex = 0;
const theme = ctx.ui.theme; const theme = ctx.ui.theme;
const MAX_COLLAPSED = 3; const MAX_COLLAPSED = 3;
const toolCalls: ToolCallEntry[] = []; const toolCalls: ToolCallEntry[] = [];
const updateWidget = () => { const updateWidget = () => {
const frame = theme.fg("accent", SPINNER_FRAMES[frameIndex]); const frame = theme.fg("accent", SPINNER_FRAMES[frameIndex]);
const lines = [`${frame} ${taskHeader}`]; const lines = [`${frame} ${taskHeader}`];
if (toolCalls.length > 0) { if (toolCalls.length > 0) {
const shown = toolCalls.slice(-MAX_COLLAPSED); const shown = toolCalls.slice(-MAX_COLLAPSED);
const remaining = toolCalls.length - shown.length; const remaining = toolCalls.length - shown.length;
if (remaining > 0) { if (remaining > 0) {
lines.push(theme.fg("dim", ` ├── ${remaining} more`)); lines.push(theme.fg("dim", ` ├── ${remaining} more`));
} }
for (let i = 0; i < shown.length; i++) { for (let i = 0; i < shown.length; i++) {
const entry = shown[i]; const entry = shown[i];
const isLast = i === shown.length - 1; const isLast = i === shown.length - 1;
const branch = isLast ? " └── " : " ├── "; const branch = isLast ? " └── " : " ├── ";
const tag = theme.fg("accent", `[${entry.name}]`); const tag = theme.fg("accent", `[${entry.name}]`);
lines.push(`${branch}${tag} ${entry.label}`); lines.push(`${branch}${tag} ${entry.label}`);
} }
} }
if (batcher) { if (batcher) {
batcher.schedule(widgetKey, lines); batcher.schedule(widgetKey, lines);
} else { } else {
ctx.ui.setWidget(widgetKey, lines); ctx.ui.setWidget(widgetKey, lines);
} }
}; };
// Smooth spinner animation at 100ms intervals // Smooth spinner animation at 100ms intervals
const spinnerTimer = setInterval(() => { const spinnerTimer = setInterval(() => {
frameIndex = (frameIndex + 1) % SPINNER_FRAMES.length; frameIndex = (frameIndex + 1) % SPINNER_FRAMES.length;
updateWidget(); updateWidget();
}, 100); }, 100);
// Initial display // Initial display
updateWidget(); updateWidget();
// Use task-level timeout if set, otherwise fall back to config // Use task-level timeout if set, otherwise fall back to config
const timeoutMs = task.timeoutMs ?? config.execution.timeoutMs; const timeoutMs = task.timeoutMs ?? config.execution.timeoutMs;
// Pre-create session file path so events stream to disk (avoids 300+ MB in-memory accumulation) // Pre-create session file path so events stream to disk (avoids 300+ MB in-memory accumulation)
const sessionsDir = path.join(ralphDir, "sessions"); const sessionsDir = path.join(ralpiDir, "sessions");
ensureDir(sessionsDir); ensureDir(sessionsDir);
const sessionFilePath = path.join(sessionsDir, `${task.id}-${startMs}.txt`); const sessionFilePath = path.join(sessionsDir, `${task.id}-${startMs}.txt`);
// Run task asynchronously via Pi SDK — event loop stays responsive // Run task asynchronously via Pi SDK — event loop stays responsive
const output = await runAgentSession( const output = await runAgentSession(
prompt, prompt,
projectDir, projectDir,
timeoutMs, timeoutMs,
(event) => { (event) => {
if (event.type === "tool_execution_start") { if (event.type === "tool_execution_start") {
const label = formatToolArg(event.toolName, event.args); const label = formatToolArg(event.toolName, event.args);
toolCalls.push({ toolCalls.push({
name: event.toolName, name: event.toolName,
label, label,
}); });
updateWidget(); updateWidget();
} }
}, },
undefined, // no abort signal undefined, // no abort signal
sessionFilePath, // stream events to file sessionFilePath, // stream events to file
); );
const durationMs = Date.now() - startMs; const durationMs = Date.now() - startMs;
// Clear progress widget and status after task finishes // Clear progress widget and status after task finishes
clearInterval(spinnerTimer); clearInterval(spinnerTimer);
if (batcher) { if (batcher) {
batcher.scheduleRemove(widgetKey); batcher.scheduleRemove(widgetKey);
} else { } else {
ctx.ui.setWidget(widgetKey, undefined); ctx.ui.setWidget(widgetKey, undefined);
} }
ctx.ui.setStatus("ralph", undefined); ctx.ui.setStatus("ralpi", undefined);
if (!output.success) { if (!output.success) {
sendChatMessage?.(`${taskHeader}${output.error}`); sendChatMessage?.(`${taskHeader}${output.error}`);
ctx.ui.notify(`Task ${task.id} failed: ${output.error}`, "error"); ctx.ui.notify(`Task ${task.id} failed: ${output.error}`, "error");
return { return {
success: false, success: false,
error: output.error, error: output.error,
durationMs, durationMs,
sessionFile: sessionFilePath, // events streamed to file for debugging sessionFile: sessionFilePath, // events streamed to file for debugging
}; };
} }
const agentText = output.text; const agentText = output.text;
const toolUsage = output.toolUsage; const toolUsage = output.toolUsage;
// Capture git commits made during this task // Capture git commits made during this task
const { commitMessages, commitSummary } = captureGitCommits(projectDir); const { commitMessages, commitSummary } = captureGitCommits(projectDir);
// Session file already written by runAgentSession (events streamed to disk) // Session file already written by runAgentSession (events streamed to disk)
const sessionFile = sessionFilePath; const sessionFile = sessionFilePath;
// Build output preview (first 500 chars of agent text) // Build output preview (first 500 chars of agent text)
const outputPreview = const outputPreview =
agentText.length > 500 agentText.length > 500
? agentText.slice(0, 500) + "\n... (truncated, see session file)" ? agentText.slice(0, 500) + "\n... (truncated, see session file)"
: agentText; : agentText;
// Extract reflection from agent output // Extract reflection from agent output
const reflection = extractReflection(agentText, task.id, task.title); const reflection = extractReflection(agentText, task.id, task.title);
// Post completion chat message — header only, renderer builds the expandable tree // Post completion chat message — header only, renderer builds the expandable tree
const dur = formatDuration(durationMs); const dur = formatDuration(durationMs);
sendChatMessage?.(`${taskHeader} (${dur})`, { toolCalls }); sendChatMessage?.(`${taskHeader} (${dur})`, { toolCalls });
return { return {
success: true, success: true,
reflection: reflection ?? undefined, reflection: reflection ?? undefined,
durationMs, durationMs,
toolUsage, toolUsage,
outputPreview, outputPreview,
sessionFile, sessionFile,
commitMessages, commitMessages,
commitSummary, commitSummary,
}; };
} }
// ─── Execute Batch ─────────────────────────────────────────────────────────── // ─── Execute Batch ───────────────────────────────────────────────────────────
@@ -211,198 +211,198 @@ export async function runTask(
* Execute a batch of tasks (sequentially or in parallel) * Execute a batch of tasks (sequentially or in parallel)
*/ */
export async function executeBatch( export async function executeBatch(
tasks: Task[], tasks: Task[],
project: Project, project: Project,
config: RalphConfig, config: RalpiConfig,
progress: ProgressTracker, progress: ProgressTracker,
ctx: ExtensionContext, ctx: ExtensionContext,
options?: { parallel?: boolean }, options?: { parallel?: boolean },
sendChatMessage?: SendChatMessage, sendChatMessage?: SendChatMessage,
projectDir?: string, projectDir?: string,
): Promise<void> { ): Promise<void> {
// Defensive: ensure tasks is an iterable array // Defensive: ensure tasks is an iterable array
if (!Array.isArray(tasks)) { if (!Array.isArray(tasks)) {
throw new Error( throw new Error(
`executeBatch received invalid tasks: expected array, got ${typeof tasks}`, `executeBatch received invalid tasks: expected array, got ${typeof tasks}`,
); );
} }
// Check if we should run parallel // Check if we should run parallel
const shouldParallel = const shouldParallel =
options?.parallel && tasks.length > 1 && config.execution.maxParallel > 0; options?.parallel && tasks.length > 1 && config.execution.maxParallel > 0;
if (shouldParallel) { if (shouldParallel) {
await executeBatchParallel( await executeBatchParallel(
tasks, tasks,
project, project,
config, config,
progress, progress,
ctx, ctx,
sendChatMessage, sendChatMessage,
projectDir, projectDir,
); );
return; return;
} }
// Execute sequentially // Execute sequentially
for (const task of tasks) { for (const task of tasks) {
await executeTask( await executeTask(
task, task,
project, project,
config, config,
progress, progress,
ctx, ctx,
sendChatMessage, sendChatMessage,
projectDir, projectDir,
); );
} }
} }
/** /**
* Execute tasks in parallel using child processes * Execute tasks in parallel using child processes
*/ */
async function executeBatchParallel( async function executeBatchParallel(
tasks: Task[], tasks: Task[],
project: Project, project: Project,
config: RalphConfig, config: RalpiConfig,
progress: ProgressTracker, progress: ProgressTracker,
ctx: ExtensionContext, ctx: ExtensionContext,
sendChatMessage?: SendChatMessage, sendChatMessage?: SendChatMessage,
projectDir?: string, projectDir?: string,
): Promise<void> { ): Promise<void> {
const maxParallel = config.execution.maxParallel; const maxParallel = config.execution.maxParallel;
const batcher = new WidgetBatcher(ctx); const batcher = new WidgetBatcher(ctx);
const results: Array<{ task: Task; result: Promise<any> }> = []; const results: Array<{ task: Task; result: Promise<any> }> = [];
for (const task of tasks) { for (const task of tasks) {
results.push({ results.push({
task, task,
result: executeTask( result: executeTask(
task, task,
project, project,
config, config,
progress, progress,
ctx, ctx,
sendChatMessage, sendChatMessage,
projectDir, projectDir,
batcher, batcher,
), ),
}); });
// Limit concurrency // Limit concurrency
if (results.length >= maxParallel) { if (results.length >= maxParallel) {
const first = results.shift(); const first = results.shift();
if (first) await first.result; if (first) await first.result;
} }
} }
// Wait for remaining tasks // Wait for remaining tasks
for (const { result } of results) { for (const { result } of results) {
await result; await result;
} }
// Flush and stop the batcher after all tasks complete // Flush and stop the batcher after all tasks complete
batcher.stop(); batcher.stop();
} }
// ─── Execute Single Task with Retry ────────────────────────────────────────── // ─── Execute Single Task with Retry ──────────────────────────────────────────
async function executeTask( async function executeTask(
task: Task, task: Task,
project: Project, project: Project,
config: RalphConfig, config: RalpiConfig,
progress: ProgressTracker, progress: ProgressTracker,
ctx: ExtensionContext, ctx: ExtensionContext,
sendChatMessage?: SendChatMessage, sendChatMessage?: SendChatMessage,
projectDir: string = project.sourceDir, projectDir: string = project.sourceDir,
batcher?: WidgetBatcher, batcher?: WidgetBatcher,
): Promise<void> { ): Promise<void> {
const maxRetries = config.execution.maxRetries; const maxRetries = config.execution.maxRetries;
let retries = 0; let retries = 0;
while (retries <= maxRetries) { while (retries <= maxRetries) {
try { try {
// Mark as in progress // Mark as in progress
progress.markInProgress(task.id); progress.markInProgress(task.id);
// Get dependency reflections // Get dependency reflections
const depReflections = progress.getDependencyReflections( const depReflections = progress.getDependencyReflections(
task.dependencies || [], task.dependencies || [],
); );
// Run the task // Run the task
const result = await runTask( const result = await runTask(
task, task,
project, project,
config, config,
depReflections, depReflections,
ctx, ctx,
sendChatMessage, sendChatMessage,
projectDir, projectDir,
batcher, batcher,
); );
if (result.success) { if (result.success) {
// Save reflection // Save reflection
if (result.reflection) { if (result.reflection) {
saveReflectionToFile(projectDir, config, result.reflection); saveReflectionToFile(projectDir, config, result.reflection);
} }
// Mark completed with all metadata // Mark completed with all metadata
progress.markCompleted( progress.markCompleted(
task.id, task.id,
result.durationMs, result.durationMs,
result.reflection, result.reflection,
result.toolUsage, result.toolUsage,
result.sessionFile, result.sessionFile,
result.outputPreview, result.outputPreview,
result.commitMessages, result.commitMessages,
result.commitSummary, result.commitSummary,
); );
return; return;
} }
// Task failed, check if we should retry // Task failed, check if we should retry
if (retries < maxRetries) { if (retries < maxRetries) {
retries = progress.incrementRetry(task.id); retries = progress.incrementRetry(task.id);
ctx.ui.notify( ctx.ui.notify(
`Retrying task ${task.id} (${retries}/${maxRetries}): ${result.error}`, `Retrying task ${task.id} (${retries}/${maxRetries}): ${result.error}`,
"warning", "warning",
); );
// Exponential backoff // Exponential backoff
const delay = config.execution.retryDelayMs * 2 ** (retries - 1); const delay = config.execution.retryDelayMs * 2 ** (retries - 1);
await sleep(delay); await sleep(delay);
} else { } else {
// Max retries exceeded // Max retries exceeded
progress.markFailed(task.id, result.error || "Unknown error"); progress.markFailed(task.id, result.error || "Unknown error");
throw new Error(`Task ${task.id} failed: ${result.error}`); throw new Error(`Task ${task.id} failed: ${result.error}`);
} }
} catch (error) { } catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error); const errorMsg = error instanceof Error ? error.message : String(error);
progress.markFailed(task.id, errorMsg); progress.markFailed(task.id, errorMsg);
throw error; throw error;
} }
} }
} }
// ─── Save Reflection to File ──────────────────────────────────────────────── // ─── Save Reflection to File ────────────────────────────────────────────────
function saveReflectionToFile( function saveReflectionToFile(
sourceDir: string, sourceDir: string,
config: RalphConfig, config: RalpiConfig,
reflection: Reflection, reflection: Reflection,
): void { ): void {
const reflectionsDir = path.join(sourceDir, config.paths.reflectionsDir); const reflectionsDir = path.join(sourceDir, config.paths.reflectionsDir);
ensureDir(reflectionsDir); ensureDir(reflectionsDir);
const filePath = path.join(reflectionsDir, `${reflection.taskId}.json`); const filePath = path.join(reflectionsDir, `${reflection.taskId}.json`);
writeFileSafe(filePath, JSON.stringify(reflection, null, 2)); writeFileSafe(filePath, JSON.stringify(reflection, null, 2));
} }
// ─── Helpers ───────────────────────────────────────────────────────────────── // ─── Helpers ─────────────────────────────────────────────────────────────────
function sleep(ms: number): Promise<void> { function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms));
} }
// ─── Tool Call Formatting ──────────────────────────────────────────────── // ─── Tool Call Formatting ────────────────────────────────────────────────
@@ -411,31 +411,34 @@ function sleep(ms: number): Promise<void> {
* Format a tool call argument into a short label. * Format a tool call argument into a short label.
*/ */
function formatToolArg(name: string, args: unknown): string { function formatToolArg(name: string, args: unknown): string {
const a = args as Record<string, unknown>; const a = args as Record<string, unknown>;
switch (name) { switch (name) {
case "bash": case "bash":
return truncateMiddle(String(a.command ?? ""), 70); return truncateMiddle(String(a.command ?? ""), 70);
case "write": case "write":
case "read": case "read":
return truncateMiddle(String(a.path ?? ""), 60); return truncateMiddle(String(a.path ?? ""), 60);
case "edit": case "edit":
return truncateMiddle(String(a.path ?? ""), 60); return truncateMiddle(String(a.path ?? ""), 60);
case "grep": case "grep":
return `${a.pattern ?? "?"}${truncateMiddle(String(a.path ?? ""), 40)}`; return `${a.pattern ?? "?"}${truncateMiddle(
case "find": String(a.path ?? ""),
return `${a.path ?? "."}${a.glob ?? "*"}`; 40,
case "ls": )}`;
return truncateMiddle(String(a.path ?? "."), 60); case "find":
default: return `${a.path ?? "."}${a.glob ?? "*"}`;
return name; case "ls":
} return truncateMiddle(String(a.path ?? "."), 60);
default:
return name;
}
} }
/** /**
* Truncate a long string in the middle, keeping start and end visible. * Truncate a long string in the middle, keeping start and end visible.
*/ */
function truncateMiddle(s: string, maxLen: number): string { function truncateMiddle(s: string, maxLen: number): string {
if (s.length <= maxLen) return s; if (s.length <= maxLen) return s;
const half = Math.floor((maxLen - 3) / 2); const half = Math.floor((maxLen - 3) / 2);
return s.slice(0, half) + "…" + s.slice(s.length - half); return s.slice(0, half) + "…" + s.slice(s.length - half);
} }

View File

@@ -1,6 +1,12 @@
import * as fs from "node:fs"; import * as fs from "node:fs";
import * as path from "node:path"; import * as path from "node:path";
import type { ProgressState, PRDProgress, Task, Reflection, ToolUsage } from "./types"; import type {
ProgressState,
PRDProgress,
Task,
Reflection,
ToolUsage,
} from "./types";
import { ensureDir } from "./utils"; import { ensureDir } from "./utils";
/** /**
@@ -8,258 +14,264 @@ import { ensureDir } from "./utils";
* e.g., "tasks/feature-x/README.md" → "tasks-feature-x-README" * e.g., "tasks/feature-x/README.md" → "tasks-feature-x-README"
*/ */
export function derivePRDKey(projectDir: string, sourcePath: string): string { export function derivePRDKey(projectDir: string, sourcePath: string): string {
const rel = path.relative(projectDir, sourcePath); const rel = path.relative(projectDir, sourcePath);
return rel.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, ""); return rel
.replace(/[^a-zA-Z0-9_-]/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "");
} }
/** /**
* Manages persistent progress state for a ralph execution. * Manages persistent progress state for a ralph execution.
* State is stored as JSON in .ralph/progress.json. * State is stored as JSON in .ralpi/progress.json.
* Supports multiple PRDs in progress simultaneously via the `prds` field. * Supports multiple PRDs in progress simultaneously via the `prds` field.
* Falls back to legacy flat format for backward compatibility. * Falls back to legacy flat format for backward compatibility.
*/ */
export class ProgressTracker { export class ProgressTracker {
private statePath: string; private statePath: string;
private state: ProgressState; private state: ProgressState;
private prdKey: string; private prdKey: string;
constructor(projectDir: string, sourcePath: string, prdKey?: string) { constructor(projectDir: string, sourcePath: string, prdKey?: string) {
const stateDir = path.join(projectDir, ".ralph"); const stateDir = path.join(projectDir, ".ralpi");
ensureDir(stateDir); ensureDir(stateDir);
this.statePath = path.join(stateDir, "progress.json"); this.statePath = path.join(stateDir, "progress.json");
this.prdKey = prdKey ?? derivePRDKey(projectDir, sourcePath); this.prdKey = prdKey ?? derivePRDKey(projectDir, sourcePath);
this.state = this.loadOrCreate(sourcePath); this.state = this.loadOrCreate(sourcePath);
} }
/** Load existing state or create a fresh one */ /** Load existing state or create a fresh one */
private loadOrCreate(sourcePathHint: string): ProgressState { private loadOrCreate(sourcePathHint: string): ProgressState {
if (fs.existsSync(this.statePath)) { if (fs.existsSync(this.statePath)) {
try { try {
const raw = fs.readFileSync(this.statePath, "utf-8"); const raw = fs.readFileSync(this.statePath, "utf-8");
const parsed = JSON.parse(raw) as ProgressState; const parsed = JSON.parse(raw) as ProgressState;
// Multi-PRD mode: check if we have a PRD entry // Multi-PRD mode: check if we have a PRD entry
if (parsed.prds?.[this.prdKey]) { if (parsed.prds?.[this.prdKey]) {
// Found PRD entry — use it, but keep legacy fields for compat // Found PRD entry — use it, but keep legacy fields for compat
return parsed; return parsed;
} }
// Legacy flat mode: check if the source path matches // Legacy flat mode: check if the source path matches
if (path.resolve(parsed.sourcePath) === path.resolve(sourcePathHint)) { if (path.resolve(parsed.sourcePath) === path.resolve(sourcePathHint)) {
// Migrate legacy state to PRD mode // Migrate legacy state to PRD mode
parsed.prds = { parsed.prds = {
[this.prdKey]: { [this.prdKey]: {
sourcePath: parsed.sourcePath, sourcePath: parsed.sourcePath,
tasks: parsed.tasks, tasks: parsed.tasks,
startedAt: parsed.startedAt, startedAt: parsed.startedAt,
lastUpdatedAt: parsed.lastUpdatedAt, lastUpdatedAt: parsed.lastUpdatedAt,
paused: parsed.paused, paused: parsed.paused,
}, },
}; };
return parsed; return parsed;
} }
// Different PRD — create new entry alongside existing ones // Different PRD — create new entry alongside existing ones
if (parsed.prds) { if (parsed.prds) {
parsed.prds[this.prdKey] = this.freshPRD(sourcePathHint); parsed.prds[this.prdKey] = this.freshPRD(sourcePathHint);
return parsed; return parsed;
} }
// Legacy flat state exists but for a different source — promote it to PRD mode // Legacy flat state exists but for a different source — promote it to PRD mode
const legacyKey = derivePRDKey(path.dirname(this.statePath), parsed.sourcePath); const legacyKey = derivePRDKey(
parsed.prds = { path.dirname(this.statePath),
[legacyKey]: { parsed.sourcePath,
sourcePath: parsed.sourcePath, );
tasks: parsed.tasks, parsed.prds = {
startedAt: parsed.startedAt, [legacyKey]: {
lastUpdatedAt: parsed.lastUpdatedAt, sourcePath: parsed.sourcePath,
paused: parsed.paused, tasks: parsed.tasks,
}, startedAt: parsed.startedAt,
[this.prdKey]: this.freshPRD(sourcePathHint), lastUpdatedAt: parsed.lastUpdatedAt,
}; paused: parsed.paused,
return parsed; },
} catch { [this.prdKey]: this.freshPRD(sourcePathHint),
// Fall through to create new };
} return parsed;
} } catch {
// Fall through to create new
}
}
return this.freshState(sourcePathHint); return this.freshState(sourcePathHint);
} }
private freshPRD(sourcePath: string): PRDProgress { private freshPRD(sourcePath: string): PRDProgress {
return { return {
sourcePath, sourcePath,
tasks: {}, tasks: {},
startedAt: new Date().toISOString(), startedAt: new Date().toISOString(),
lastUpdatedAt: new Date().toISOString(), lastUpdatedAt: new Date().toISOString(),
paused: false, paused: false,
}; };
} }
private freshState(sourcePath: string): ProgressState { private freshState(sourcePath: string): ProgressState {
return { return {
sourcePath, sourcePath,
tasks: {}, tasks: {},
startedAt: new Date().toISOString(), startedAt: new Date().toISOString(),
lastUpdatedAt: new Date().toISOString(), lastUpdatedAt: new Date().toISOString(),
paused: false, paused: false,
prds: { prds: {
[this.prdKey]: { [this.prdKey]: {
sourcePath, sourcePath,
tasks: {}, tasks: {},
startedAt: new Date().toISOString(), startedAt: new Date().toISOString(),
lastUpdatedAt: new Date().toISOString(), lastUpdatedAt: new Date().toISOString(),
paused: false, paused: false,
}, },
}, },
}; };
} }
/** Get the PRD-scoped progress entry */ /** Get the PRD-scoped progress entry */
private getPRD(): PRDProgress { private getPRD(): PRDProgress {
if (!this.state.prds) { if (!this.state.prds) {
// Should not happen after loadOrCreate, but guard anyway // Should not happen after loadOrCreate, but guard anyway
this.state.prds = { [this.prdKey]: this.freshPRD(this.state.sourcePath) }; this.state.prds = { [this.prdKey]: this.freshPRD(this.state.sourcePath) };
} }
if (!this.state.prds[this.prdKey]) { if (!this.state.prds[this.prdKey]) {
this.state.prds[this.prdKey] = this.freshPRD(this.state.sourcePath); this.state.prds[this.prdKey] = this.freshPRD(this.state.sourcePath);
} }
return this.state.prds[this.prdKey]; return this.state.prds[this.prdKey];
} }
/** Save current state to disk */ /** Save current state to disk */
save(): void { save(): void {
const prd = this.getPRD(); const prd = this.getPRD();
prd.lastUpdatedAt = new Date().toISOString(); prd.lastUpdatedAt = new Date().toISOString();
// Sync legacy flat fields with current PRD for backward compat // Sync legacy flat fields with current PRD for backward compat
this.state.sourcePath = prd.sourcePath; this.state.sourcePath = prd.sourcePath;
this.state.tasks = prd.tasks; this.state.tasks = prd.tasks;
this.state.startedAt = prd.startedAt; this.state.startedAt = prd.startedAt;
this.state.lastUpdatedAt = prd.lastUpdatedAt; this.state.lastUpdatedAt = prd.lastUpdatedAt;
this.state.paused = prd.paused; this.state.paused = prd.paused;
fs.writeFileSync( fs.writeFileSync(
this.statePath, this.statePath,
JSON.stringify(this.state, null, 2), JSON.stringify(this.state, null, 2),
"utf-8", "utf-8",
); );
} }
/** Mark a task as in progress */ /** Mark a task as in progress */
markInProgress(taskId: string): void { markInProgress(taskId: string): void {
const prd = this.getPRD(); const prd = this.getPRD();
this.ensureTask(prd, taskId); this.ensureTask(prd, taskId);
prd.tasks[taskId].status = "in_progress"; prd.tasks[taskId].status = "in_progress";
prd.tasks[taskId].startedAt = new Date().toISOString(); prd.tasks[taskId].startedAt = new Date().toISOString();
this.save(); this.save();
} }
/** Mark a task as completed */ /** Mark a task as completed */
markCompleted( markCompleted(
taskId: string, taskId: string,
durationMs: number, durationMs: number,
reflection?: Reflection, reflection?: Reflection,
toolUsage?: ToolUsage, toolUsage?: ToolUsage,
sessionFile?: string, sessionFile?: string,
outputPreview?: string, outputPreview?: string,
commitMessages?: string[], commitMessages?: string[],
commitSummary?: string, commitSummary?: string,
): void { ): void {
const prd = this.getPRD(); const prd = this.getPRD();
this.ensureTask(prd, taskId); this.ensureTask(prd, taskId);
prd.tasks[taskId].status = "completed"; prd.tasks[taskId].status = "completed";
prd.tasks[taskId].completedAt = new Date().toISOString(); prd.tasks[taskId].completedAt = new Date().toISOString();
prd.tasks[taskId].durationMs = durationMs; prd.tasks[taskId].durationMs = durationMs;
if (reflection) prd.tasks[taskId].reflection = reflection; if (reflection) prd.tasks[taskId].reflection = reflection;
if (toolUsage) prd.tasks[taskId].toolUsage = toolUsage; if (toolUsage) prd.tasks[taskId].toolUsage = toolUsage;
if (sessionFile) prd.tasks[taskId].sessionFile = sessionFile; if (sessionFile) prd.tasks[taskId].sessionFile = sessionFile;
if (outputPreview) prd.tasks[taskId].outputPreview = outputPreview; if (outputPreview) prd.tasks[taskId].outputPreview = outputPreview;
if (commitMessages) prd.tasks[taskId].commitMessages = commitMessages; if (commitMessages) prd.tasks[taskId].commitMessages = commitMessages;
if (commitSummary) prd.tasks[taskId].commitSummary = commitSummary; if (commitSummary) prd.tasks[taskId].commitSummary = commitSummary;
this.save(); this.save();
} }
/** Mark a task as failed */ /** Mark a task as failed */
markFailed(taskId: string, error: string): void { markFailed(taskId: string, error: string): void {
const prd = this.getPRD(); const prd = this.getPRD();
this.ensureTask(prd, taskId); this.ensureTask(prd, taskId);
prd.tasks[taskId].status = "failed"; prd.tasks[taskId].status = "failed";
prd.tasks[taskId].error = error; prd.tasks[taskId].error = error;
this.save(); this.save();
} }
/** Get task status */ /** Get task status */
getTaskStatus(taskId: string): Task["status"] { getTaskStatus(taskId: string): Task["status"] {
const prd = this.getPRD(); const prd = this.getPRD();
return prd.tasks[taskId]?.status ?? "pending"; return prd.tasks[taskId]?.status ?? "pending";
} }
/** Get IDs of all completed tasks */ /** Get IDs of all completed tasks */
getCompletedTaskIds(): string[] { getCompletedTaskIds(): string[] {
const prd = this.getPRD(); const prd = this.getPRD();
return Object.entries(prd.tasks) return Object.entries(prd.tasks)
.filter(([, info]) => info.status === "completed") .filter(([, info]) => info.status === "completed")
.map(([id]) => id); .map(([id]) => id);
} }
/** Get all reflections from completed tasks */ /** Get all reflections from completed tasks */
getAllReflections(): Reflection[] { getAllReflections(): Reflection[] {
const prd = this.getPRD(); const prd = this.getPRD();
const reflections: Reflection[] = []; const reflections: Reflection[] = [];
for (const info of Object.values(prd.tasks)) { for (const info of Object.values(prd.tasks)) {
if (info.reflection) reflections.push(info.reflection); if (info.reflection) reflections.push(info.reflection);
} }
return reflections; return reflections;
} }
/** Get reflections for specific dependency tasks */ /** Get reflections for specific dependency tasks */
getDependencyReflections(depIds: string[]): Reflection[] { getDependencyReflections(depIds: string[]): Reflection[] {
const prd = this.getPRD(); const prd = this.getPRD();
return depIds return depIds
.map((id) => prd.tasks[id]?.reflection) .map((id) => prd.tasks[id]?.reflection)
.filter((r): r is Reflection => r !== undefined); .filter((r): r is Reflection => r !== undefined);
} }
/** Increment retry count */ /** Increment retry count */
incrementRetry(taskId: string): number { incrementRetry(taskId: string): number {
const prd = this.getPRD(); const prd = this.getPRD();
this.ensureTask(prd, taskId); this.ensureTask(prd, taskId);
prd.tasks[taskId].retries++; prd.tasks[taskId].retries++;
this.save(); this.save();
return prd.tasks[taskId].retries; return prd.tasks[taskId].retries;
} }
/** Set paused state */ /** Set paused state */
setPaused(paused: boolean): void { setPaused(paused: boolean): void {
const prd = this.getPRD(); const prd = this.getPRD();
prd.paused = paused; prd.paused = paused;
this.save(); this.save();
} }
/** Get the raw PRD state (for status display) */ /** Get the raw PRD state (for status display) */
getState(): PRDProgress { getState(): PRDProgress {
return this.getPRD(); return this.getPRD();
} }
/** Get all PRDs (for multi-PRD status display) */ /** Get all PRDs (for multi-PRD status display) */
getAllPRDs(): Record<string, PRDProgress> { getAllPRDs(): Record<string, PRDProgress> {
return this.state.prds ?? {}; return this.state.prds ?? {};
} }
/** Get the PRD key for this tracker */ /** Get the PRD key for this tracker */
getKey(): string { getKey(): string {
return this.prdKey; return this.prdKey;
} }
/** Reset all progress for this PRD */ /** Reset all progress for this PRD */
reset(): void { reset(): void {
const prd = this.getPRD(); const prd = this.getPRD();
Object.assign(prd, this.freshPRD(prd.sourcePath)); Object.assign(prd, this.freshPRD(prd.sourcePath));
this.save(); this.save();
} }
private ensureTask(prd: PRDProgress, taskId: string): void { private ensureTask(prd: PRDProgress, taskId: string): void {
if (!prd.tasks[taskId]) { if (!prd.tasks[taskId]) {
prd.tasks[taskId] = { status: "pending", retries: 0 }; prd.tasks[taskId] = { status: "pending", retries: 0 };
} }
} }
} }

View File

@@ -137,9 +137,9 @@ export interface PRDProgress {
// ─── Configuration ──────────────────────────────────────────────────────────── // ─── Configuration ────────────────────────────────────────────────────────────
export interface RalphConfig { export interface RalpiConfig {
paths: { paths: {
/** Directory for ralph state files */ /** Directory for ralpi state files */
stateDir: string; stateDir: string;
/** Directory for per-task reflections */ /** Directory for per-task reflections */
reflectionsDir: string; reflectionsDir: string;
@@ -162,10 +162,10 @@ export interface RalphConfig {
}; };
} }
export const DEFAULT_CONFIG: RalphConfig = { export const DEFAULT_CONFIG: RalpiConfig = {
paths: { paths: {
stateDir: ".ralph", stateDir: ".ralpi",
reflectionsDir: ".ralph/reflections", reflectionsDir: ".ralpi/reflections",
}, },
execution: { execution: {
maxRetries: 3, maxRetries: 3,

View File

@@ -1,7 +1,7 @@
import * as fs from "node:fs"; import * as fs from "node:fs";
import * as path from "node:path"; import * as path from "node:path";
import type { import type {
RalphConfig, RalpiConfig,
PRDProgress, PRDProgress,
ProgressState, ProgressState,
ToolUsage, ToolUsage,
@@ -39,7 +39,7 @@ export function writeFileSafe(filePath: string, content: string): void {
// ─── Progress Discovery ───────────────────────────────────────────────────── // ─── Progress Discovery ─────────────────────────────────────────────────────
/** /**
* Find the nearest .ralph/progress.json by walking up from the given directory. * Find the nearest .ralpi/progress.json by walking up from the given directory.
* For a specific sourcePath, finds the matching PRD entry. * For a specific sourcePath, finds the matching PRD entry.
*/ */
export function findProgressFile( export function findProgressFile(
@@ -50,7 +50,7 @@ export function findProgressFile(
const root = path.parse(current).root; const root = path.parse(current).root;
while (current !== root) { while (current !== root) {
const candidate = path.join(current, ".ralph", "progress.json"); const candidate = path.join(current, ".ralpi", "progress.json");
if (fs.existsSync(candidate)) { if (fs.existsSync(candidate)) {
try { try {
const raw = fs.readFileSync(candidate, "utf-8"); const raw = fs.readFileSync(candidate, "utf-8");
@@ -113,9 +113,9 @@ function parseSimpleYaml(content: string): Record<string, any> {
* Deep merge configuration objects * Deep merge configuration objects
*/ */
function mergeConfig( function mergeConfig(
defaults: RalphConfig, defaults: RalpiConfig,
overrides: Record<string, any>, overrides: Record<string, any>,
): RalphConfig { ): RalpiConfig {
const result = { ...defaults }; const result = { ...defaults };
for (const [key, value] of Object.entries(overrides)) { for (const [key, value] of Object.entries(overrides)) {
@@ -126,14 +126,14 @@ function mergeConfig(
} }
} }
return result as RalphConfig; return result as RalpiConfig;
} }
/** /**
* Load configuration from .ralph/config.yaml or return defaults * Load configuration from .ralpi/config.yaml or return defaults
*/ */
export function loadConfig(projectDir: string): RalphConfig { export function loadConfig(projectDir: string): RalpiConfig {
const configPath = path.join(projectDir, ".ralph", "config.yaml"); const configPath = path.join(projectDir, ".ralpi", "config.yaml");
// Return defaults silently when config file does not exist // Return defaults silently when config file does not exist
if (!fs.existsSync(configPath)) { if (!fs.existsSync(configPath)) {