readme cleanup

This commit is contained in:
2026-05-31 08:46:59 -04:00
parent 3c01652b90
commit 9f90ed4252
5 changed files with 509 additions and 675 deletions

View File

@@ -54,7 +54,7 @@ Task IDs are zero-padded strings (`"01"`, `"02"`, etc.). The parser prepends `0`
## Command routing ## Command routing
`/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`). `/ralpi` with no args → plan. First token looks like a path (`@path`, `./path`, `.md`, etc.) → run. Otherwise dispatches to subcommand (`run`, `plan`, `resume`, `reset`).
## Config ## Config

117
README.md
View File

@@ -4,75 +4,33 @@ Execute tasks from task files using DAG-based dependency resolution with persist
## Features ## Features
- **DAG-based execution**: Tasks are ordered by dependencies using Kahn's algorithm
- **Parallel batching**: Independent tasks in each batch can run concurrently - **Parallel batching**: Independent tasks in each batch can run concurrently
- **Persistent progress**: Execution state saved to `.ralpi/progress.json` - **Persistent progress**: Execution state saved to `.ralpi/progress.json`
- **Reflection system**: Each task produces a reflection for downstream tasks - **Reflection system**: Each task produces a reflection for downstream tasks
- **Retry with backoff**: Failed tasks retry with exponential backoff - **Retry with backoff**: Failed tasks retry with exponential backoff
- **Multiple formats**: Supports Fio README, simple checkboxes, and YAML - **Multiple formats**: Supports simple checkboxes, and YAML
- **Chat progress**: Real-time progress messages in Pi chat via `pi.sendMessage`
- **Tool usage tracking**: Detects and reports tool usage (read, write, edit, bash) from task execution - **Tool usage tracking**: Detects and reports tool usage (read, write, edit, bash) from task execution
- **Git commit capture**: Captures git commit messages and generates summaries per task
- **Configurable timeouts**: Task-level timeouts via meta blocks, with global fallback - **Configurable timeouts**: Task-level timeouts via meta blocks, with global fallback
- **Session saving**: Saves full task output for expandable session review - **Session saving**: Saves full task output for expandable session review
- **Resume auto-discovery**: Automatically finds and resumes interrupted execution - **Resume auto-discovery**: Automatically finds and resumes interrupted execution
- **Custom message renderer**: Compact UI labels with expandable details in Pi TUI
## Usage ## Usage
``` ```
/ralpi plan [task-file] # Show execution plan /ralpi [task-file] # Execute all tasks
/ralpi run [task-file] # Execute all tasks /ralpi plan # Alias to /task-manager to plan new tasks
/ralpi status [task-file] # Show current progress /ralpi resume # Resume paused execution
/ralpi resume [task-file] # Resume paused execution /ralpi reset [task-file] # Reset progress and .ralpi directory - does not modify PRD
/ralpi next [task-file] # Execute next batch only
/ralpi reset [task-file] # Reset all progress
``` ```
## Task File Formats ## Task File Formats
### Fio README Format ### Highly recommended to use the task-manager prompt for prd construction, it's output pairs perfectly
```markdown
# Project Title # Project Title
## Tasks ## Tasks
- [ ] 01 — Setup project structure -> `tasks/01-setup.md`
- [ ] 02 — Implement auth -> `tasks/02-auth.md`
- [ ] 03 — Build API -> `tasks/03-api.md`
## Dependencies
1 -> 2,3
2 -> 3
```
#### Supported Dependency Formats
The parser supports two dependency declaration styles in the `## Dependencies` section:
**Arrow Notation** (recommended):
```
1 -> 2,3,4
5 -> 6
```
This means: "Task 1 must complete before tasks 2, 3, and 4 can start."
**Natural Language**:
```
13 depends on 17, 18, 19, 20
14 depends on 13, 15, 16
```
This means: "Task 13 depends on tasks 17, 18, 19, and 20."
**Parallel Groups** (informational only):
```
1, 2, 3, 4 can be done in parallel
5, 6, 7, 8 can be done in parallel
```
Note: These lines are ignored by the parser. Use explicit dependencies to control execution order.
### Simple Checkbox Format ### Simple Checkbox Format
```markdown ```markdown
@@ -96,9 +54,45 @@ tasks:
depends_on: ["01"] depends_on: ["01"]
``` ```
## Dependencies
### Arrow Notation (recommended):
1 -> 2,3,4
5 -> 6
This means: "Task 1 must complete before tasks 2, 3, and 4 can start."
### Natural Language:
13 depends on 17, 18, 19, 20
14 depends on 13, 15, 16
This means: "Task 13 depends on tasks 17, 18, 19, and 20."
### Parallel Groups (informational only):
1, 2, 3, 4 can be done in parallel
5, 6, 7, 8 can be done in parallel
Note: These lines are ignored by the parser. Use explicit dependencies to control execution order.
## Configuration ## Configuration
Create config files. Both are optional: ### Task-Level Timeout
You can set a timeout for individual tasks using a meta block in the task file:
```markdown
- [ ] 01: Setup project structure
timeout: 10m
```
Supported formats: `10m` (minutes), `600s` (seconds), `3600000` (milliseconds)
### Config files
| Scope | Path | | Scope | Path |
|-------|------| |-------|------|
@@ -115,9 +109,6 @@ prompts:
projectContext: "Additional context for all tasks" projectContext: "Additional context for all tasks"
``` ```
> ralpi deliberately does **not** set timeouts or retries — those are inherited
> from Pi's own settings. Tasks run until they complete or Pi's own flow stops them.
>
> `execution.models` uses slot-aware round-robin: with 3 models and 2 concurrent > `execution.models` uses slot-aware round-robin: with 3 models and 2 concurrent
> tasks, only the first two models are used. The third model is only touched when > tasks, only the first two models are used. The third model is only touched when
> a third concurrent task starts. Freed model slots are reused before new ones > a third concurrent task starts. Freed model slots are reused before new ones
@@ -127,28 +118,6 @@ prompts:
> the task automatically cycles to the next model in the list without counting it > the task automatically cycles to the next model in the list without counting it
> as a task failure. Each model is tried once before the task is marked as failed. > as a task failure. Each model is tried once before the task is marked as failed.
The keys mirror the nested structure of `RalpiConfig` in `src/types.ts`.
### Precedence (highest wins)
| Priority | Source |
|----------|--------|
| **1st** | In-memory overrides (`model`, `thinkingLevel` from parent Pi session) |
| **2nd** | `./.ralpi/config.yaml` — project-level |
| **3rd** | `~/.pi/ralpi/config.yaml` — global, shared across projects |
| **4th** | `DEFAULT_CONFIG` in `src/types.ts` |
### Task-Level Timeout
You can set a timeout for individual tasks using a meta block in the task file:
```markdown
- [ ] 01: Setup project structure
timeout: 10m
```
Supported formats: `10m` (minutes), `600s` (seconds), `3600000` (milliseconds)
## State Files ## State Files
- `.ralpi/progress.json` - Execution progress - `.ralpi/progress.json` - Execution progress

