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
`/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

117
README.md
View File

@@ -4,75 +4,33 @@ Execute tasks from task files using DAG-based dependency resolution with persist
## Features
- **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 `.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
- **Chat progress**: Real-time progress messages in Pi chat via `pi.sendMessage`
- **Multiple formats**: Supports simple checkboxes, and YAML
- **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
- **Session saving**: Saves full task output for expandable session review
- **Resume auto-discovery**: Automatically finds and resumes interrupted execution
- **Custom message renderer**: Compact UI labels with expandable details in Pi TUI
## Usage
```
/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
/ralpi [task-file] # Execute all tasks
/ralpi plan # Alias to /task-manager to plan new tasks
/ralpi resume # Resume paused execution
/ralpi reset [task-file] # Reset progress and .ralpi directory - does not modify PRD
```
## 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
## 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
```markdown
@@ -96,9 +54,45 @@ tasks:
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
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 |
|-------|------|
@@ -115,9 +109,6 @@ prompts:
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
> 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
@@ -127,28 +118,6 @@ prompts:
> 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.
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
- `.ralpi/progress.json` - Execution progress

144
index.ts
View File

@@ -10,8 +10,6 @@ import {
buildExecutionPlan,
buildSequentialPlan,
formatExecutionPlan,
getReadyTasks,
getBlockedTasks,
} from "./src/dag";
import { ProgressTracker } from "./src/progress";
import { buildPlanPrompt } from "./src/prompts";
@@ -21,11 +19,10 @@ import {
loadConfig,
resolveTaskArg,
formatProgressStatus,
formatAllPRDsStatus,
findProgressFile,
} from "./src/utils";
const COMMANDS = ["status", "resume", "next", "reset"] as const;
const COMMANDS = ["plan", "resume", "reset"] as const;
type ExecutionMode = "parallel" | "sequential";
@@ -320,9 +317,9 @@ export default function ralpiLoopExtension(pi: ExtensionAPI): void {
pi.getThinkingLevel(),
);
case "plan":
return handlePlan(ctx, parts.slice(1));
case "status":
return handleStatus(ctx, parts.slice(1));
pi.sendUserMessage("@task-manager");
ctx.ui.notify("Opening Task Manager...", "info");
return;
case "resume":
return handleResume(
ctx,
@@ -331,14 +328,6 @@ export default function ralpiLoopExtension(pi: ExtensionAPI): void {
ctx.model,
pi.getThinkingLevel(),
);
case "next":
return handleNext(
ctx,
parts.slice(1),
sendProgress,
ctx.model,
pi.getThinkingLevel(),
);
case "reset":
return handleReset(ctx, parts.slice(1));
default: {
@@ -473,46 +462,7 @@ async function handleRun(
}
// ─── /ralpi status ───────────────────────────────────────────────────────────
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");
}
// (removed — use /ralpi plan to invoke @task-manager)
// ─── /ralpi resume ───────────────────────────────────────────────────────────
@@ -587,89 +537,7 @@ async function handleResume(
}
// ─── /ralpi next ─────────────────────────────────────────────────────────────
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",
);
}
// (removed — use /ralpi run to execute tasks)
// ─── /ralpi reset ────────────────────────────────────────────────────────────

View File

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

View File

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