dependency parsing broken
This commit is contained in:
249
index.ts
249
index.ts
@@ -1,3 +1,4 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import type {
|
||||
ExtensionAPI,
|
||||
@@ -14,7 +15,7 @@ import {
|
||||
import { ProgressTracker } from "./src/progress";
|
||||
import { buildPlanPrompt } from "./src/prompts";
|
||||
import { formatReflections } from "./src/reflection";
|
||||
import { executeBatch } from "./src/executor";
|
||||
import { executeBatch, type SendChatMessage } from "./src/executor";
|
||||
import {
|
||||
loadConfig,
|
||||
resolveTaskArg,
|
||||
@@ -25,6 +26,10 @@ import {
|
||||
|
||||
const COMMANDS = ["status", "resume", "next", "reset"] as const;
|
||||
|
||||
type ExecutionMode = "parallel" | "sequential";
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Detect if a token looks like a file path rather than a subcommand.
|
||||
* Matches: @path, /path, ./path, ../path, path/to/file, path.md, path.yaml
|
||||
@@ -42,6 +47,116 @@ function looksLikePath(token: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** Build the set of completed tasks from progress tracker and PRD checkboxes. */
|
||||
function buildCompletedSet(
|
||||
progress: ProgressTracker,
|
||||
project: import("./src/types").Project,
|
||||
): Set<string> {
|
||||
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,
|
||||
): Promise<ExecutionMode> {
|
||||
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";
|
||||
|
||||
// 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "parallel";
|
||||
}
|
||||
|
||||
/** Build an execution plan based on the selected mode. */
|
||||
function buildPlanByMode(
|
||||
mode: ExecutionMode,
|
||||
project: Parameters<typeof buildExecutionPlan>[0],
|
||||
completed: Set<string>,
|
||||
) {
|
||||
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<typeof buildPlanByMode>,
|
||||
project: Parameters<typeof buildExecutionPlan>[0],
|
||||
taskFile: string,
|
||||
config: import("./src/types").RalphConfig,
|
||||
progress: ProgressTracker,
|
||||
ctx: ExtensionContext,
|
||||
mode: ExecutionMode,
|
||||
sendChatMessage?: SendChatMessage,
|
||||
projectDir?: string,
|
||||
): Promise<void> {
|
||||
for (const batch of plan.batches) {
|
||||
if (progress.getState().paused) {
|
||||
ctx.ui.notify(
|
||||
"Execution paused. Use /ralph 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}`,
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Extension Entry ────────────────────────────────────────────────────────
|
||||
|
||||
export default function ralphLoopExtension(pi: ExtensionAPI): void {
|
||||
@@ -113,7 +228,7 @@ export default function ralphLoopExtension(pi: ExtensionAPI): void {
|
||||
// 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 = (
|
||||
const sendProgress: SendChatMessage = (
|
||||
content: string,
|
||||
meta?: { toolCalls?: Array<{ name: string; label: string }> },
|
||||
) => {
|
||||
@@ -180,6 +295,11 @@ async function handlePlan(
|
||||
): Promise<void> {
|
||||
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());
|
||||
@@ -193,7 +313,7 @@ async function handlePlan(
|
||||
async function handleRun(
|
||||
ctx: ExtensionContext,
|
||||
args: string[],
|
||||
sendChatMessage?: (content: string) => void,
|
||||
sendChatMessage?: SendChatMessage,
|
||||
): Promise<void> {
|
||||
const taskFile = resolveTaskArg(args[0] || "README.md", process.cwd());
|
||||
|
||||
@@ -201,7 +321,7 @@ async function handleRun(
|
||||
// auto-resume instead of starting fresh
|
||||
const existingProgress = findProgressFile(process.cwd(), taskFile);
|
||||
if (existingProgress) {
|
||||
return handleResume(ctx, [args[0]!], sendChatMessage);
|
||||
return handleResume(ctx, args.slice(0, 1), sendChatMessage);
|
||||
}
|
||||
|
||||
// No existing progress for this task — check for any progress at all
|
||||
@@ -218,13 +338,11 @@ async function handleRun(
|
||||
}
|
||||
}
|
||||
|
||||
const project = parseTaskFile(taskFile);
|
||||
|
||||
// Determine projectDir: prefer existing .ralph/ location, otherwise use 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);
|
||||
|
||||
@@ -234,47 +352,22 @@ async function handleRun(
|
||||
`Starting ${project.tasks.length} tasks from ${path.basename(taskFile)}`,
|
||||
);
|
||||
|
||||
const completed = new Set(progress.getCompletedTaskIds());
|
||||
const completed = buildCompletedSet(progress, project);
|
||||
const mode = await selectExecutionMode(ctx, project, taskFile);
|
||||
const plan = buildPlanByMode(mode, project, completed);
|
||||
|
||||
// Ask user for execution mode
|
||||
const mode = await ctx.ui.select("Execution mode for this run?", [
|
||||
"Parallel (DAG-optimized)",
|
||||
"Sequential (one at a time)",
|
||||
]);
|
||||
const useParallel = mode?.startsWith("Parallel");
|
||||
|
||||
// Sequential mode: use buildSequentialPlan to avoid 29-task mega-batches
|
||||
const plan = useParallel
|
||||
? buildExecutionPlan(project, completed)
|
||||
: buildSequentialPlan(project, completed);
|
||||
|
||||
for (const batch of plan.batches) {
|
||||
if (progress.getState().paused) {
|
||||
ctx.ui.notify(
|
||||
"Execution paused. Use /ralph resume to continue.",
|
||||
"warning",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await executeBatch(
|
||||
batch.batchIndex,
|
||||
batch.tasks,
|
||||
await executePlanBatches(
|
||||
plan,
|
||||
project,
|
||||
taskFile,
|
||||
config,
|
||||
progress,
|
||||
ctx as any,
|
||||
{ parallel: useParallel },
|
||||
ctx,
|
||||
mode,
|
||||
sendChatMessage,
|
||||
projectDir,
|
||||
);
|
||||
|
||||
for (const task of batch.tasks) {
|
||||
const status = progress.getTaskStatus(task.id);
|
||||
updateTaskInFile(taskFile, task.id, status);
|
||||
}
|
||||
}
|
||||
|
||||
const state = progress.getState();
|
||||
const output = formatProgressStatus(state);
|
||||
|
||||
@@ -334,9 +427,8 @@ async function handleStatus(
|
||||
async function handleResume(
|
||||
ctx: ExtensionContext,
|
||||
args: string[],
|
||||
sendChatMessage?: (content: string) => void,
|
||||
sendChatMessage?: SendChatMessage,
|
||||
): Promise<void> {
|
||||
// If a task file arg is provided, find progress for that specific PRD
|
||||
let taskFile: string;
|
||||
let projectDir: string;
|
||||
let found: ReturnType<typeof findProgressFile>;
|
||||
@@ -369,6 +461,11 @@ async function handleResume(
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -377,39 +474,22 @@ async function handleResume(
|
||||
// Set resume status
|
||||
ctx.ui.setStatus("ralph", `Resuming from ${path.basename(taskFile)}`);
|
||||
|
||||
const completed = new Set(progress.getCompletedTaskIds());
|
||||
const completed = buildCompletedSet(progress, project);
|
||||
const mode = await selectExecutionMode(ctx, project, taskFile);
|
||||
const plan = buildPlanByMode(mode, project, completed);
|
||||
|
||||
// Ask user for execution mode
|
||||
const mode = await ctx.ui.select("Execution mode for this run?", [
|
||||
"Parallel (DAG-optimized)",
|
||||
"Sequential (one at a time)",
|
||||
]);
|
||||
const useParallel = mode?.startsWith("Parallel");
|
||||
|
||||
// Sequential mode: use buildSequentialPlan to avoid 29-task mega-batches
|
||||
const plan = useParallel
|
||||
? buildExecutionPlan(project, completed)
|
||||
: buildSequentialPlan(project, completed);
|
||||
|
||||
for (const batch of plan.batches) {
|
||||
await executeBatch(
|
||||
batch.batchIndex,
|
||||
batch.tasks,
|
||||
await executePlanBatches(
|
||||
plan,
|
||||
project,
|
||||
taskFile,
|
||||
config,
|
||||
progress,
|
||||
ctx as any,
|
||||
{ parallel: useParallel },
|
||||
ctx,
|
||||
mode,
|
||||
sendChatMessage,
|
||||
projectDir,
|
||||
);
|
||||
|
||||
for (const task of batch.tasks) {
|
||||
const status = progress.getTaskStatus(task.id);
|
||||
updateTaskInFile(taskFile, task.id, status);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.ui.notify(formatProgressStatus(progress.getState()), "info");
|
||||
}
|
||||
|
||||
@@ -418,7 +498,7 @@ async function handleResume(
|
||||
async function handleNext(
|
||||
ctx: ExtensionContext,
|
||||
args: string[],
|
||||
sendChatMessage?: (content: string) => void,
|
||||
sendChatMessage?: SendChatMessage,
|
||||
): Promise<void> {
|
||||
let taskFile: string;
|
||||
let projectDir: string;
|
||||
@@ -448,10 +528,15 @@ async function handleNext(
|
||||
}
|
||||
|
||||
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 = new Set(progress.getCompletedTaskIds());
|
||||
const completed = buildCompletedSet(progress, project);
|
||||
const ready = getReadyTasks(project, completed);
|
||||
|
||||
if (ready.length === 0) {
|
||||
@@ -469,13 +554,12 @@ async function handleNext(
|
||||
|
||||
for (const task of nextBatch) {
|
||||
await executeBatch(
|
||||
0,
|
||||
[task],
|
||||
project,
|
||||
config,
|
||||
progress,
|
||||
ctx as any,
|
||||
undefined,
|
||||
ctx,
|
||||
{ parallel: false },
|
||||
sendChatMessage,
|
||||
projectDir,
|
||||
);
|
||||
@@ -496,11 +580,12 @@ async function handleReset(
|
||||
ctx: ExtensionContext,
|
||||
args: string[],
|
||||
): Promise<void> {
|
||||
let projectDir: string;
|
||||
if (args[0]) {
|
||||
const taskFile = resolveTaskArg(args[0], process.cwd());
|
||||
const found = findProgressFile(process.cwd(), taskFile);
|
||||
projectDir = found ? path.dirname(path.dirname(found.path)) : process.cwd();
|
||||
const projectDir = found
|
||||
? path.dirname(path.dirname(found.path))
|
||||
: process.cwd();
|
||||
const progress = new ProgressTracker(projectDir, taskFile, found?.prdKey);
|
||||
progress.reset();
|
||||
} else {
|
||||
@@ -524,19 +609,3 @@ async function handleReset(
|
||||
|
||||
ctx.ui.notify("Progress reset. All task statuses cleared.", "info");
|
||||
}
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes % 60}m`;
|
||||
}
|
||||
if (minutes > 0) {
|
||||
return `${minutes}m ${seconds % 60}s`;
|
||||
}
|
||||
return `${seconds}s`;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as path from "node:path";
|
||||
import type { Task, Project, Reflection, ToolUsage } from "./types";
|
||||
import type { RalphConfig } from "./types";
|
||||
import type { ProgressTracker } from "./progress";
|
||||
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
||||
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
||||
import { buildTaskPrompt } from "./prompts";
|
||||
import { extractReflection } from "./reflection";
|
||||
import {
|
||||
@@ -36,7 +36,7 @@ export async function runTask(
|
||||
project: Project,
|
||||
config: RalphConfig,
|
||||
depReflections: Reflection[],
|
||||
ctx: ExtensionCommandContext,
|
||||
ctx: ExtensionContext,
|
||||
sendChatMessage?: SendChatMessage,
|
||||
projectDir: string = project.sourceDir,
|
||||
): Promise<{
|
||||
@@ -210,16 +210,22 @@ function saveSessionOutput(
|
||||
* Execute a batch of tasks (sequentially or in parallel)
|
||||
*/
|
||||
export async function executeBatch(
|
||||
_batchIndex: number,
|
||||
tasks: Task[],
|
||||
project: Project,
|
||||
config: RalphConfig,
|
||||
progress: ProgressTracker,
|
||||
ctx: ExtensionCommandContext,
|
||||
ctx: ExtensionContext,
|
||||
options?: { parallel?: boolean },
|
||||
sendChatMessage?: SendChatMessage,
|
||||
projectDir?: string,
|
||||
): Promise<void> {
|
||||
// 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;
|
||||
@@ -259,7 +265,7 @@ async function executeBatchParallel(
|
||||
project: Project,
|
||||
config: RalphConfig,
|
||||
progress: ProgressTracker,
|
||||
ctx: ExtensionCommandContext,
|
||||
ctx: ExtensionContext,
|
||||
sendChatMessage?: SendChatMessage,
|
||||
projectDir?: string,
|
||||
): Promise<void> {
|
||||
@@ -300,7 +306,7 @@ async function executeTask(
|
||||
project: Project,
|
||||
config: RalphConfig,
|
||||
progress: ProgressTracker,
|
||||
ctx: ExtensionCommandContext,
|
||||
ctx: ExtensionContext,
|
||||
sendChatMessage?: SendChatMessage,
|
||||
projectDir: string = project.sourceDir,
|
||||
): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user