144
index.ts
View File

@@ -10,8 +10,6 @@ import {
buildExecutionPlan, buildExecutionPlan,
buildSequentialPlan, buildSequentialPlan,
formatExecutionPlan, formatExecutionPlan,
getReadyTasks,
getBlockedTasks,
} from "./src/dag"; } from "./src/dag";
import { ProgressTracker } from "./src/progress"; import { ProgressTracker } from "./src/progress";
import { buildPlanPrompt } from "./src/prompts"; import { buildPlanPrompt } from "./src/prompts";
@@ -21,11 +19,10 @@ import {
loadConfig, loadConfig,
resolveTaskArg, resolveTaskArg,
formatProgressStatus, formatProgressStatus,
formatAllPRDsStatus,
findProgressFile, findProgressFile,
} from "./src/utils"; } from "./src/utils";
const COMMANDS = ["status", "resume", "next", "reset"] as const; const COMMANDS = ["plan", "resume", "reset"] as const;
type ExecutionMode = "parallel" | "sequential"; type ExecutionMode = "parallel" | "sequential";
@@ -320,9 +317,9 @@ export default function ralpiLoopExtension(pi: ExtensionAPI): void {
pi.getThinkingLevel(), pi.getThinkingLevel(),
); );
case "plan": case "plan":
return handlePlan(ctx, parts.slice(1)); pi.sendUserMessage("@task-manager");
case "status": ctx.ui.notify("Opening Task Manager...", "info");
return handleStatus(ctx, parts.slice(1)); return;
case "resume": case "resume":
return handleResume( return handleResume(
ctx, ctx,
@@ -331,14 +328,6 @@ export default function ralpiLoopExtension(pi: ExtensionAPI): void {
ctx.model, ctx.model,
pi.getThinkingLevel(), pi.getThinkingLevel(),
); );
case "next":
return handleNext(
ctx,
parts.slice(1),
sendProgress,
ctx.model,
pi.getThinkingLevel(),
);
case "reset": case "reset":
return handleReset(ctx, parts.slice(1)); return handleReset(ctx, parts.slice(1));
default: { default: {
@@ -473,46 +462,7 @@ async function handleRun(
} }
// ─── /ralpi status ─────────────────────────────────────────────────────────── // ─── /ralpi status ───────────────────────────────────────────────────────────
// (removed — use /ralpi plan to invoke @task-manager)
async function handleStatus(
ctx: ExtensionContext,
args: string[],
): Promise<void> {
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 .ralpi/progress.json found. Start with /ralpi run [task-file]",
"warning",
);
return;
}
ctx.ui.notify(formatAllPRDsStatus(found.state), "info");
}
// ─── /ralpi resume ─────────────────────────────────────────────────────────── // ─── /ralpi resume ───────────────────────────────────────────────────────────
@@ -587,89 +537,7 @@ async function handleResume(
} }
// ─── /ralpi next ───────────────────────────────────────────────────────────── // ─── /ralpi next ─────────────────────────────────────────────────────────────
// (removed — use /ralpi run to execute tasks)
async function handleNext(
ctx: ExtensionContext,
args: string[],
sendChatMessage?: SendChatMessage,
parentModel?: unknown,
parentThinkingLevel?: unknown,
): Promise<void> {
let taskFile: string;
let projectDir: string;
let found: ReturnType<typeof findProgressFile>;
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);
config.model = parentModel ?? ctx.model;
config.thinkingLevel = parentThinkingLevel;
const progress = new ProgressTracker(projectDir, taskFile, found?.prdKey);
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;
}
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));
}
ctx.ui.notify(
`Executed: ${nextBatch
.map((t) => t.id)
.join(", ")}\n\n${formatProgressStatus(progress.getState())}`,
"info",
);
}
// ─── /ralpi reset ──────────────────────────────────────────────────────────── // ─── /ralpi reset ────────────────────────────────────────────────────────────

