From ead5d9be3a2afc6d1ca15d473b7f17df0d106acb Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Sat, 30 May 2026 23:54:11 -0400 Subject: [PATCH] reference updates --- .gitignore | 1 + AGENTS.md | 22 +- README.md | 24 +- index.ts | 972 ++++++++++--------- package-lock.json | 65 -- package.json | 12 +- skills/{ralph-task/SKILL.md => ralpi-use.md} | 14 +- src/constants.ts | 22 +- src/executor.ts | 657 ++++++------- src/progress.ts | 456 ++++----- src/types.ts | 10 +- src/utils.ts | 18 +- 12 files changed, 1119 insertions(+), 1154 deletions(-) delete mode 100644 package-lock.json rename skills/{ralph-task/SKILL.md => ralpi-use.md} (57%) diff --git a/.gitignore b/.gitignore index 8407f3d..8dc9c97 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules dist .pi-lens +package-lock.json diff --git a/AGENTS.md b/AGENTS.md index 57cf9ad..f0eb54b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## 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 @@ -32,22 +32,22 @@ The only real npm dependency is `yaml` (^2.4.0). - `parser.ts` — task file parsing (Fio, checkbox, YAML formats) - `dag.ts` — Kahn's algorithm dependency resolution, batch planning - `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 - `reflection.ts` — reflection extraction from agent output - `utils.ts` — config loading, progress discovery, `runAgentSession()` - `types.ts` — all interfaces and `DEFAULT_CONFIG` - `widget-batcher.ts` — debounced widget updates for parallel tasks -- `skills/ralph-task/SKILL.md` — Pi skill definition for task execution -- `tasks/` — example ralph task files (self-modification history) +- `skills/ralpi-use.md` — Pi skill definition for task execution +- `tasks/` — example ralpi task files (self-modification history) ## Runtime state -All runtime state lives in `.ralph/` (gitignored): -- `.ralph/progress.json` — execution progress, supports multiple PRDs -- `.ralph/reflections/` — per-task reflection JSON files -- `.ralph/prompts/` — generated prompts (timestamped, for debugging) -- `.ralph/sessions/` — full session transcripts +All runtime state lives in `.ralpi/` (gitignored): +- `.ralpi/progress.json` — execution progress, supports multiple PRDs +- `.ralpi/reflections/` — per-task reflection JSON files +- `.ralpi/prompts/` — generated prompts (timestamped, for debugging) +- `.ralpi/sessions/` — full session transcripts ## Task ID convention @@ -55,8 +55,8 @@ Task IDs are zero-padded strings (`"01"`, `"02"`, etc.). The parser prepends `0` ## 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 -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. diff --git a/README.md b/README.md index 8d667d2..238cc19 100644 --- a/README.md +++ b/README.md @@ -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 - **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 - **Retry with backoff**: Failed tasks retry with exponential backoff - **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 ``` -/ralph plan [task-file] # Show execution plan -/ralph run [task-file] # Execute all tasks -/ralph status [task-file] # Show current progress -/ralph resume [task-file] # Resume paused execution -/ralph next [task-file] # Execute next batch only -/ralph reset [task-file] # Reset all progress +/ralpi plan [task-file] # Show execution plan +/ralpi run [task-file] # Execute all tasks +/ralpi status [task-file] # Show current progress +/ralpi resume [task-file] # Resume paused execution +/ralpi next [task-file] # Execute next batch only +/ralpi reset [task-file] # Reset all progress ``` ## Task File Formats @@ -98,7 +98,7 @@ tasks: ## Configuration -Create `.ralph/config.yaml`: +Create `.ralpi/config.yaml`: ```yaml maxRetries: 3 @@ -121,7 +121,7 @@ Supported formats: `10m` (minutes), `600s` (seconds), `3600000` (milliseconds) ## State Files -- `.ralph/progress.json` - Execution progress -- `.ralph/reflections/` - Per-task reflections -- `.ralph/prompts/` - Generated prompts -- `.ralph/sessions/` - Full task output for review +- `.ralpi/progress.json` - Execution progress +- `.ralpi/reflections/` - Per-task reflections +- `.ralpi/prompts/` - Generated prompts +- `.ralpi/sessions/` - Full task output for review diff --git a/index.ts b/index.ts index b658a79..614b25c 100644 --- a/index.ts +++ b/index.ts @@ -1,27 +1,27 @@ import * as fs from "node:fs"; import * as path from "node:path"; import type { - ExtensionAPI, - ExtensionContext, + ExtensionAPI, + ExtensionContext, } from "@earendil-works/pi-coding-agent"; import { Box, Text } from "@earendil-works/pi-tui"; import { parseTaskFile, updateTaskInFile } from "./src/parser"; import { - buildExecutionPlan, - buildSequentialPlan, - formatExecutionPlan, - getReadyTasks, + buildExecutionPlan, + buildSequentialPlan, + formatExecutionPlan, + getReadyTasks, } from "./src/dag"; import { ProgressTracker } from "./src/progress"; import { buildPlanPrompt } from "./src/prompts"; import { formatReflections } from "./src/reflection"; import { executeBatch, type SendChatMessage } from "./src/executor"; import { - loadConfig, - resolveTaskArg, - formatProgressStatus, - formatAllPRDsStatus, - findProgressFile, + loadConfig, + resolveTaskArg, + formatProgressStatus, + formatAllPRDsStatus, + findProgressFile, } from "./src/utils"; const COMMANDS = ["status", "resume", "next", "reset"] as const; @@ -35,581 +35,583 @@ type ExecutionMode = "parallel" | "sequential"; * Matches: @path, /path, ./path, ../path, path/to/file, path.md, path.yaml */ function looksLikePath(token: string): boolean { - return ( - token.startsWith("@") || - token.startsWith("/") || - token.startsWith("./") || - token.startsWith("../") || - token.includes("/") || - token.endsWith(".md") || - token.endsWith(".yaml") || - token.endsWith(".yml") - ); + return ( + token.startsWith("@") || + token.startsWith("/") || + token.startsWith("./") || + token.startsWith("../") || + token.includes("/") || + token.endsWith(".md") || + token.endsWith(".yaml") || + token.endsWith(".yml") + ); } /** Build the set of completed tasks from progress tracker and PRD checkboxes. */ function buildCompletedSet( - progress: ProgressTracker, - project: import("./src/types").Project, + progress: ProgressTracker, + project: import("./src/types").Project, ): Set { - const completed = new Set(progress.getCompletedTaskIds()); - for (const task of project.tasks) { - if (task.status === "completed") { - completed.add(task.id); - } - } - return completed; + const completed = new Set(progress.getCompletedTaskIds()); + for (const task of project.tasks) { + if (task.status === "completed") { + completed.add(task.id); + } + } + return completed; } /** Prompt user to select an execution mode with dependency validation. */ async function selectExecutionMode( - ctx: ExtensionContext, - project: import("./src/types").Project, - taskFile: string, + ctx: ExtensionContext, + project: import("./src/types").Project, + taskFile: string, ): Promise { - const mode = await ctx.ui.select("Execution mode for this run?", [ - "Parallel (where dependencies allow)", - "Sequential (one at a time)", - ]); - const isParallel = mode?.startsWith("Parallel") ?? false; + const mode = await ctx.ui.select("Execution mode for this run?", [ + "Parallel (where dependencies allow)", + "Sequential (one at a time)", + ]); + const isParallel = mode?.startsWith("Parallel") ?? false; - if (!isParallel) return "sequential"; + if (!isParallel) return "sequential"; - // Validate dependency graph for parallel mode - if (Object.keys(project.dependencies).length === 0) { - const hasDepsSection = await fs.promises - .readFile(taskFile, "utf-8") - .then((content) => /^##\s+Dependencies\s*$/m.test(content)) - .catch(() => false); + // Validate dependency graph for parallel mode + if (Object.keys(project.dependencies).length === 0) { + const hasDepsSection = await fs.promises + .readFile(taskFile, "utf-8") + .then((content) => /^##\s+Dependencies\s*$/m.test(content)) + .catch(() => false); - if (hasDepsSection) { - const choice = await ctx.ui.select( - "Found ## Dependencies section but no valid dependencies were parsed.\n\n" + - "This may be due to unsupported format. Parallel mode requires explicit dependencies.\n\n" + - "See README.md for supported dependency formats:\n" + - "- Arrow notation: `1 -> 2,3,4`\n" + - "- Natural language: `13 depends on 17, 18, 19, 20`\n\n" + - "Fall back to sequential mode?", - ["Yes, use sequential", "No, continue with parallel"], - ); - if (choice?.startsWith("Yes")) { - return "sequential"; - } - } - } + if (hasDepsSection) { + const choice = await ctx.ui.select( + "Found ## Dependencies section but no valid dependencies were parsed.\n\n" + + "This may be due to unsupported format. Parallel mode requires explicit dependencies.\n\n" + + "See README.md for supported dependency formats:\n" + + "- Arrow notation: `1 -> 2,3,4`\n" + + "- Natural language: `13 depends on 17, 18, 19, 20`\n\n" + + "Fall back to sequential mode?", + ["Yes, use sequential", "No, continue with parallel"], + ); + if (choice?.startsWith("Yes")) { + return "sequential"; + } + } + } - return "parallel"; + return "parallel"; } /** Build an execution plan based on the selected mode. */ function buildPlanByMode( - mode: ExecutionMode, - project: Parameters[0], - completed: Set, + mode: ExecutionMode, + project: Parameters[0], + completed: Set, ) { - return mode === "parallel" - ? buildExecutionPlan(project, completed) - : buildSequentialPlan(project, completed); + return mode === "parallel" + ? buildExecutionPlan(project, completed) + : buildSequentialPlan(project, completed); } /** Run all batches in a plan, updating the task file after each batch. */ async function executePlanBatches( - plan: ReturnType, - project: Parameters[0], - taskFile: string, - config: import("./src/types").RalphConfig, - progress: ProgressTracker, - ctx: ExtensionContext, - mode: ExecutionMode, - sendChatMessage?: SendChatMessage, - projectDir?: string, + plan: ReturnType, + project: Parameters[0], + taskFile: string, + config: import("./src/types").ralpiConfig, + progress: ProgressTracker, + ctx: ExtensionContext, + mode: ExecutionMode, + sendChatMessage?: SendChatMessage, + projectDir?: string, ): Promise { - for (const batch of plan.batches) { - if (progress.getState().paused) { - ctx.ui.notify( - "Execution paused. Use /ralph resume to continue.", - "warning", - ); - return; - } + for (const batch of plan.batches) { + if (progress.getState().paused) { + ctx.ui.notify( + "Execution paused. Use /ralpi resume to continue.", + "warning", + ); + return; + } - if (!Array.isArray(batch.tasks)) { - throw new Error( - `Batch ${batch.batchIndex} has invalid tasks: expected array, got ${typeof batch.tasks}`, - ); - } + if (!Array.isArray(batch.tasks)) { + throw new Error( + `Batch ${ + batch.batchIndex + } has invalid tasks: expected array, got ${typeof batch.tasks}`, + ); + } - await executeBatch( - batch.tasks, - project, - config, - progress, - ctx, - { parallel: mode === "parallel" }, - sendChatMessage, - projectDir, - ); + await executeBatch( + batch.tasks, + project, + config, + progress, + ctx, + { parallel: mode === "parallel" }, + sendChatMessage, + projectDir, + ); - for (const task of batch.tasks) { - const status = progress.getTaskStatus(task.id); - updateTaskInFile(taskFile, task.id, status); - } - } + for (const task of batch.tasks) { + const status = progress.getTaskStatus(task.id); + updateTaskInFile(taskFile, task.id, status); + } + } } // ─── Extension Entry ──────────────────────────────────────────────────────── -export default function ralphLoopExtension(pi: ExtensionAPI): void { - // Register custom message renderer for ralph progress messages. - // Renders an expandable tool-call tree: collapsed shows last 3 + "N more", - // expanded (Ctrl+O) shows every tool call. - pi.registerMessageRenderer( - "ralph-progress", - (message, { expanded }, theme) => { - const details = message.details as - | { - phase?: string; - toolCalls?: Array<{ name: string; label: string }>; - } - | undefined; +export default function ralpiLoopExtension(pi: ExtensionAPI): void { + // Register custom message renderer for ralpi progress messages. + // Renders an expandable tool-call tree: collapsed shows last 3 + "N more", + // expanded (Ctrl+O) shows every tool call. + pi.registerMessageRenderer( + "ralpi-progress", + (message, { expanded }, theme) => { + const details = message.details as + | { + phase?: string; + toolCalls?: Array<{ name: string; label: string }>; + } + | undefined; - const MAX_COLLAPSED = 3; - const lines: string[] = []; + const MAX_COLLAPSED = 3; + const lines: string[] = []; - // Header line — e.g. "✓ 05 · billing-subscriptions-trials (2m 14s)" - lines.push(String(message.content)); + // Header line — e.g. "✓ 05 · billing-subscriptions-trials (2m 14s)" + lines.push(String(message.content)); - // Build tool-call tree - if (details?.toolCalls && details.toolCalls.length > 0) { - const all = details.toolCalls; + // Build tool-call tree + if (details?.toolCalls && details.toolCalls.length > 0) { + const all = details.toolCalls; - if (expanded) { - // Expanded: show ALL tool calls - for (let i = 0; i < all.length; i++) { - const entry = all[i]; - const isLast = i === all.length - 1; - const branch = isLast ? " └── " : " ├── "; - const tag = theme.fg("accent", `[${entry.name}]`); - lines.push(`${branch}${tag} ${entry.label}`); - } - } else { - // Collapsed: last N + "X more" - const shown = all.slice(-MAX_COLLAPSED); - const remaining = all.length - shown.length; + if (expanded) { + // Expanded: show ALL tool calls + for (let i = 0; i < all.length; i++) { + const entry = all[i]; + const isLast = i === all.length - 1; + const branch = isLast ? " └── " : " ├── "; + const tag = theme.fg("accent", `[${entry.name}]`); + lines.push(`${branch}${tag} ${entry.label}`); + } + } else { + // Collapsed: last N + "X more" + const shown = all.slice(-MAX_COLLAPSED); + const remaining = all.length - shown.length; - if (remaining > 0) { - lines.push(theme.fg("dim", ` ├── ${remaining} more`)); - } + if (remaining > 0) { + lines.push(theme.fg("dim", ` ├── ${remaining} more`)); + } - for (let i = 0; i < shown.length; i++) { - const entry = shown[i]; - const isLast = i === shown.length - 1; - const branch = isLast ? " └── " : " ├── "; - const tag = theme.fg("accent", `[${entry.name}]`); - lines.push(`${branch}${tag} ${entry.label}`); - } - } - } + for (let i = 0; i < shown.length; i++) { + const entry = shown[i]; + const isLast = i === shown.length - 1; + const branch = isLast ? " └── " : " ├── "; + const tag = theme.fg("accent", `[${entry.name}]`); + lines.push(`${branch}${tag} ${entry.label}`); + } + } + } - const text = lines.join("\n"); - const box = new Box(1, 1, (t) => theme.bg("customMessageBg", t)); - box.addChild(new Text(text, 0, 0)); - return box; - }, - ); + const text = lines.join("\n"); + const box = new Box(1, 1, (t) => theme.bg("customMessageBg", t)); + box.addChild(new Text(text, 0, 0)); + return box; + }, + ); - pi.registerCommand("ralph", { - description: - "Execute tasks from a task file using DAG-based dependency resolution", - handler: async (args: string, ctx: ExtensionContext) => { - const parts = (args || "").trim().split(/\s+/).filter(Boolean); + pi.registerCommand("ralpi", { + description: + "Execute tasks from a task file using DAG-based dependency resolution", + handler: async (args: string, ctx: ExtensionContext) => { + const parts = (args || "").trim().split(/\s+/).filter(Boolean); - // Wraps pi.sendMessage() for posting status to the chat history. - // Uses "ralph-progress" customType with a "progress" phase so the - // renderer omits the label prefix entirely (no [INFO] etc.). - // Accepts an optional meta object with toolCalls for the expandable view. - const sendProgress: SendChatMessage = ( - content: string, - meta?: { toolCalls?: Array<{ name: string; label: string }> }, - ) => { - pi.sendMessage({ - customType: "ralph-progress", - content, - display: true, - details: { phase: "progress", toolCalls: meta?.toolCalls }, - }); - }; + // Wraps pi.sendMessage() for posting status to the chat history. + // Uses "ralpi-progress" customType with a "progress" phase so the + // renderer omits the label prefix entirely (no [INFO] etc.). + // Accepts an optional meta object with toolCalls for the expandable view. + const sendProgress: SendChatMessage = ( + content: string, + meta?: { toolCalls?: Array<{ name: string; label: string }> }, + ) => { + pi.sendMessage({ + customType: "ralpi-progress", + content, + display: true, + details: { phase: "progress", toolCalls: meta?.toolCalls }, + }); + }; - // If no args, show plan. If first token looks like a path (@path, /path, ./path), - // route to run so the execution mode prompt fires. - if (parts.length === 0) { - return handlePlan(ctx, parts); - } - if (looksLikePath(parts[0])) { - return handleRun(ctx, parts, sendProgress); - } + // If no args, show plan. If first token looks like a path (@path, /path, ./path), + // route to run so the execution mode prompt fires. + if (parts.length === 0) { + return handlePlan(ctx, parts); + } + if (looksLikePath(parts[0])) { + return handleRun(ctx, parts, sendProgress); + } - const command = parts[0]; - switch (command) { - case "run": - return handleRun(ctx, parts.slice(1), sendProgress); - case "plan": - return handlePlan(ctx, parts.slice(1)); - case "status": - return handleStatus(ctx, parts.slice(1)); - case "resume": - return handleResume(ctx, parts.slice(1), sendProgress); - case "next": - return handleNext(ctx, parts.slice(1), sendProgress); - case "reset": - return handleReset(ctx, parts.slice(1)); - default: { - // Auto-discover progress and offer resume - const found = findProgressFile(process.cwd()); - if (found) { - ctx.ui.notify( - `Unknown command: ${command}\n\nFound existing progress in ${ - found.path - }\nUse /ralph resume to continue.\n\nAvailable: ${COMMANDS.join( - ", ", - )}`, - "warning", - ); - } else { - ctx.ui.notify( - `Unknown command: ${command}\nAvailable: ${COMMANDS.join(", ")}`, - "error", - ); - } - } - } - }, - }); + const command = parts[0]; + switch (command) { + case "run": + return handleRun(ctx, parts.slice(1), sendProgress); + case "plan": + return handlePlan(ctx, parts.slice(1)); + case "status": + return handleStatus(ctx, parts.slice(1)); + case "resume": + return handleResume(ctx, parts.slice(1), sendProgress); + case "next": + return handleNext(ctx, parts.slice(1), sendProgress); + case "reset": + return handleReset(ctx, parts.slice(1)); + default: { + // Auto-discover progress and offer resume + const found = findProgressFile(process.cwd()); + if (found) { + ctx.ui.notify( + `Unknown command: ${command}\n\nFound existing progress in ${ + found.path + }\nUse /ralpi resume to continue.\n\nAvailable: ${COMMANDS.join( + ", ", + )}`, + "warning", + ); + } else { + ctx.ui.notify( + `Unknown command: ${command}\nAvailable: ${COMMANDS.join(", ")}`, + "error", + ); + } + } + } + }, + }); } -// ─── /ralph plan ───────────────────────────────────────────────────────────── +// ─── /ralpi plan ───────────────────────────────────────────────────────────── async function handlePlan( - ctx: ExtensionContext, - args: string[], + ctx: ExtensionContext, + args: string[], ): Promise { - const taskFile = resolveTaskArg(args[0] || "README.md", process.cwd()); - const project = parseTaskFile(taskFile); - if (!Array.isArray(project.tasks)) { - throw new Error( - `Parsed project from ${taskFile} has invalid tasks: expected array, got ${typeof project.tasks}`, - ); - } + const taskFile = resolveTaskArg(args[0] || "README.md", process.cwd()); + const project = parseTaskFile(taskFile); + if (!Array.isArray(project.tasks)) { + throw new Error( + `Parsed project from ${taskFile} has invalid tasks: expected array, got ${typeof project.tasks}`, + ); + } - const planPrompt = buildPlanPrompt(project); - const plan = buildExecutionPlan(project, new Set()); - const formatted = formatExecutionPlan(plan); + const planPrompt = buildPlanPrompt(project); + const plan = buildExecutionPlan(project, new Set()); + const formatted = formatExecutionPlan(plan); - ctx.ui.notify(`${planPrompt}\n\n${formatted}`, "info"); + ctx.ui.notify(`${planPrompt}\n\n${formatted}`, "info"); } -// ─── /ralph run ────────────────────────────────────────────────────────────── +// ─── /ralpi run ────────────────────────────────────────────────────────────── async function handleRun( - ctx: ExtensionContext, - args: string[], - sendChatMessage?: SendChatMessage, + ctx: ExtensionContext, + args: string[], + sendChatMessage?: SendChatMessage, ): Promise { - const taskFile = resolveTaskArg(args[0] || "README.md", process.cwd()); + const taskFile = resolveTaskArg(args[0] || "README.md", process.cwd()); - // If targeting a specific task file and there's existing progress for it, - // auto-resume instead of starting fresh - const existingProgress = findProgressFile(process.cwd(), taskFile); - if (existingProgress) { - return handleResume(ctx, args.slice(0, 1), sendChatMessage); - } + // If targeting a specific task file and there's existing progress for it, + // auto-resume instead of starting fresh + const existingProgress = findProgressFile(process.cwd(), taskFile); + if (existingProgress) { + return handleResume(ctx, args.slice(0, 1), sendChatMessage); + } - // No existing progress for this task — check for any progress at all - const found = findProgressFile(process.cwd()); - if (found && !args[0]) { - // Offer to resume instead of starting fresh - const shouldResume = await ctx.ui.select( - "Found existing ralph progress. Resume?", - ["Yes, resume", "No, start fresh"], - ); + // No existing progress for this task — check for any progress at all + const found = findProgressFile(process.cwd()); + if (found && !args[0]) { + // Offer to resume instead of starting fresh + const shouldResume = await ctx.ui.select( + "Found existing ralpi progress. Resume?", + ["Yes, resume", "No, start fresh"], + ); - if (shouldResume?.startsWith("Yes")) { - return handleResume(ctx, [], sendChatMessage); - } - } + if (shouldResume?.startsWith("Yes")) { + return handleResume(ctx, [], sendChatMessage); + } + } - const projectDir = found - ? path.dirname(path.dirname(found.path)) - : process.cwd(); + const projectDir = found + ? path.dirname(path.dirname(found.path)) + : process.cwd(); - const project = parseTaskFile(taskFile); - const config = loadConfig(projectDir); - const progress = new ProgressTracker(projectDir, taskFile); + const project = parseTaskFile(taskFile); + const config = loadConfig(projectDir); + const progress = new ProgressTracker(projectDir, taskFile); - // Set initial status - ctx.ui.setStatus( - "ralph", - `Starting ${project.tasks.length} tasks from ${path.basename(taskFile)}`, - ); + // Set initial status + ctx.ui.setStatus( + "ralpi", + `Starting ${project.tasks.length} tasks from ${path.basename(taskFile)}`, + ); - const completed = buildCompletedSet(progress, project); - const mode = await selectExecutionMode(ctx, project, taskFile); - const plan = buildPlanByMode(mode, project, completed); + const completed = buildCompletedSet(progress, project); + const mode = await selectExecutionMode(ctx, project, taskFile); + const plan = buildPlanByMode(mode, project, completed); - // Show execution plan before starting so user can see batch breakdown - const formattedPlan = formatExecutionPlan(plan); - ctx.ui.notify(`${formattedPlan}\n\nStarting ${mode} execution...`, "info"); + // Show execution plan before starting so user can see batch breakdown + const formattedPlan = formatExecutionPlan(plan); + ctx.ui.notify(`${formattedPlan}\n\nStarting ${mode} execution...`, "info"); - await executePlanBatches( - plan, - project, - taskFile, - config, - progress, - ctx, - mode, - sendChatMessage, - projectDir, - ); + await executePlanBatches( + plan, + project, + taskFile, + config, + progress, + ctx, + mode, + sendChatMessage, + projectDir, + ); - const state = progress.getState(); - const output = formatProgressStatus(state); + const state = progress.getState(); + const output = formatProgressStatus(state); - const reflections = progress.getAllReflections(); - if (reflections.length > 0) { - ctx.ui.notify(`${output}\n\n${formatReflections(reflections)}`, "info"); - return; - } + const reflections = progress.getAllReflections(); + if (reflections.length > 0) { + ctx.ui.notify(`${output}\n\n${formatReflections(reflections)}`, "info"); + return; + } - ctx.ui.notify(output, "info"); + ctx.ui.notify(output, "info"); } -// ─── /ralph status ─────────────────────────────────────────────────────────── +// ─── /ralpi status ─────────────────────────────────────────────────────────── async function handleStatus( - ctx: ExtensionContext, - args: string[], + ctx: ExtensionContext, + args: string[], ): Promise { - if (args[0]) { - const taskFile = resolveTaskArg(args[0], process.cwd()); - const existingProgress = findProgressFile(process.cwd(), taskFile); - if (existingProgress) { - const projectDir = path.dirname(path.dirname(existingProgress.path)); - const progress = new ProgressTracker( - projectDir, - taskFile, - existingProgress.prdKey, - ); - ctx.ui.notify(formatProgressStatus(progress.getState()), "info"); - return; - } - // No progress yet for this task — parse and show plan instead - const project = parseTaskFile(taskFile); - ctx.ui.notify( - `No progress for ${path.basename(taskFile)}. ${ - project.tasks.length - } tasks found.\nUse /ralph run ${args[0]} to start.`, - "info", - ); - return; - } + if (args[0]) { + const taskFile = resolveTaskArg(args[0], process.cwd()); + const existingProgress = findProgressFile(process.cwd(), taskFile); + if (existingProgress) { + const projectDir = path.dirname(path.dirname(existingProgress.path)); + const progress = new ProgressTracker( + projectDir, + taskFile, + existingProgress.prdKey, + ); + ctx.ui.notify(formatProgressStatus(progress.getState()), "info"); + return; + } + // No progress yet for this task — parse and show plan instead + const project = parseTaskFile(taskFile); + ctx.ui.notify( + `No progress for ${path.basename(taskFile)}. ${ + project.tasks.length + } tasks found.\nUse /ralpi run ${args[0]} to start.`, + "info", + ); + return; + } - const found = findProgressFile(process.cwd()); - if (!found) { - ctx.ui.notify( - "No .ralph/progress.json found. Start with /ralph run [task-file]", - "warning", - ); - return; - } + const found = findProgressFile(process.cwd()); + if (!found) { + ctx.ui.notify( + "No .ralpi/progress.json found. Start with /ralpi run [task-file]", + "warning", + ); + return; + } - ctx.ui.notify(formatAllPRDsStatus(found.state), "info"); + ctx.ui.notify(formatAllPRDsStatus(found.state), "info"); } -// ─── /ralph resume ─────────────────────────────────────────────────────────── +// ─── /ralpi resume ─────────────────────────────────────────────────────────── async function handleResume( - ctx: ExtensionContext, - args: string[], - sendChatMessage?: SendChatMessage, + ctx: ExtensionContext, + args: string[], + sendChatMessage?: SendChatMessage, ): Promise { - let taskFile: string; - let projectDir: string; - let found: ReturnType; + let taskFile: string; + let projectDir: string; + let found: ReturnType; - if (args[0]) { - taskFile = resolveTaskArg(args[0], process.cwd()); - found = findProgressFile(process.cwd(), taskFile); - if (!found) { - ctx.ui.notify( - `No existing progress for ${args[0]}. Start with /ralph run ${args[0]}`, - "warning", - ); - return; - } - projectDir = path.dirname(path.dirname(found.path)); - } else { - found = findProgressFile(process.cwd()); - if (!found) { - ctx.ui.notify( - "No .ralph/progress.json found. Start with /ralph run [task-file]", - "warning", - ); - return; - } - projectDir = path.dirname(path.dirname(found.path)); - // For no-arg resume, use the first PRD's source path or legacy sourcePath - taskFile = found.state.prds - ? Object.values(found.state.prds)[0].sourcePath - : found.state.sourcePath; - } + if (args[0]) { + taskFile = resolveTaskArg(args[0], process.cwd()); + found = findProgressFile(process.cwd(), taskFile); + if (!found) { + ctx.ui.notify( + `No existing progress for ${args[0]}. Start with /ralpi run ${args[0]}`, + "warning", + ); + return; + } + projectDir = path.dirname(path.dirname(found.path)); + } else { + found = findProgressFile(process.cwd()); + if (!found) { + ctx.ui.notify( + "No .ralpi/progress.json found. Start with /ralpi run [task-file]", + "warning", + ); + return; + } + projectDir = path.dirname(path.dirname(found.path)); + // For no-arg resume, use the first PRD's source path or legacy sourcePath + taskFile = found.state.prds + ? Object.values(found.state.prds)[0].sourcePath + : found.state.sourcePath; + } - const project = parseTaskFile(taskFile); - if (!Array.isArray(project.tasks)) { - throw new Error( - `Parsed project from ${taskFile} has invalid tasks: expected array, got ${typeof project.tasks}`, - ); - } - const config = loadConfig(projectDir); - const progress = new ProgressTracker(projectDir, taskFile, found.prdKey); + const project = parseTaskFile(taskFile); + if (!Array.isArray(project.tasks)) { + throw new Error( + `Parsed project from ${taskFile} has invalid tasks: expected array, got ${typeof project.tasks}`, + ); + } + const config = loadConfig(projectDir); + const progress = new ProgressTracker(projectDir, taskFile, found.prdKey); - progress.setPaused(false); + progress.setPaused(false); - // Set resume status - ctx.ui.setStatus("ralph", `Resuming from ${path.basename(taskFile)}`); + // Set resume status + ctx.ui.setStatus("ralpi", `Resuming from ${path.basename(taskFile)}`); - const completed = buildCompletedSet(progress, project); - const mode = await selectExecutionMode(ctx, project, taskFile); - const plan = buildPlanByMode(mode, project, completed); + const completed = buildCompletedSet(progress, project); + const mode = await selectExecutionMode(ctx, project, taskFile); + const plan = buildPlanByMode(mode, project, completed); - await executePlanBatches( - plan, - project, - taskFile, - config, - progress, - ctx, - mode, - sendChatMessage, - projectDir, - ); + await executePlanBatches( + plan, + project, + taskFile, + config, + progress, + ctx, + mode, + sendChatMessage, + projectDir, + ); - ctx.ui.notify(formatProgressStatus(progress.getState()), "info"); + ctx.ui.notify(formatProgressStatus(progress.getState()), "info"); } -// ─── /ralph next ───────────────────────────────────────────────────────────── +// ─── /ralpi next ───────────────────────────────────────────────────────────── async function handleNext( - ctx: ExtensionContext, - args: string[], - sendChatMessage?: SendChatMessage, + ctx: ExtensionContext, + args: string[], + sendChatMessage?: SendChatMessage, ): Promise { - let taskFile: string; - let projectDir: string; - let found: ReturnType; + let taskFile: string; + let projectDir: string; + let found: ReturnType; - if (args[0]) { - taskFile = resolveTaskArg(args[0], process.cwd()); - found = findProgressFile(process.cwd(), taskFile); - if (found) { - projectDir = path.dirname(path.dirname(found.path)); - } else { - projectDir = process.cwd(); - } - } else { - found = findProgressFile(process.cwd()); - if (!found) { - ctx.ui.notify( - "No .ralph/progress.json found. Start with /ralph run [task-file]", - "warning", - ); - return; - } - taskFile = found.state.prds - ? Object.values(found.state.prds)[0].sourcePath - : found.state.sourcePath; - projectDir = path.dirname(path.dirname(found.path)); - } + if (args[0]) { + taskFile = resolveTaskArg(args[0], process.cwd()); + found = findProgressFile(process.cwd(), taskFile); + if (found) { + projectDir = path.dirname(path.dirname(found.path)); + } else { + projectDir = process.cwd(); + } + } else { + found = findProgressFile(process.cwd()); + if (!found) { + ctx.ui.notify( + "No .ralpi/progress.json found. Start with /ralpi run [task-file]", + "warning", + ); + return; + } + taskFile = found.state.prds + ? Object.values(found.state.prds)[0].sourcePath + : found.state.sourcePath; + projectDir = path.dirname(path.dirname(found.path)); + } - const project = parseTaskFile(taskFile); - if (!Array.isArray(project.tasks)) { - throw new Error( - `Parsed project from ${taskFile} has invalid tasks: expected array, got ${typeof project.tasks}`, - ); - } - const config = loadConfig(projectDir); - const progress = new ProgressTracker(projectDir, taskFile, found?.prdKey); + const project = parseTaskFile(taskFile); + if (!Array.isArray(project.tasks)) { + throw new Error( + `Parsed project from ${taskFile} has invalid tasks: expected array, got ${typeof project.tasks}`, + ); + } + const config = loadConfig(projectDir); + const progress = new ProgressTracker(projectDir, taskFile, found?.prdKey); - const completed = buildCompletedSet(progress, project); - const ready = getReadyTasks(project, completed); + const completed = buildCompletedSet(progress, project); + const ready = getReadyTasks(project, completed); - if (ready.length === 0) { - ctx.ui.notify( - "No tasks ready to execute. All tasks completed or blocked.", - "info", - ); - return; - } + if (ready.length === 0) { + ctx.ui.notify( + "No tasks ready to execute. All tasks completed or blocked.", + "info", + ); + return; + } - const nextBatch = ready.slice( - 0, - config.execution.maxParallel || ready.length, - ); + const nextBatch = ready.slice( + 0, + config.execution.maxParallel || ready.length, + ); - for (const task of nextBatch) { - await executeBatch( - [task], - project, - config, - progress, - ctx, - { parallel: false }, - sendChatMessage, - projectDir, - ); - updateTaskInFile(taskFile, task.id, progress.getTaskStatus(task.id)); - } + for (const task of nextBatch) { + await executeBatch( + [task], + project, + config, + progress, + ctx, + { parallel: false }, + sendChatMessage, + projectDir, + ); + updateTaskInFile(taskFile, task.id, progress.getTaskStatus(task.id)); + } - ctx.ui.notify( - `Executed: ${nextBatch - .map((t) => t.id) - .join(", ")}\n\n${formatProgressStatus(progress.getState())}`, - "info", - ); + ctx.ui.notify( + `Executed: ${nextBatch + .map((t) => t.id) + .join(", ")}\n\n${formatProgressStatus(progress.getState())}`, + "info", + ); } -// ─── /ralph reset ──────────────────────────────────────────────────────────── +// ─── /ralpi reset ──────────────────────────────────────────────────────────── async function handleReset( - ctx: ExtensionContext, - args: string[], + ctx: ExtensionContext, + args: string[], ): Promise { - if (args[0]) { - const taskFile = resolveTaskArg(args[0], process.cwd()); - const found = findProgressFile(process.cwd(), taskFile); - const projectDir = found - ? path.dirname(path.dirname(found.path)) - : process.cwd(); - const progress = new ProgressTracker(projectDir, taskFile, found?.prdKey); - progress.reset(); - } else { - const found = findProgressFile(process.cwd()); - if (!found) { - ctx.ui.notify( - "No .ralph/progress.json found. Start with /ralph run [task-file]", - "warning", - ); - return; - } - const projectDir = path.dirname(path.dirname(found.path)); - const progress = new ProgressTracker( - projectDir, - found.state.prds - ? Object.values(found.state.prds)[0].sourcePath - : found.state.sourcePath, - ); - progress.reset(); - } + if (args[0]) { + const taskFile = resolveTaskArg(args[0], process.cwd()); + const found = findProgressFile(process.cwd(), taskFile); + const projectDir = found + ? path.dirname(path.dirname(found.path)) + : process.cwd(); + const progress = new ProgressTracker(projectDir, taskFile, found?.prdKey); + progress.reset(); + } else { + const found = findProgressFile(process.cwd()); + if (!found) { + ctx.ui.notify( + "No .ralpi/progress.json found. Start with /ralpi run [task-file]", + "warning", + ); + return; + } + const projectDir = path.dirname(path.dirname(found.path)); + const progress = new ProgressTracker( + projectDir, + found.state.prds + ? Object.values(found.state.prds)[0].sourcePath + : found.state.sourcePath, + ); + progress.reset(); + } - ctx.ui.notify("Progress reset. All task statuses cleared.", "info"); + ctx.ui.notify("Progress reset. All task statuses cleared.", "info"); } diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 692db40..0000000 --- a/package-lock.json +++ /dev/null @@ -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" - } - } - } -} diff --git a/package.json b/package.json index 0843f72..634e0f9 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "ralph-loop", + "name": "ralpi-loop", "version": "1.0.0", "description": "Execute tasks from task files using DAG-based dependency resolution with persistent progress tracking", "main": "dist/index.js", @@ -8,7 +8,9 @@ "pi-extension", "task-runner", "dag", - "task-manager" + "task-manager", + "ralpi-loop", + "prd" ], "author": "", "license": "MIT", @@ -24,9 +26,9 @@ "prepublishOnly": "npm run build" }, "pi": { - "extensions": ["./dist/index.js"], - "skills": ["./skills"], - "prompts": ["./prompts"] + "extensions": [ + "./dist/index.js" + ] }, "dependencies": { "yaml": "^2.4.0" diff --git a/skills/ralph-task/SKILL.md b/skills/ralpi-use.md similarity index 57% rename from skills/ralph-task/SKILL.md rename to skills/ralpi-use.md index 767c7d4..342ca0f 100644 --- a/skills/ralph-task/SKILL.md +++ b/skills/ralpi-use.md @@ -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 @@ -11,9 +15,9 @@ Execute a single task from a ralph task file. ## Usage ``` -/ralph run [task-file] # Run all tasks -/ralph next [task-file] # Run next batch -/ralph status [task-file] # Check progress +/ralpi run [task-file] # Run all tasks +/ralpi next [task-file] # Run next batch +/ralpi status [task-file] # Check progress ``` ## Task File Location diff --git a/src/constants.ts b/src/constants.ts index 2a7868c..c63378b 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,19 +1,25 @@ -import type { RalphConfig } from "./types"; import { DEFAULT_CONFIG } from "./types"; export { DEFAULT_CONFIG }; // CLI -export const SLASH_COMMAND = "/ralph"; -export const COMMANDS = ["run", "plan", "status", "resume", "next", "reset"] as const; +export const SLASH_COMMAND = "/ralpi"; +export const COMMANDS = [ + "run", + "plan", + "status", + "resume", + "next", + "reset", +] as const; // Task file detection export const TASK_FILE_NAMES = [ - "README.md", - "PRD.md", - "tasks.md", - "tasks.yaml", - "tasks.yml", + "README.md", + "PRD.md", + "tasks.md", + "tasks.yaml", + "tasks.yml", ] as const; // Reflection parsing diff --git a/src/executor.ts b/src/executor.ts index 96ac64c..0e31f72 100644 --- a/src/executor.ts +++ b/src/executor.ts @@ -1,29 +1,29 @@ import * as path from "node:path"; 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 { ExtensionContext } from "@earendil-works/pi-coding-agent"; import { buildTaskPrompt } from "./prompts"; import { extractReflection } from "./reflection"; import { WidgetBatcher } from "./widget-batcher"; import { - runAgentSession, - writeFileSafe, - ensureDir, - captureGitCommits, - formatDuration, + runAgentSession, + writeFileSafe, + ensureDir, + captureGitCommits, + formatDuration, } from "./utils"; /** Optional callback to post a progress message into the chat history. */ export type SendChatMessage = ( - content: string, - /** Extra data passed to the message renderer for the expanded view. */ - meta?: { toolCalls?: ToolCallEntry[] }, + content: string, + /** Extra data passed to the message renderer for the expanded view. */ + meta?: { toolCalls?: ToolCallEntry[] }, ) => void; export interface ToolCallEntry { - name: string; - label: string; + name: string; + label: string; } // ─── Run Single Task ──────────────────────────────────────────────────────── @@ -33,176 +33,176 @@ export interface ToolCallEntry { * Non-blocking — the TUI remains responsive throughout. */ export async function runTask( - task: Task, - project: Project, - config: RalphConfig, - depReflections: Reflection[], - ctx: ExtensionContext, - sendChatMessage?: SendChatMessage, - projectDir: string = project.sourceDir, - batcher?: WidgetBatcher, + task: Task, + project: Project, + config: RalpiConfig, + depReflections: Reflection[], + ctx: ExtensionContext, + sendChatMessage?: SendChatMessage, + projectDir: string = project.sourceDir, + batcher?: WidgetBatcher, ): Promise<{ - success: boolean; - reflection?: Reflection; - error?: string; - durationMs: number; - toolUsage?: ToolUsage; - outputPreview?: string; - sessionFile?: string; - commitMessages?: string[]; - commitSummary?: string; + success: boolean; + reflection?: Reflection; + error?: string; + durationMs: number; + toolUsage?: ToolUsage; + outputPreview?: string; + sessionFile?: string; + commitMessages?: string[]; + commitSummary?: string; }> { - const startMs = Date.now(); + const startMs = Date.now(); - // Build prompt - const prompt = buildTaskPrompt( - task, - project, - depReflections, - config.prompts.projectContext, - ); + // Build prompt + const prompt = buildTaskPrompt( + task, + project, + depReflections, + config.prompts.projectContext, + ); - // Write prompt to .ralph/ with timestamp (for debugging) - const ralphDir = path.join(projectDir, ".ralph"); - ensureDir(ralphDir); - const promptFile = path.join(ralphDir, `prompt-${startMs}.md`); - writeFileSafe(promptFile, prompt); + // Write prompt to .ralpi/ with timestamp (for debugging) + const ralpiDir = path.join(projectDir, ".ralpi"); + ensureDir(ralpiDir); + const promptFile = path.join(ralpiDir, `prompt-${startMs}.md`); + writeFileSafe(promptFile, prompt); - // Footer shows just the task title (no batch prefix) - ctx.ui.setStatus("ralph", task.title); + // Footer shows just the task title (no batch prefix) + 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 - // Using setWidget instead of setWorkingMessage because the working message area - // 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. - const widgetKey = `ralph-task-${task.id}`; - const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; - let frameIndex = 0; - const theme = ctx.ui.theme; - const MAX_COLLAPSED = 3; + // Live progress widget above the editor — animated spinner + tool call tree + // Using setWidget instead of setWorkingMessage because the working message area + // 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. + const widgetKey = `ralpi-task-${task.id}`; + const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; + let frameIndex = 0; + const theme = ctx.ui.theme; + const MAX_COLLAPSED = 3; - const toolCalls: ToolCallEntry[] = []; + const toolCalls: ToolCallEntry[] = []; - const updateWidget = () => { - const frame = theme.fg("accent", SPINNER_FRAMES[frameIndex]); - const lines = [`${frame} ${taskHeader}`]; + const updateWidget = () => { + const frame = theme.fg("accent", SPINNER_FRAMES[frameIndex]); + const lines = [`${frame} ${taskHeader}`]; - if (toolCalls.length > 0) { - const shown = toolCalls.slice(-MAX_COLLAPSED); - const remaining = toolCalls.length - shown.length; + if (toolCalls.length > 0) { + const shown = toolCalls.slice(-MAX_COLLAPSED); + const remaining = toolCalls.length - shown.length; - if (remaining > 0) { - lines.push(theme.fg("dim", ` ├── ${remaining} more`)); - } + if (remaining > 0) { + lines.push(theme.fg("dim", ` ├── ${remaining} more`)); + } - for (let i = 0; i < shown.length; i++) { - const entry = shown[i]; - const isLast = i === shown.length - 1; - const branch = isLast ? " └── " : " ├── "; - const tag = theme.fg("accent", `[${entry.name}]`); - lines.push(`${branch}${tag} ${entry.label}`); - } - } + for (let i = 0; i < shown.length; i++) { + const entry = shown[i]; + const isLast = i === shown.length - 1; + const branch = isLast ? " └── " : " ├── "; + const tag = theme.fg("accent", `[${entry.name}]`); + lines.push(`${branch}${tag} ${entry.label}`); + } + } - if (batcher) { - batcher.schedule(widgetKey, lines); - } else { - ctx.ui.setWidget(widgetKey, lines); - } - }; + if (batcher) { + batcher.schedule(widgetKey, lines); + } else { + ctx.ui.setWidget(widgetKey, lines); + } + }; - // Smooth spinner animation at 100ms intervals - const spinnerTimer = setInterval(() => { - frameIndex = (frameIndex + 1) % SPINNER_FRAMES.length; - updateWidget(); - }, 100); + // Smooth spinner animation at 100ms intervals + const spinnerTimer = setInterval(() => { + frameIndex = (frameIndex + 1) % SPINNER_FRAMES.length; + updateWidget(); + }, 100); - // Initial display - updateWidget(); + // Initial display + updateWidget(); - // Use task-level timeout if set, otherwise fall back to config - const timeoutMs = task.timeoutMs ?? config.execution.timeoutMs; + // Use task-level timeout if set, otherwise fall back to config + const timeoutMs = task.timeoutMs ?? config.execution.timeoutMs; - // Pre-create session file path so events stream to disk (avoids 300+ MB in-memory accumulation) - const sessionsDir = path.join(ralphDir, "sessions"); - ensureDir(sessionsDir); - const sessionFilePath = path.join(sessionsDir, `${task.id}-${startMs}.txt`); + // Pre-create session file path so events stream to disk (avoids 300+ MB in-memory accumulation) + const sessionsDir = path.join(ralpiDir, "sessions"); + ensureDir(sessionsDir); + const sessionFilePath = path.join(sessionsDir, `${task.id}-${startMs}.txt`); - // Run task asynchronously via Pi SDK — event loop stays responsive - const output = await runAgentSession( - prompt, - projectDir, - timeoutMs, - (event) => { - if (event.type === "tool_execution_start") { - const label = formatToolArg(event.toolName, event.args); - toolCalls.push({ - name: event.toolName, - label, - }); - updateWidget(); - } - }, - undefined, // no abort signal - sessionFilePath, // stream events to file - ); + // Run task asynchronously via Pi SDK — event loop stays responsive + const output = await runAgentSession( + prompt, + projectDir, + timeoutMs, + (event) => { + if (event.type === "tool_execution_start") { + const label = formatToolArg(event.toolName, event.args); + toolCalls.push({ + name: event.toolName, + label, + }); + updateWidget(); + } + }, + undefined, // no abort signal + sessionFilePath, // stream events to file + ); - const durationMs = Date.now() - startMs; + const durationMs = Date.now() - startMs; - // Clear progress widget and status after task finishes - clearInterval(spinnerTimer); - if (batcher) { - batcher.scheduleRemove(widgetKey); - } else { - ctx.ui.setWidget(widgetKey, undefined); - } - ctx.ui.setStatus("ralph", undefined); + // Clear progress widget and status after task finishes + clearInterval(spinnerTimer); + if (batcher) { + batcher.scheduleRemove(widgetKey); + } else { + ctx.ui.setWidget(widgetKey, undefined); + } + ctx.ui.setStatus("ralpi", undefined); - if (!output.success) { - sendChatMessage?.(`✗ ${taskHeader} — ${output.error}`); - ctx.ui.notify(`Task ${task.id} failed: ${output.error}`, "error"); - return { - success: false, - error: output.error, - durationMs, - sessionFile: sessionFilePath, // events streamed to file for debugging - }; - } + if (!output.success) { + sendChatMessage?.(`✗ ${taskHeader} — ${output.error}`); + ctx.ui.notify(`Task ${task.id} failed: ${output.error}`, "error"); + return { + success: false, + error: output.error, + durationMs, + sessionFile: sessionFilePath, // events streamed to file for debugging + }; + } - const agentText = output.text; - const toolUsage = output.toolUsage; + const agentText = output.text; + const toolUsage = output.toolUsage; - // Capture git commits made during this task - const { commitMessages, commitSummary } = captureGitCommits(projectDir); + // Capture git commits made during this task + const { commitMessages, commitSummary } = captureGitCommits(projectDir); - // Session file already written by runAgentSession (events streamed to disk) - const sessionFile = sessionFilePath; + // Session file already written by runAgentSession (events streamed to disk) + const sessionFile = sessionFilePath; - // Build output preview (first 500 chars of agent text) - const outputPreview = - agentText.length > 500 - ? agentText.slice(0, 500) + "\n... (truncated, see session file)" - : agentText; + // Build output preview (first 500 chars of agent text) + const outputPreview = + agentText.length > 500 + ? agentText.slice(0, 500) + "\n... (truncated, see session file)" + : agentText; - // Extract reflection from agent output - const reflection = extractReflection(agentText, task.id, task.title); + // Extract reflection from agent output + const reflection = extractReflection(agentText, task.id, task.title); - // Post completion chat message — header only, renderer builds the expandable tree - const dur = formatDuration(durationMs); - sendChatMessage?.(`✓ ${taskHeader} (${dur})`, { toolCalls }); + // Post completion chat message — header only, renderer builds the expandable tree + const dur = formatDuration(durationMs); + sendChatMessage?.(`✓ ${taskHeader} (${dur})`, { toolCalls }); - return { - success: true, - reflection: reflection ?? undefined, - durationMs, - toolUsage, - outputPreview, - sessionFile, - commitMessages, - commitSummary, - }; + return { + success: true, + reflection: reflection ?? undefined, + durationMs, + toolUsage, + outputPreview, + sessionFile, + commitMessages, + commitSummary, + }; } // ─── Execute Batch ─────────────────────────────────────────────────────────── @@ -211,198 +211,198 @@ export async function runTask( * Execute a batch of tasks (sequentially or in parallel) */ export async function executeBatch( - tasks: Task[], - project: Project, - config: RalphConfig, - progress: ProgressTracker, - ctx: ExtensionContext, - options?: { parallel?: boolean }, - sendChatMessage?: SendChatMessage, - projectDir?: string, + tasks: Task[], + project: Project, + config: RalpiConfig, + progress: ProgressTracker, + ctx: ExtensionContext, + options?: { parallel?: boolean }, + sendChatMessage?: SendChatMessage, + projectDir?: string, ): Promise { - // Defensive: ensure tasks is an iterable array - if (!Array.isArray(tasks)) { - throw new Error( - `executeBatch received invalid tasks: expected array, got ${typeof tasks}`, - ); - } + // Defensive: ensure tasks is an iterable array + if (!Array.isArray(tasks)) { + throw new Error( + `executeBatch received invalid tasks: expected array, got ${typeof tasks}`, + ); + } - // Check if we should run parallel - const shouldParallel = - options?.parallel && tasks.length > 1 && config.execution.maxParallel > 0; + // Check if we should run parallel + const shouldParallel = + options?.parallel && tasks.length > 1 && config.execution.maxParallel > 0; - if (shouldParallel) { - await executeBatchParallel( - tasks, - project, - config, - progress, - ctx, - sendChatMessage, - projectDir, - ); - return; - } + if (shouldParallel) { + await executeBatchParallel( + tasks, + project, + config, + progress, + ctx, + sendChatMessage, + projectDir, + ); + return; + } - // Execute sequentially - for (const task of tasks) { - await executeTask( - task, - project, - config, - progress, - ctx, - sendChatMessage, - projectDir, - ); - } + // Execute sequentially + for (const task of tasks) { + await executeTask( + task, + project, + config, + progress, + ctx, + sendChatMessage, + projectDir, + ); + } } /** * Execute tasks in parallel using child processes */ async function executeBatchParallel( - tasks: Task[], - project: Project, - config: RalphConfig, - progress: ProgressTracker, - ctx: ExtensionContext, - sendChatMessage?: SendChatMessage, - projectDir?: string, + tasks: Task[], + project: Project, + config: RalpiConfig, + progress: ProgressTracker, + ctx: ExtensionContext, + sendChatMessage?: SendChatMessage, + projectDir?: string, ): Promise { - const maxParallel = config.execution.maxParallel; - const batcher = new WidgetBatcher(ctx); - const results: Array<{ task: Task; result: Promise }> = []; + const maxParallel = config.execution.maxParallel; + const batcher = new WidgetBatcher(ctx); + const results: Array<{ task: Task; result: Promise }> = []; - for (const task of tasks) { - results.push({ - task, - result: executeTask( - task, - project, - config, - progress, - ctx, - sendChatMessage, - projectDir, - batcher, - ), - }); + for (const task of tasks) { + results.push({ + task, + result: executeTask( + task, + project, + config, + progress, + ctx, + sendChatMessage, + projectDir, + batcher, + ), + }); - // Limit concurrency - if (results.length >= maxParallel) { - const first = results.shift(); - if (first) await first.result; - } - } + // Limit concurrency + if (results.length >= maxParallel) { + const first = results.shift(); + if (first) await first.result; + } + } - // Wait for remaining tasks - for (const { result } of results) { - await result; - } + // Wait for remaining tasks + for (const { result } of results) { + await result; + } - // Flush and stop the batcher after all tasks complete - batcher.stop(); + // Flush and stop the batcher after all tasks complete + batcher.stop(); } // ─── Execute Single Task with Retry ────────────────────────────────────────── async function executeTask( - task: Task, - project: Project, - config: RalphConfig, - progress: ProgressTracker, - ctx: ExtensionContext, - sendChatMessage?: SendChatMessage, - projectDir: string = project.sourceDir, - batcher?: WidgetBatcher, + task: Task, + project: Project, + config: RalpiConfig, + progress: ProgressTracker, + ctx: ExtensionContext, + sendChatMessage?: SendChatMessage, + projectDir: string = project.sourceDir, + batcher?: WidgetBatcher, ): Promise { - const maxRetries = config.execution.maxRetries; - let retries = 0; + const maxRetries = config.execution.maxRetries; + let retries = 0; - while (retries <= maxRetries) { - try { - // Mark as in progress - progress.markInProgress(task.id); + while (retries <= maxRetries) { + try { + // Mark as in progress + progress.markInProgress(task.id); - // Get dependency reflections - const depReflections = progress.getDependencyReflections( - task.dependencies || [], - ); + // Get dependency reflections + const depReflections = progress.getDependencyReflections( + task.dependencies || [], + ); - // Run the task - const result = await runTask( - task, - project, - config, - depReflections, - ctx, - sendChatMessage, - projectDir, - batcher, - ); + // Run the task + const result = await runTask( + task, + project, + config, + depReflections, + ctx, + sendChatMessage, + projectDir, + batcher, + ); - if (result.success) { - // Save reflection - if (result.reflection) { - saveReflectionToFile(projectDir, config, result.reflection); - } + if (result.success) { + // Save reflection + if (result.reflection) { + saveReflectionToFile(projectDir, config, result.reflection); + } - // Mark completed with all metadata - progress.markCompleted( - task.id, - result.durationMs, - result.reflection, - result.toolUsage, - result.sessionFile, - result.outputPreview, - result.commitMessages, - result.commitSummary, - ); - return; - } + // Mark completed with all metadata + progress.markCompleted( + task.id, + result.durationMs, + result.reflection, + result.toolUsage, + result.sessionFile, + result.outputPreview, + result.commitMessages, + result.commitSummary, + ); + return; + } - // Task failed, check if we should retry - if (retries < maxRetries) { - retries = progress.incrementRetry(task.id); - ctx.ui.notify( - `Retrying task ${task.id} (${retries}/${maxRetries}): ${result.error}`, - "warning", - ); + // Task failed, check if we should retry + if (retries < maxRetries) { + retries = progress.incrementRetry(task.id); + ctx.ui.notify( + `Retrying task ${task.id} (${retries}/${maxRetries}): ${result.error}`, + "warning", + ); - // Exponential backoff - const delay = config.execution.retryDelayMs * 2 ** (retries - 1); - await sleep(delay); - } else { - // Max retries exceeded - progress.markFailed(task.id, result.error || "Unknown error"); - throw new Error(`Task ${task.id} failed: ${result.error}`); - } - } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error); - progress.markFailed(task.id, errorMsg); - throw error; - } - } + // Exponential backoff + const delay = config.execution.retryDelayMs * 2 ** (retries - 1); + await sleep(delay); + } else { + // Max retries exceeded + progress.markFailed(task.id, result.error || "Unknown error"); + throw new Error(`Task ${task.id} failed: ${result.error}`); + } + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + progress.markFailed(task.id, errorMsg); + throw error; + } + } } // ─── Save Reflection to File ──────────────────────────────────────────────── function saveReflectionToFile( - sourceDir: string, - config: RalphConfig, - reflection: Reflection, + sourceDir: string, + config: RalpiConfig, + reflection: Reflection, ): void { - const reflectionsDir = path.join(sourceDir, config.paths.reflectionsDir); - ensureDir(reflectionsDir); - const filePath = path.join(reflectionsDir, `${reflection.taskId}.json`); - writeFileSafe(filePath, JSON.stringify(reflection, null, 2)); + const reflectionsDir = path.join(sourceDir, config.paths.reflectionsDir); + ensureDir(reflectionsDir); + const filePath = path.join(reflectionsDir, `${reflection.taskId}.json`); + writeFileSafe(filePath, JSON.stringify(reflection, null, 2)); } // ─── Helpers ───────────────────────────────────────────────────────────────── function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)); } // ─── Tool Call Formatting ──────────────────────────────────────────────── @@ -411,31 +411,34 @@ function sleep(ms: number): Promise { * Format a tool call argument into a short label. */ function formatToolArg(name: string, args: unknown): string { - const a = args as Record; - switch (name) { - case "bash": - return truncateMiddle(String(a.command ?? ""), 70); - case "write": - case "read": - return truncateMiddle(String(a.path ?? ""), 60); - case "edit": - return truncateMiddle(String(a.path ?? ""), 60); - case "grep": - return `${a.pattern ?? "?"} — ${truncateMiddle(String(a.path ?? ""), 40)}`; - case "find": - return `${a.path ?? "."} — ${a.glob ?? "*"}`; - case "ls": - return truncateMiddle(String(a.path ?? "."), 60); - default: - return name; - } + const a = args as Record; + switch (name) { + case "bash": + return truncateMiddle(String(a.command ?? ""), 70); + case "write": + case "read": + return truncateMiddle(String(a.path ?? ""), 60); + case "edit": + return truncateMiddle(String(a.path ?? ""), 60); + case "grep": + return `${a.pattern ?? "?"} — ${truncateMiddle( + String(a.path ?? ""), + 40, + )}`; + case "find": + return `${a.path ?? "."} — ${a.glob ?? "*"}`; + case "ls": + return truncateMiddle(String(a.path ?? "."), 60); + default: + return name; + } } /** * Truncate a long string in the middle, keeping start and end visible. */ function truncateMiddle(s: string, maxLen: number): string { - if (s.length <= maxLen) return s; - const half = Math.floor((maxLen - 3) / 2); - return s.slice(0, half) + "…" + s.slice(s.length - half); + if (s.length <= maxLen) return s; + const half = Math.floor((maxLen - 3) / 2); + return s.slice(0, half) + "…" + s.slice(s.length - half); } diff --git a/src/progress.ts b/src/progress.ts index f41de84..2be01dc 100644 --- a/src/progress.ts +++ b/src/progress.ts @@ -1,6 +1,12 @@ import * as fs from "node:fs"; 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"; /** @@ -8,258 +14,264 @@ import { ensureDir } from "./utils"; * e.g., "tasks/feature-x/README.md" → "tasks-feature-x-README" */ export function derivePRDKey(projectDir: string, sourcePath: string): string { - const rel = path.relative(projectDir, sourcePath); - return rel.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, ""); + const rel = path.relative(projectDir, sourcePath); + return rel + .replace(/[^a-zA-Z0-9_-]/g, "-") + .replace(/-+/g, "-") + .replace(/^-|-$/g, ""); } /** * 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. * Falls back to legacy flat format for backward compatibility. */ export class ProgressTracker { - private statePath: string; - private state: ProgressState; - private prdKey: string; + private statePath: string; + private state: ProgressState; + private prdKey: string; - constructor(projectDir: string, sourcePath: string, prdKey?: string) { - const stateDir = path.join(projectDir, ".ralph"); - ensureDir(stateDir); - this.statePath = path.join(stateDir, "progress.json"); - this.prdKey = prdKey ?? derivePRDKey(projectDir, sourcePath); - this.state = this.loadOrCreate(sourcePath); - } + constructor(projectDir: string, sourcePath: string, prdKey?: string) { + const stateDir = path.join(projectDir, ".ralpi"); + ensureDir(stateDir); + this.statePath = path.join(stateDir, "progress.json"); + this.prdKey = prdKey ?? derivePRDKey(projectDir, sourcePath); + this.state = this.loadOrCreate(sourcePath); + } - /** Load existing state or create a fresh one */ - private loadOrCreate(sourcePathHint: string): ProgressState { - if (fs.existsSync(this.statePath)) { - try { - const raw = fs.readFileSync(this.statePath, "utf-8"); - const parsed = JSON.parse(raw) as ProgressState; + /** Load existing state or create a fresh one */ + private loadOrCreate(sourcePathHint: string): ProgressState { + if (fs.existsSync(this.statePath)) { + try { + const raw = fs.readFileSync(this.statePath, "utf-8"); + const parsed = JSON.parse(raw) as ProgressState; - // Multi-PRD mode: check if we have a PRD entry - if (parsed.prds?.[this.prdKey]) { - // Found PRD entry — use it, but keep legacy fields for compat - return parsed; - } + // Multi-PRD mode: check if we have a PRD entry + if (parsed.prds?.[this.prdKey]) { + // Found PRD entry — use it, but keep legacy fields for compat + return parsed; + } - // Legacy flat mode: check if the source path matches - if (path.resolve(parsed.sourcePath) === path.resolve(sourcePathHint)) { - // Migrate legacy state to PRD mode - parsed.prds = { - [this.prdKey]: { - sourcePath: parsed.sourcePath, - tasks: parsed.tasks, - startedAt: parsed.startedAt, - lastUpdatedAt: parsed.lastUpdatedAt, - paused: parsed.paused, - }, - }; - return parsed; - } + // Legacy flat mode: check if the source path matches + if (path.resolve(parsed.sourcePath) === path.resolve(sourcePathHint)) { + // Migrate legacy state to PRD mode + parsed.prds = { + [this.prdKey]: { + sourcePath: parsed.sourcePath, + tasks: parsed.tasks, + startedAt: parsed.startedAt, + lastUpdatedAt: parsed.lastUpdatedAt, + paused: parsed.paused, + }, + }; + return parsed; + } - // Different PRD — create new entry alongside existing ones - if (parsed.prds) { - parsed.prds[this.prdKey] = this.freshPRD(sourcePathHint); - return parsed; - } + // Different PRD — create new entry alongside existing ones + if (parsed.prds) { + parsed.prds[this.prdKey] = this.freshPRD(sourcePathHint); + return parsed; + } - // Legacy flat state exists but for a different source — promote it to PRD mode - const legacyKey = derivePRDKey(path.dirname(this.statePath), parsed.sourcePath); - parsed.prds = { - [legacyKey]: { - sourcePath: parsed.sourcePath, - tasks: parsed.tasks, - startedAt: parsed.startedAt, - lastUpdatedAt: parsed.lastUpdatedAt, - paused: parsed.paused, - }, - [this.prdKey]: this.freshPRD(sourcePathHint), - }; - return parsed; - } catch { - // Fall through to create new - } - } + // Legacy flat state exists but for a different source — promote it to PRD mode + const legacyKey = derivePRDKey( + path.dirname(this.statePath), + parsed.sourcePath, + ); + parsed.prds = { + [legacyKey]: { + sourcePath: parsed.sourcePath, + tasks: parsed.tasks, + startedAt: parsed.startedAt, + lastUpdatedAt: parsed.lastUpdatedAt, + paused: parsed.paused, + }, + [this.prdKey]: this.freshPRD(sourcePathHint), + }; + return parsed; + } catch { + // Fall through to create new + } + } - return this.freshState(sourcePathHint); - } + return this.freshState(sourcePathHint); + } - private freshPRD(sourcePath: string): PRDProgress { - return { - sourcePath, - tasks: {}, - startedAt: new Date().toISOString(), - lastUpdatedAt: new Date().toISOString(), - paused: false, - }; - } + private freshPRD(sourcePath: string): PRDProgress { + return { + sourcePath, + tasks: {}, + startedAt: new Date().toISOString(), + lastUpdatedAt: new Date().toISOString(), + paused: false, + }; + } - private freshState(sourcePath: string): ProgressState { - return { - sourcePath, - tasks: {}, - startedAt: new Date().toISOString(), - lastUpdatedAt: new Date().toISOString(), - paused: false, - prds: { - [this.prdKey]: { - sourcePath, - tasks: {}, - startedAt: new Date().toISOString(), - lastUpdatedAt: new Date().toISOString(), - paused: false, - }, - }, - }; - } + private freshState(sourcePath: string): ProgressState { + return { + sourcePath, + tasks: {}, + startedAt: new Date().toISOString(), + lastUpdatedAt: new Date().toISOString(), + paused: false, + prds: { + [this.prdKey]: { + sourcePath, + tasks: {}, + startedAt: new Date().toISOString(), + lastUpdatedAt: new Date().toISOString(), + paused: false, + }, + }, + }; + } - /** Get the PRD-scoped progress entry */ - private getPRD(): PRDProgress { - if (!this.state.prds) { - // Should not happen after loadOrCreate, but guard anyway - this.state.prds = { [this.prdKey]: this.freshPRD(this.state.sourcePath) }; - } - if (!this.state.prds[this.prdKey]) { - this.state.prds[this.prdKey] = this.freshPRD(this.state.sourcePath); - } - return this.state.prds[this.prdKey]; - } + /** Get the PRD-scoped progress entry */ + private getPRD(): PRDProgress { + if (!this.state.prds) { + // Should not happen after loadOrCreate, but guard anyway + this.state.prds = { [this.prdKey]: this.freshPRD(this.state.sourcePath) }; + } + if (!this.state.prds[this.prdKey]) { + this.state.prds[this.prdKey] = this.freshPRD(this.state.sourcePath); + } + return this.state.prds[this.prdKey]; + } - /** Save current state to disk */ - save(): void { - const prd = this.getPRD(); - prd.lastUpdatedAt = new Date().toISOString(); - // Sync legacy flat fields with current PRD for backward compat - this.state.sourcePath = prd.sourcePath; - this.state.tasks = prd.tasks; - this.state.startedAt = prd.startedAt; - this.state.lastUpdatedAt = prd.lastUpdatedAt; - this.state.paused = prd.paused; - fs.writeFileSync( - this.statePath, - JSON.stringify(this.state, null, 2), - "utf-8", - ); - } + /** Save current state to disk */ + save(): void { + const prd = this.getPRD(); + prd.lastUpdatedAt = new Date().toISOString(); + // Sync legacy flat fields with current PRD for backward compat + this.state.sourcePath = prd.sourcePath; + this.state.tasks = prd.tasks; + this.state.startedAt = prd.startedAt; + this.state.lastUpdatedAt = prd.lastUpdatedAt; + this.state.paused = prd.paused; + fs.writeFileSync( + this.statePath, + JSON.stringify(this.state, null, 2), + "utf-8", + ); + } - /** Mark a task as in progress */ - markInProgress(taskId: string): void { - const prd = this.getPRD(); - this.ensureTask(prd, taskId); - prd.tasks[taskId].status = "in_progress"; - prd.tasks[taskId].startedAt = new Date().toISOString(); - this.save(); - } + /** Mark a task as in progress */ + markInProgress(taskId: string): void { + const prd = this.getPRD(); + this.ensureTask(prd, taskId); + prd.tasks[taskId].status = "in_progress"; + prd.tasks[taskId].startedAt = new Date().toISOString(); + this.save(); + } - /** Mark a task as completed */ - markCompleted( - taskId: string, - durationMs: number, - reflection?: Reflection, - toolUsage?: ToolUsage, - sessionFile?: string, - outputPreview?: string, - commitMessages?: string[], - commitSummary?: string, - ): void { - const prd = this.getPRD(); - this.ensureTask(prd, taskId); - prd.tasks[taskId].status = "completed"; - prd.tasks[taskId].completedAt = new Date().toISOString(); - prd.tasks[taskId].durationMs = durationMs; - if (reflection) prd.tasks[taskId].reflection = reflection; - if (toolUsage) prd.tasks[taskId].toolUsage = toolUsage; - if (sessionFile) prd.tasks[taskId].sessionFile = sessionFile; - if (outputPreview) prd.tasks[taskId].outputPreview = outputPreview; - if (commitMessages) prd.tasks[taskId].commitMessages = commitMessages; - if (commitSummary) prd.tasks[taskId].commitSummary = commitSummary; - this.save(); - } + /** Mark a task as completed */ + markCompleted( + taskId: string, + durationMs: number, + reflection?: Reflection, + toolUsage?: ToolUsage, + sessionFile?: string, + outputPreview?: string, + commitMessages?: string[], + commitSummary?: string, + ): void { + const prd = this.getPRD(); + this.ensureTask(prd, taskId); + prd.tasks[taskId].status = "completed"; + prd.tasks[taskId].completedAt = new Date().toISOString(); + prd.tasks[taskId].durationMs = durationMs; + if (reflection) prd.tasks[taskId].reflection = reflection; + if (toolUsage) prd.tasks[taskId].toolUsage = toolUsage; + if (sessionFile) prd.tasks[taskId].sessionFile = sessionFile; + if (outputPreview) prd.tasks[taskId].outputPreview = outputPreview; + if (commitMessages) prd.tasks[taskId].commitMessages = commitMessages; + if (commitSummary) prd.tasks[taskId].commitSummary = commitSummary; + this.save(); + } - /** Mark a task as failed */ - markFailed(taskId: string, error: string): void { - const prd = this.getPRD(); - this.ensureTask(prd, taskId); - prd.tasks[taskId].status = "failed"; - prd.tasks[taskId].error = error; - this.save(); - } + /** Mark a task as failed */ + markFailed(taskId: string, error: string): void { + const prd = this.getPRD(); + this.ensureTask(prd, taskId); + prd.tasks[taskId].status = "failed"; + prd.tasks[taskId].error = error; + this.save(); + } - /** Get task status */ - getTaskStatus(taskId: string): Task["status"] { - const prd = this.getPRD(); - return prd.tasks[taskId]?.status ?? "pending"; - } + /** Get task status */ + getTaskStatus(taskId: string): Task["status"] { + const prd = this.getPRD(); + return prd.tasks[taskId]?.status ?? "pending"; + } - /** Get IDs of all completed tasks */ - getCompletedTaskIds(): string[] { - const prd = this.getPRD(); - return Object.entries(prd.tasks) - .filter(([, info]) => info.status === "completed") - .map(([id]) => id); - } + /** Get IDs of all completed tasks */ + getCompletedTaskIds(): string[] { + const prd = this.getPRD(); + return Object.entries(prd.tasks) + .filter(([, info]) => info.status === "completed") + .map(([id]) => id); + } - /** Get all reflections from completed tasks */ - getAllReflections(): Reflection[] { - const prd = this.getPRD(); - const reflections: Reflection[] = []; - for (const info of Object.values(prd.tasks)) { - if (info.reflection) reflections.push(info.reflection); - } - return reflections; - } + /** Get all reflections from completed tasks */ + getAllReflections(): Reflection[] { + const prd = this.getPRD(); + const reflections: Reflection[] = []; + for (const info of Object.values(prd.tasks)) { + if (info.reflection) reflections.push(info.reflection); + } + return reflections; + } - /** Get reflections for specific dependency tasks */ - getDependencyReflections(depIds: string[]): Reflection[] { - const prd = this.getPRD(); - return depIds - .map((id) => prd.tasks[id]?.reflection) - .filter((r): r is Reflection => r !== undefined); - } + /** Get reflections for specific dependency tasks */ + getDependencyReflections(depIds: string[]): Reflection[] { + const prd = this.getPRD(); + return depIds + .map((id) => prd.tasks[id]?.reflection) + .filter((r): r is Reflection => r !== undefined); + } - /** Increment retry count */ - incrementRetry(taskId: string): number { - const prd = this.getPRD(); - this.ensureTask(prd, taskId); - prd.tasks[taskId].retries++; - this.save(); - return prd.tasks[taskId].retries; - } + /** Increment retry count */ + incrementRetry(taskId: string): number { + const prd = this.getPRD(); + this.ensureTask(prd, taskId); + prd.tasks[taskId].retries++; + this.save(); + return prd.tasks[taskId].retries; + } - /** Set paused state */ - setPaused(paused: boolean): void { - const prd = this.getPRD(); - prd.paused = paused; - this.save(); - } + /** Set paused state */ + setPaused(paused: boolean): void { + const prd = this.getPRD(); + prd.paused = paused; + this.save(); + } - /** Get the raw PRD state (for status display) */ - getState(): PRDProgress { - return this.getPRD(); - } + /** Get the raw PRD state (for status display) */ + getState(): PRDProgress { + return this.getPRD(); + } - /** Get all PRDs (for multi-PRD status display) */ - getAllPRDs(): Record { - return this.state.prds ?? {}; - } + /** Get all PRDs (for multi-PRD status display) */ + getAllPRDs(): Record { + return this.state.prds ?? {}; + } - /** Get the PRD key for this tracker */ - getKey(): string { - return this.prdKey; - } + /** Get the PRD key for this tracker */ + getKey(): string { + return this.prdKey; + } - /** Reset all progress for this PRD */ - reset(): void { - const prd = this.getPRD(); - Object.assign(prd, this.freshPRD(prd.sourcePath)); - this.save(); - } + /** Reset all progress for this PRD */ + reset(): void { + const prd = this.getPRD(); + Object.assign(prd, this.freshPRD(prd.sourcePath)); + this.save(); + } - private ensureTask(prd: PRDProgress, taskId: string): void { - if (!prd.tasks[taskId]) { - prd.tasks[taskId] = { status: "pending", retries: 0 }; - } - } + private ensureTask(prd: PRDProgress, taskId: string): void { + if (!prd.tasks[taskId]) { + prd.tasks[taskId] = { status: "pending", retries: 0 }; + } + } } diff --git a/src/types.ts b/src/types.ts index 30272bf..003f35e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -137,9 +137,9 @@ export interface PRDProgress { // ─── Configuration ──────────────────────────────────────────────────────────── -export interface RalphConfig { +export interface RalpiConfig { paths: { - /** Directory for ralph state files */ + /** Directory for ralpi state files */ stateDir: string; /** Directory for per-task reflections */ reflectionsDir: string; @@ -162,10 +162,10 @@ export interface RalphConfig { }; } -export const DEFAULT_CONFIG: RalphConfig = { +export const DEFAULT_CONFIG: RalpiConfig = { paths: { - stateDir: ".ralph", - reflectionsDir: ".ralph/reflections", + stateDir: ".ralpi", + reflectionsDir: ".ralpi/reflections", }, execution: { maxRetries: 3, diff --git a/src/utils.ts b/src/utils.ts index 5d74c43..da45d0d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,7 +1,7 @@ import * as fs from "node:fs"; import * as path from "node:path"; import type { - RalphConfig, + RalpiConfig, PRDProgress, ProgressState, ToolUsage, @@ -39,7 +39,7 @@ export function writeFileSafe(filePath: string, content: string): void { // ─── 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. */ export function findProgressFile( @@ -50,7 +50,7 @@ export function findProgressFile( const root = path.parse(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)) { try { const raw = fs.readFileSync(candidate, "utf-8"); @@ -113,9 +113,9 @@ function parseSimpleYaml(content: string): Record { * Deep merge configuration objects */ function mergeConfig( - defaults: RalphConfig, + defaults: RalpiConfig, overrides: Record, -): RalphConfig { +): RalpiConfig { const result = { ...defaults }; 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 { - const configPath = path.join(projectDir, ".ralph", "config.yaml"); +export function loadConfig(projectDir: string): RalpiConfig { + const configPath = path.join(projectDir, ".ralpi", "config.yaml"); // Return defaults silently when config file does not exist if (!fs.existsSync(configPath)) {