dependency parsing broken

This commit is contained in:
2026-05-30 20:35:02 -04:00
parent fcc0aa618e
commit 73d1ee1a47
3 changed files with 522 additions and 447 deletions

249
index.ts
View File

@@ -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`;
}

View File

@@ -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> {