View File

@@ -8,7 +8,7 @@
"task-runner", "task-runner",
"dag", "dag",
"task-manager", "task-manager",
"ralpi-loop", "ralph-loop",
"prd" "prd"
], ],
"author": "Michael Freno", "author": "Michael Freno",
@@ -54,14 +54,6 @@
"@earendil-works/pi-coding-agent": "*", "@earendil-works/pi-coding-agent": "*",
"@earendil-works/pi-tui": "*" "@earendil-works/pi-tui": "*"
}, },
"peerDependenciesMeta": {
"@earendil-works/pi-coding-agent": {
"optional": true
},
"@earendil-works/pi-tui": {
"optional": true
}
},
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
}, },

View File

@@ -296,8 +296,8 @@ export async function runTask(
} }
if (!output.success) { if (!output.success) {
sendChatMessage?.(`${taskHeader}${output.error}`); // Failure reporting is handled by the caller (executeTask) to avoid
ctx.ui.notify(`Task ${task.id} failed: ${output.error}`, "error"); // duplicate messages when model failover or retry cycling is active.
return { return {
success: false, success: false,
error: output.error, error: output.error,
@@ -433,6 +433,7 @@ export async function executeBatch(
roundRobin?.release(task.id); roundRobin?.release(task.id);
const errorMsg = error instanceof Error ? error.message : String(error); const errorMsg = error instanceof Error ? error.message : String(error);
progress.markFailed(task.id, errorMsg); progress.markFailed(task.id, errorMsg);
sendChatMessage?.(`${task.id} · ${task.title}${errorMsg}`);
ctx.ui.notify(`Task ${task.id} failed: ${errorMsg}`, "error"); ctx.ui.notify(`Task ${task.id} failed: ${errorMsg}`, "error");
break; break;
} }
@@ -555,6 +556,7 @@ async function executeBatchParallel(
roundRobin?.release(task.id); roundRobin?.release(task.id);
const errorMsg = error instanceof Error ? error.message : String(error); const errorMsg = error instanceof Error ? error.message : String(error);
progress.markFailed(task.id, errorMsg); progress.markFailed(task.id, errorMsg);
sendChatMessage?.(`${task.id} · ${task.title}${errorMsg}`);
ctx.ui.notify(`Task ${task.id} failed: ${errorMsg}`, "error"); ctx.ui.notify(`Task ${task.id} failed: ${errorMsg}`, "error");
}), }),
}); });
@@ -658,9 +660,8 @@ async function executeTask(
// release() would put the slot in freeSlots, then assign() // release() would put the slot in freeSlots, then assign()
// would pick it right back up, getting stuck on the same model. // would pick it right back up, getting stuck on the same model.
modelAttempt++; modelAttempt++;
ctx.ui.notify( sendChatMessage?.(
`Task ${task.id}: model failed, trying next (${modelAttempt + 1}/${maxModelAttempts}): ${result.error}`, `~ ${task.id} · ${task.title} trying model ${modelAttempt + 1}/${maxModelAttempts} (previous: ${result.error})`,
"warning",
); );
break; // exit retry loop, cycle to next model break; // exit retry loop, cycle to next model
} }
@@ -668,9 +669,8 @@ async function executeTask(
// No more models — use normal retry logic // No more models — use normal retry logic
if (retries < maxRetries) { if (retries < maxRetries) {
retries = progress.incrementRetry(task.id); retries = progress.incrementRetry(task.id);
ctx.ui.notify( sendChatMessage?.(
`Retrying task ${task.id} (${retries}/${maxRetries}): ${result.error}`, `~ ${task.id} · ${task.title} — retrying (${retries}/${maxRetries}): ${result.error}`,
"warning",
); );
// Exponential backoff // Exponential backoff
@@ -679,6 +679,7 @@ async function executeTask(
} else { } else {
// Max retries exceeded // Max retries exceeded
progress.markFailed(task.id, result.error || "Unknown error"); progress.markFailed(task.id, result.error || "Unknown error");
sendChatMessage?.(`${task.id} · ${task.title}${result.error}`);
ctx.ui.notify( ctx.ui.notify(
`Task ${task.id} failed after ${maxRetries} retries: ${ `Task ${task.id} failed after ${maxRetries} retries: ${
result.error || "Unknown error" result.error || "Unknown error"
@@ -691,6 +692,7 @@ async function executeTask(
roundRobin?.release(task.id); roundRobin?.release(task.id);
const errorMsg = error instanceof Error ? error.message : String(error); const errorMsg = error instanceof Error ? error.message : String(error);
progress.markFailed(task.id, errorMsg); progress.markFailed(task.id, errorMsg);
sendChatMessage?.(`${task.id} · ${task.title}${errorMsg}`);
ctx.ui.notify(`Task ${task.id} failed: ${errorMsg}`, "error"); ctx.ui.notify(`Task ${task.id} failed: ${errorMsg}`, "error");
return; return;
} }
@@ -703,6 +705,9 @@ async function executeTask(
// All models exhausted — release the slot // All models exhausted — release the slot
roundRobin?.release(task.id); roundRobin?.release(task.id);
progress.markFailed(task.id, "All configured models exhausted"); progress.markFailed(task.id, "All configured models exhausted");
sendChatMessage?.(
`${task.id} · ${task.title} — all ${maxModelAttempts} models exhausted`,
);
ctx.ui.notify( ctx.ui.notify(
`Task ${task.id} failed: all configured models exhausted`, `Task ${task.id} failed: all configured models exhausted`,
"error", "error",