diff --git a/index.ts b/index.ts index 45848d3..d67b720 100644 --- a/index.ts +++ b/index.ts @@ -1,25 +1,25 @@ 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, + buildExecutionPlan, + buildSequentialPlan, + formatExecutionPlan, } 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, - findProgressFile, + loadConfig, + resolveTaskArg, + formatProgressStatus, + findProgressFile, } from "./src/utils"; const COMMANDS = ["plan", "resume", "reset"] as const; @@ -33,432 +33,422 @@ 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, - config: import("./src/types").RalpiConfig, + ctx: ExtensionContext, + project: import("./src/types").Project, + taskFile: string, + config: import("./src/types").RalpiConfig, ): Promise { - const mode = await ctx.ui.select("Execution mode for this run?", [ - `Parallel (where dependencies allow)[${config.execution.maxParallel} max]`, - "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)[${config.execution.maxParallel} max]`, + "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").RalpiConfig, - 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 { - // Track failed task IDs across batches to block downstream tasks - const failedTaskIds = new Set(progress.getFailedTaskIds()); + // Track failed task IDs across batches to block downstream tasks + const failedTaskIds = new Set(progress.getFailedTaskIds()); - for (const batch of plan.batches) { - if (progress.getState().paused) { - ctx.ui.notify( - "Execution paused. Use /ralpi 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); + } - // Update failed task IDs after batch completes - const newFailed = progress.getFailedTaskIds(); - for (const id of newFailed) { - failedTaskIds.add(id); - } + // Update failed task IDs after batch completes + const newFailed = progress.getFailedTaskIds(); + for (const id of newFailed) { + failedTaskIds.add(id); + } - // In sequential mode, stop after any failure - if (mode === "sequential" && failedTaskIds.size > 0) { - break; - } + // In sequential mode, stop after any failure + if (mode === "sequential" && failedTaskIds.size > 0) { + break; + } - // In parallel mode, rebuild the plan to filter out newly blocked tasks - if (mode === "parallel") { - const completed = new Set(progress.getCompletedTaskIds()); - const newPlan = buildExecutionPlan( - project, - completed, - undefined, - failedTaskIds, - ); + // In parallel mode, rebuild the plan to filter out newly blocked tasks + if (mode === "parallel") { + const completed = new Set(progress.getCompletedTaskIds()); + const newPlan = buildExecutionPlan( + project, + completed, + undefined, + failedTaskIds, + ); - // Replace remaining batches with filtered ones - const currentIdx = plan.batches.indexOf(batch); - const remainingBatches = newPlan.batches.filter( - (b) => b.batchIndex > currentIdx, - ); + // Replace remaining batches with filtered ones + const currentIdx = plan.batches.indexOf(batch); + const remainingBatches = newPlan.batches.filter( + (b) => b.batchIndex > currentIdx, + ); - // Update the plan's batches in-place - plan.batches.length = 0; - plan.batches.push(...remainingBatches); + // Update the plan's batches in-place + plan.batches.length = 0; + plan.batches.push(...remainingBatches); - // Skip empty batches - if (remainingBatches.length === 0) { - break; - } - } - } + // Skip empty batches + if (remainingBatches.length === 0) { + break; + } + } + } } // ─── Extension Entry ──────────────────────────────────────────────────────── 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; + // 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; + }, + ); - // Register the extension's prompts/ directory so Pi discovers @task-manager - pi.on("resources_discover", async (_event, _ctx) => { - const promptsDir = fs.existsSync(path.resolve(__dirname, "prompts")) - ? path.resolve(__dirname, "prompts") - : path.resolve(__dirname, "..", "prompts"); - return { - promptPaths: [promptsDir], - }; - }); + 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); - 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 "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 }, + }); + }; - // 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, + ctx.model, + pi.getThinkingLevel(), + ); + } - // 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, - ctx.model, - pi.getThinkingLevel(), - ); - } - - const command = parts[0]; - switch (command) { - case "run": - return handleRun( - ctx, - parts.slice(1), - sendProgress, - ctx.model, - pi.getThinkingLevel(), - ); - case "plan": - pi.sendUserMessage("@task-manager"); - ctx.ui.notify("Opening Task Manager...", "info"); - return; - case "resume": - return handleResume( - ctx, - parts.slice(1), - sendProgress, - ctx.model, - pi.getThinkingLevel(), - ); - 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", - ); - } - } - } - }, - }); + const command = parts[0]; + switch (command) { + case "run": + return handleRun( + ctx, + parts.slice(1), + sendProgress, + ctx.model, + pi.getThinkingLevel(), + ); + case "plan": + pi.sendUserMessage("@task-manager"); + ctx.ui.notify("Opening Task Manager...", "info"); + return; + case "resume": + return handleResume( + ctx, + parts.slice(1), + sendProgress, + ctx.model, + pi.getThinkingLevel(), + ); + 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", + ); + } + } + } + }, + }); } // ─── /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"); } // ─── /ralpi run ────────────────────────────────────────────────────────────── async function handleRun( - ctx: ExtensionContext, - args: string[], - sendChatMessage?: SendChatMessage, - parentModel?: unknown, - parentThinkingLevel?: unknown, + ctx: ExtensionContext, + args: string[], + sendChatMessage?: SendChatMessage, + parentModel?: unknown, + parentThinkingLevel?: unknown, ): 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, - parentModel, - parentThinkingLevel, - ); - } + // 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, + parentModel, + parentThinkingLevel, + ); + } - // 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"], - ); + // 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, - parentModel, - parentThinkingLevel, - ); - } - } + if (shouldResume?.startsWith("Yes")) { + return handleResume( + ctx, + [], + sendChatMessage, + parentModel, + parentThinkingLevel, + ); + } + } - 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); - config.model = parentModel ?? ctx.model; - config.thinkingLevel = parentThinkingLevel; - const progress = new ProgressTracker(projectDir, taskFile); + const project = parseTaskFile(taskFile); + const config = loadConfig(projectDir); + config.model = parentModel ?? ctx.model; + config.thinkingLevel = parentThinkingLevel; + const progress = new ProgressTracker(projectDir, taskFile); - const completed = buildCompletedSet(progress, project); - const mode = await selectExecutionMode(ctx, project, taskFile, config); - const plan = buildPlanByMode(mode, project, completed); + const completed = buildCompletedSet(progress, project); + const mode = await selectExecutionMode(ctx, project, taskFile, config); + 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"); } // ─── /ralpi status ─────────────────────────────────────────────────────────── @@ -467,73 +457,73 @@ async function handleRun( // ─── /ralpi resume ─────────────────────────────────────────────────────────── async function handleResume( - ctx: ExtensionContext, - args: string[], - sendChatMessage?: SendChatMessage, - parentModel?: unknown, - parentThinkingLevel?: unknown, + ctx: ExtensionContext, + args: string[], + sendChatMessage?: SendChatMessage, + parentModel?: unknown, + parentThinkingLevel?: unknown, ): 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 /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; - } + 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); - config.model = parentModel ?? ctx.model; - config.thinkingLevel = parentThinkingLevel; - 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); + config.model = parentModel ?? ctx.model; + config.thinkingLevel = parentThinkingLevel; + const progress = new ProgressTracker(projectDir, taskFile, found.prdKey); - progress.setPaused(false); + progress.setPaused(false); - const completed = buildCompletedSet(progress, project); - const mode = await selectExecutionMode(ctx, project, taskFile, config); - const plan = buildPlanByMode(mode, project, completed); + const completed = buildCompletedSet(progress, project); + const mode = await selectExecutionMode(ctx, project, taskFile, config); + 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"); } // ─── /ralpi next ───────────────────────────────────────────────────────────── @@ -542,35 +532,35 @@ async function handleResume( // ─── /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 .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(); - } + 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"); }