drop tasks, drop thrashing bandaid

This commit is contained in:
2026-05-30 23:16:17 -04:00
parent c7ab908bae
commit 923f174f3b
14 changed files with 392 additions and 309 deletions

62
AGENTS.md Normal file
View File

@@ -0,0 +1,62 @@
# AGENTS.md
## What this is
A Pi coding agent extension that registers the `/ralph` slash command. Not a standalone app — it runs inside Pi's extension host.
## Build
```
npm run build # tsc → dist/
npm run watch # tsc --watch
```
No bundler, no linter, no test framework. Plain `tsc` with strict mode.
## Entry point
`index.ts` at repo root (not `src/`). Exports a default function receiving `ExtensionAPI`. The `tsconfig.json` sets `rootDir: "./"` so `index.ts` compiles to `dist/index.js`.
## External dependencies
The extension imports from Pi SDK packages (not in `package.json` — provided by the host):
- `@earendil-works/pi-coding-agent``ExtensionAPI`, `ExtensionContext`, `createAgentSession`, etc.
- `@earendil-works/pi-tui``Box`, `Text` for custom message renderer
The only real npm dependency is `yaml` (^2.4.0).
## Source structure
- `index.ts` — extension entry, command routing, UI registration
- `src/` — all logic modules:
- `parser.ts` — task file parsing (Fio, checkbox, YAML formats)
- `dag.ts` — Kahn's algorithm dependency resolution, batch planning
- `executor.ts` — task execution, retry, parallel/sequential modes
- `progress.ts``.ralph/progress.json` state management
- `prompts.ts` — prompt generation for spawned agent sessions
- `reflection.ts` — reflection extraction from agent output
- `utils.ts` — config loading, progress discovery, `runAgentSession()`
- `types.ts` — all interfaces and `DEFAULT_CONFIG`
- `widget-batcher.ts` — debounced widget updates for parallel tasks
- `skills/ralph-task/SKILL.md` — Pi skill definition for task execution
- `tasks/` — example ralph task files (self-modification history)
## Runtime state
All runtime state lives in `.ralph/` (gitignored):
- `.ralph/progress.json` — execution progress, supports multiple PRDs
- `.ralph/reflections/` — per-task reflection JSON files
- `.ralph/prompts/` — generated prompts (timestamped, for debugging)
- `.ralph/sessions/` — full session transcripts
## Task ID convention
Task IDs are zero-padded strings (`"01"`, `"02"`, etc.). The parser prepends `0` to parsed digits. Never use raw numeric IDs.
## Command routing
`/ralph` 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`).
## Config
Read from `.ralph/config.yaml` in project directory. Falls back to `DEFAULT_CONFIG` in `src/types.ts` when file is missing. Config is loaded at `projectDir` level, not extension level.

View File

@@ -1,4 +1,4 @@
# ralph-loop # Ralpi
Execute tasks from task files using DAG-based dependency resolution with persistent progress tracking. Execute tasks from task files using DAG-based dependency resolution with persistent progress tracking.

View File

@@ -3,13 +3,38 @@
"version": "1.0.0", "version": "1.0.0",
"description": "Execute tasks from task files using DAG-based dependency resolution with persistent progress tracking", "description": "Execute tasks from task files using DAG-based dependency resolution with persistent progress tracking",
"main": "dist/index.js", "main": "dist/index.js",
"keywords": [
"pi-package",
"pi-extension",
"task-runner",
"dag",
"task-manager"
],
"author": "",
"license": "MIT",
"files": [
"dist/",
"skills/",
"prompts/",
"index.ts"
],
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"watch": "tsc --watch" "watch": "tsc --watch",
"prepublishOnly": "npm run build"
},
"pi": {
"extensions": ["./dist/index.js"],
"skills": ["./skills"],
"prompts": ["./prompts"]
}, },
"dependencies": { "dependencies": {
"yaml": "^2.4.0" "yaml": "^2.4.0"
}, },
"peerDependencies": {
"@earendil-works/pi-coding-agent": "*",
"@earendil-works/pi-tui": "*"
},
"devDependencies": { "devDependencies": {
"@types/node": "^20.0.0", "@types/node": "^20.0.0",
"typescript": "^5.3.0" "typescript": "^5.3.0"

160
prompts/task-manager.md Normal file
View File

@@ -0,0 +1,160 @@
---
name: task-manager
description: Breaks down complex features into small, verifiable subtasks
tools: read, edit, write, web_search, code_search, fetch_content, get_search_content, mcp, memory, skill, session_search, memory_search, ask_user_question, ctx_execute, ctx_execute_file, ctx_index, ctx_search, ctx_batch_execute
systemPromptMode: replace
inheritProjectContext: false
inheritSkills: false
defaultContext: fork
---
# Task Manager (@task-manager)
Purpose:
You are a Task Manager (@task-manager), an expert at breaking down complex software features into small, verifiable subtasks. Your role is to create structured task plans that enable efficient, atomic implementation work.
## Core Responsibilities
- Break complex features into atomic tasks
- Create structured directories with task files and indexes
- Generate clear acceptance criteria and dependency mapping
- Follow strict naming conventions and file templates
## Mandatory Two-Phase Workflow
### Phase 1: Planning (Approval Required)
When given a complex feature request:
1. **Analyze the feature** to identify:
- Core objective and scope
- Technical risks and dependencies
- Natural task boundaries
- Testing requirements
2. **Create a subtask plan** with:
- Feature slug (kebab-case)
- Clear task sequence and dependencies
- Exit criteria for feature completion
3. **Present plan using this exact format:**```
## Subtask Plan
feature: {kebab-case-feature-name}
objective: {one-line description}
tasks:
- seq: {2-digit}, filename: {seq}-{task-description}.md, title: {clear title}
- seq: {2-digit}, filename: {seq}-{task-description}.md, title: {clear title}
dependencies:
- {seq} -> {seq} (task dependencies)
exit_criteria:
- {specific, measurable completion criteria}
Approval needed before file creation.
```
4. **Wait for explicit approval** before proceeding to Phase 2.
### Phase 2: File Creation (After Approval)
Once approved:
1. **Create directory structure:**
- Base: `tasks/{feature}/`
- Create feature README.md index
- Create individual task files
2. **Use these exact templates (Dependencies only if applicable):**
**Feature Index Template** (`tasks/{feature}/README.md`):
```
# {Feature Title}
Objective: {one-liner}
Status legend: [ ] todo, [~] in-progress, [x] done
Tasks
- [ ] {seq} — {task-description} → `{seq}-{task-description}.md`
Dependencies
- {seq} depends on {seq}
Exit criteria
- The feature is complete when {specific criteria}
```
**Task File Template** (`{seq}-{task-description}.md`):
```
# {seq}. {Title}
meta:
id: {feature}-{seq}
feature: {feature}
priority: P2
depends_on: [{dependency-ids}]
tags: [implementation, tests-required]
objective:
- Clear, single outcome for this task
deliverables:
- What gets added/changed (files, modules, endpoints)
steps:
- Step-by-step actions to complete the task
tests:
- Unit: which functions/modules to cover (ArrangeActAssert)
- Integration/e2e: how to validate behavior
acceptance_criteria:
- Observable, binary pass/fail conditions
validation:
- Commands or scripts to run and how to verify
notes:
- Assumptions, links to relevant docs or design
```
3. **Provide creation summary:**
```
## Subtasks Created
- tasks/{feature}/README.md
- tasks/{feature}/{seq}-{task-description}.md
Next suggested task: {seq} — {title}
```
## Strict Conventions
- **Naming:** Always use kebab-case for features and task descriptions
- **Sequencing:** 2-digits (01, 02, 03...)
- **File pattern:** `{seq}-{task-description}.md`
- **Dependencies:** Always map task relationships (if applicable)
- **Tests:** Every task must include test requirements
- **Acceptance:** Must have binary pass/fail criteria
## Quality Guidelines
- Keep tasks atomic and implementation-ready
- Include clear validation steps
- Specify exact deliverables (files, functions, endpoints)
- Use functional, declarative language
- Avoid unnecessary complexity
- Ensure each task can be completed independently (given dependencies)
## Available Tools
You have access to: read,edit,write,grep,glob,patch (but NOT bash)
You cannot modify: .env files, .key files, .secret files, node_modules, .git
## Response Instructions
- Always follow the two-phase workflow exactly
- Use the exact templates and formats provided
- Wait for approval after Phase 1
- Provide clear, actionable task breakdowns
- Include all required metadata and structure
Break down the complex features into subtasks and create a task plan. Put all tasks in the /tasks/ directory.
Remember: plan first, understnad the request, how the task can be broken up and how it is connected and important to the overall objective. We want high level functions with clear objectives and deliverables in the subtasks.
---
User request: $@

View File

@@ -5,6 +5,7 @@ import type { ProgressTracker } from "./progress";
import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
import { buildTaskPrompt } from "./prompts"; import { buildTaskPrompt } from "./prompts";
import { extractReflection } from "./reflection"; import { extractReflection } from "./reflection";
import { WidgetBatcher } from "./widget-batcher";
import { import {
runAgentSession, runAgentSession,
writeFileSafe, writeFileSafe,
@@ -39,6 +40,7 @@ export async function runTask(
ctx: ExtensionContext, ctx: ExtensionContext,
sendChatMessage?: SendChatMessage, sendChatMessage?: SendChatMessage,
projectDir: string = project.sourceDir, projectDir: string = project.sourceDir,
batcher?: WidgetBatcher,
): Promise<{ ): Promise<{
success: boolean; success: boolean;
reflection?: Reflection; reflection?: Reflection;
@@ -104,7 +106,11 @@ export async function runTask(
} }
} }
ctx.ui.setWidget(widgetKey, lines); if (batcher) {
batcher.schedule(widgetKey, lines);
} else {
ctx.ui.setWidget(widgetKey, lines);
}
}; };
// Smooth spinner animation at 100ms intervals // Smooth spinner animation at 100ms intervals
@@ -119,6 +125,11 @@ export async function runTask(
// Use task-level timeout if set, otherwise fall back to config // Use task-level timeout if set, otherwise fall back to config
const timeoutMs = task.timeoutMs ?? config.execution.timeoutMs; const timeoutMs = task.timeoutMs ?? config.execution.timeoutMs;
// Pre-create session file path so events stream to disk (avoids 300+ MB in-memory accumulation)
const sessionsDir = path.join(ralphDir, "sessions");
ensureDir(sessionsDir);
const sessionFilePath = path.join(sessionsDir, `${task.id}-${startMs}.txt`);
// Run task asynchronously via Pi SDK — event loop stays responsive // Run task asynchronously via Pi SDK — event loop stays responsive
const output = await runAgentSession( const output = await runAgentSession(
prompt, prompt,
@@ -134,13 +145,19 @@ export async function runTask(
updateWidget(); updateWidget();
} }
}, },
undefined, // no abort signal
sessionFilePath, // stream events to file
); );
const durationMs = Date.now() - startMs; const durationMs = Date.now() - startMs;
// Clear progress widget and status after task finishes // Clear progress widget and status after task finishes
clearInterval(spinnerTimer); clearInterval(spinnerTimer);
ctx.ui.setWidget(widgetKey, undefined); if (batcher) {
batcher.scheduleRemove(widgetKey);
} else {
ctx.ui.setWidget(widgetKey, undefined);
}
ctx.ui.setStatus("ralph", undefined); ctx.ui.setStatus("ralph", undefined);
if (!output.success) { if (!output.success) {
@@ -150,6 +167,7 @@ export async function runTask(
success: false, success: false,
error: output.error, error: output.error,
durationMs, durationMs,
sessionFile: sessionFilePath, // events streamed to file for debugging
}; };
} }
@@ -159,12 +177,8 @@ export async function runTask(
// Capture git commits made during this task // Capture git commits made during this task
const { commitMessages, commitSummary } = captureGitCommits(projectDir); const { commitMessages, commitSummary } = captureGitCommits(projectDir);
// Save full session transcript to .ralph/sessions/ // Session file already written by runAgentSession (events streamed to disk)
const sessionFile = saveSessionOutput( const sessionFile = sessionFilePath;
projectDir,
task.id,
JSON.stringify(output.events, null, 2),
);
// Build output preview (first 500 chars of agent text) // Build output preview (first 500 chars of agent text)
const outputPreview = const outputPreview =
@@ -191,21 +205,6 @@ export async function runTask(
}; };
} }
// ─── Save Session Output ────────────────────────────────────────────────────
function saveSessionOutput(
sourceDir: string,
taskId: string,
output: string,
): string {
const sessionsDir = path.join(sourceDir, ".ralph", "sessions");
ensureDir(sessionsDir);
const fileName = `${taskId}-${Date.now()}.txt`;
const filePath = path.join(sessionsDir, fileName);
writeFileSafe(filePath, output);
return filePath;
}
// ─── Execute Batch ─────────────────────────────────────────────────────────── // ─── Execute Batch ───────────────────────────────────────────────────────────
/** /**
@@ -272,6 +271,7 @@ async function executeBatchParallel(
projectDir?: string, projectDir?: string,
): Promise<void> { ): Promise<void> {
const maxParallel = config.execution.maxParallel; const maxParallel = config.execution.maxParallel;
const batcher = new WidgetBatcher(ctx);
const results: Array<{ task: Task; result: Promise<any> }> = []; const results: Array<{ task: Task; result: Promise<any> }> = [];
for (const task of tasks) { for (const task of tasks) {
@@ -285,6 +285,7 @@ async function executeBatchParallel(
ctx, ctx,
sendChatMessage, sendChatMessage,
projectDir, projectDir,
batcher,
), ),
}); });
@@ -299,6 +300,9 @@ async function executeBatchParallel(
for (const { result } of results) { for (const { result } of results) {
await result; await result;
} }
// Flush and stop the batcher after all tasks complete
batcher.stop();
} }
// ─── Execute Single Task with Retry ────────────────────────────────────────── // ─── Execute Single Task with Retry ──────────────────────────────────────────
@@ -311,6 +315,7 @@ async function executeTask(
ctx: ExtensionContext, ctx: ExtensionContext,
sendChatMessage?: SendChatMessage, sendChatMessage?: SendChatMessage,
projectDir: string = project.sourceDir, projectDir: string = project.sourceDir,
batcher?: WidgetBatcher,
): Promise<void> { ): Promise<void> {
const maxRetries = config.execution.maxRetries; const maxRetries = config.execution.maxRetries;
let retries = 0; let retries = 0;
@@ -334,6 +339,7 @@ async function executeTask(
ctx, ctx,
sendChatMessage, sendChatMessage,
projectDir, projectDir,
batcher,
); );
if (result.success) { if (result.success) {

View File

@@ -338,6 +338,7 @@ export async function runAgentSession(
timeoutMs: number, timeoutMs: number,
onEvent?: (event: AgentSessionEvent) => void, onEvent?: (event: AgentSessionEvent) => void,
signal?: AbortSignal, signal?: AbortSignal,
sessionFile?: string,
): Promise<{ ): Promise<{
success: boolean; success: boolean;
text: string; text: string;
@@ -353,7 +354,12 @@ export async function runAgentSession(
bash: 0, bash: 0,
other: 0, other: 0,
}; };
const recordedEvents: AgentSessionEvent[] = []; // Stream events to file instead of accumulating in memory.
// Accumulating caused "Invalid string length" crashes when
// JSON.stringify(output.events, null, 2) produced 300+ MB strings.
const eventStream = sessionFile
? fs.createWriteStream(sessionFile, { flags: "a" })
: null;
// Wire timeout via abort signal // Wire timeout via abort signal
const timeoutHandle = setTimeout(() => { const timeoutHandle = setTimeout(() => {
@@ -393,7 +399,10 @@ export async function runAgentSession(
let stopReason: string | undefined; let stopReason: string | undefined;
const unsubscribe = result.session.subscribe((event) => { const unsubscribe = result.session.subscribe((event) => {
recordedEvents.push(event); // Stream event to file (avoids accumulating 300+ MB in memory)
if (eventStream) {
eventStream.write(JSON.stringify(event) + "\n");
}
onEvent?.(event); onEvent?.(event);
if (event.type === "message_end") { if (event.type === "message_end") {
@@ -430,6 +439,11 @@ export async function runAgentSession(
signal?.removeEventListener("abort", abortHandler); signal?.removeEventListener("abort", abortHandler);
clearTimeout(timeoutHandle); clearTimeout(timeoutHandle);
// Flush and close the event stream before returning
if (eventStream) {
await new Promise<void>((resolve) => eventStream.end(resolve));
}
if (errorMessage && !finalText) { if (errorMessage && !finalText) {
return { return {
success: false, success: false,
@@ -437,7 +451,7 @@ export async function runAgentSession(
error: errorMessage, error: errorMessage,
toolUsage, toolUsage,
stopReason, stopReason,
events: recordedEvents, events: [], // streamed to file
}; };
} }
@@ -446,16 +460,19 @@ export async function runAgentSession(
text: finalText.trim(), text: finalText.trim(),
toolUsage, toolUsage,
stopReason, stopReason,
events: recordedEvents, events: [], // streamed to file
}; };
} catch (error) { } catch (error) {
clearTimeout(timeoutHandle); clearTimeout(timeoutHandle);
if (eventStream && !eventStream.destroyed) {
eventStream.end();
}
return { return {
success: false, success: false,
text: "", text: "",
error: error instanceof Error ? error.message : String(error), error: error instanceof Error ? error.message : String(error),
toolUsage, toolUsage,
events: recordedEvents, events: [], // streamed to file
}; };
} finally { } finally {
sessionRef.session?.dispose(); sessionRef.session?.dispose();

92
src/widget-batcher.ts Normal file
View File

@@ -0,0 +1,92 @@
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
/**
* Batches widget updates from multiple parallel tasks into a single
* render cycle, preventing TUI thrashing when agents update independently.
*
* Uses microtask debouncing: updates within the same event-loop tick
* are coalesced into one flush. No artificial interval — updates hit the
* screen as soon as the current tick yields, but never duplicatively.
*/
export class WidgetBatcher {
/** Pending widget updates keyed by widget key. */
private pending: Map<string, string[]> = new Map();
/** Widget keys scheduled for removal. */
private pendingRemovals: Set<string> = new Set();
/** Whether a microtask flush is already queued. */
private scheduled = false;
/** Whether a flush is currently executing (prevents re-entry). */
private flushing = false;
constructor(private ctx: ExtensionContext) {}
/**
* Schedule a widget update. Flushed asynchronously at end of the
* current event-loop tick; multiple calls in the same tick coalesce.
*/
schedule(key: string, lines: string[]): void {
this.pending.set(key, lines);
this.scheduleFlush();
}
/**
* Remove a widget (e.g., when a task completes).
* Flushed asynchronously at end of the current tick.
*/
scheduleRemove(key: string): void {
this.pending.delete(key);
this.pendingRemovals.add(key);
this.scheduleFlush();
}
/** Synchronously flush all pending updates. */
flush(): void {
this.doFlush();
}
/** Flush remaining updates then stop scheduling. */
stop(): void {
this.doFlush();
}
// ── Internal ────────────────────────────────────────────────────────
private scheduleFlush(): void {
if (this.scheduled) return;
this.scheduled = true;
queueMicrotask(() => {
this.scheduled = false;
this.doFlush();
});
}
private doFlush(): void {
if (this.flushing) return;
this.flushing = true;
// Atomically swap — new schedule()/scheduleRemove() calls land on fresh
// collections, so the batch we iterate stays immutable and nothing is lost.
const toRender = this.pending;
const toRemove = this.pendingRemovals;
this.pending = new Map();
this.pendingRemovals = new Set();
// Apply removals first
for (const key of toRemove) {
this.ctx.ui.setWidget(key, undefined);
}
// Sort by key for deterministic, stable ordering across every flush.
// Task IDs are zero-padded ("008", "012", "013") so alpha sort = numeric order.
const sortedKeys = Array.from(toRender.keys()).sort();
for (const key of sortedKeys) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.ctx.ui.setWidget(key, toRender.get(key)!);
}
this.flushing = false;
}
}

View File

@@ -1,38 +0,0 @@
# 01. Fix `loadConfig` to return defaults gracefully when `.ralph/config.yaml` is missing
meta:
id: ralph-loop-fixes-01
feature: ralph-loop-fixes
priority: P1
depends_on: []
tags: [implementation, utils]
objective:
- `loadConfig()` should return `DEFAULT_CONFIG` silently when `.ralph/config.yaml` does not exist, without logging a warning to stderr
deliverables:
- Modified `src/utils.ts``loadConfig()` function
steps:
- Open `src/utils.ts` and locate `loadConfig()`
- Add `fs.existsSync()` check before `fs.readFileSync()`
- If config file does not exist, return a deep copy of `DEFAULT_CONFIG` without any console output
- If config file exists but is malformed, fall back to defaults silently
- Remove or suppress the `console.warn()` call
tests:
- Manual: Run `/ralph resume` in a project directory with no `.ralph/` directory — should not print warning
- Manual: Run `/ralph run` in a project with `.ralph/progress.json` but no `config.yaml` — should proceed with defaults
acceptance_criteria:
- No console warning when config.yaml is missing
- `loadConfig()` returns a valid `RalphConfig` object in all cases
- Existing behavior with valid config.yaml is unchanged
validation:
- Check `src/utils.ts` loadConfig function returns silently on missing file
- Verify no `console.warn` or `console.error` in the missing-config path
notes:
- Current code at line ~145 in utils.ts: `fs.readFileSync(configPath, "utf-8")` throws ENOENT
- The try-catch does catch it but still logs the warning — the warning is noisy for normal usage where config is optional

View File

@@ -1,42 +0,0 @@
# 02. Replace `spawnPi` with `--print` mode and stdin piping
meta:
id: ralph-loop-fixes-02
feature: ralph-loop-fixes
priority: P1
depends_on: []
tags: [implementation, utils]
objective:
- Replace `spawnPi()` so it invokes `pi --print` with prompt content piped via stdin, instead of using non-existent `--no-stream` and `--prompt` flags
deliverables:
- Modified `src/utils.ts``spawnPi()` function
- Updated `src/executor.ts` — import and call site for `spawnPi`
steps:
- Open `src/utils.ts` and locate `spawnPi()`
- Replace `spawnSync` args from `["--no-stream", "--prompt", promptFile, ...]` to `["--print"]`
- Read the prompt file content and pass it as `input` to `spawnSync`
- The `input` option accepts a string that is piped to the child process stdin
- Keep `encoding`, `timeout`, and `maxBuffer` options as-is
- Update the function signature if needed (no longer needs `promptFile` path, can take prompt content directly, or read it internally)
tests:
- Manual: Spawn pi with a simple prompt — verify it returns text output and exits cleanly
- Manual: Verify `result.stdout` contains the pi response text (not NDJSON or event stream)
acceptance_criteria:
- `spawnPi()` exits with code 0 on successful execution
- `result.stdout` contains plain text response from pi
- No "Unknown options: --no-stream, --prompt" error
validation:
- Run `pi --print` with piped input manually to verify behavior
- Check spawnSync call uses `["--print"]` args and `input` option
notes:
- Pi's `--print` flag runs in non-interactive mode: reads from stdin, writes to stdout, exits
- `spawnSync` accepts an `input` option (string) that pipes to child stdin
- Current broken args: `["--no-stream", "--prompt", promptFile]`
- The `extractTextFromEvent()` function can be simplified or removed since `--print` returns plain text

View File

@@ -1,47 +0,0 @@
# 03. Replace `sendMessage` with `ctx.ui` progress API
meta:
id: ralph-loop-fixes-03
feature: ralph-loop-fixes
priority: P1
depends_on: [ralph-loop-fixes-04]
tags: [implementation, executor]
objective:
- Replace all `piApi.sendMessage({ customType: "ralph-progress", display: true })` calls with `ctx.ui.notify()` and `ctx.ui.setStatus()` to avoid TUI crash from unregistered custom message renderer
deliverables:
- Modified `src/executor.ts` — remove `sendProgressMessage()`, replace with `ctx.ui` calls
- Modified `src/executor.ts` — remove `formatToolUsage()` if no longer needed, or keep for status text
steps:
- Open `src/executor.ts`
- Remove `sendProgressMessage()` function entirely
- In `runTask()`, replace `sendProgressMessage(piApi, task, project, "starting")` with `ctx.ui.setStatus("ralph", "Running ${task.id}: ${task.title}")`
- In `runTask()` success path, replace `sendProgressMessage(..., "completed")` with `ctx.ui.notify()` for completion summary
- In `runTask()` failure path, replace `sendProgressMessage(..., "failed")` with `ctx.ui.notify()` for error
- In `executeBatch()`, replace batch start `piApi.sendMessage()` with `ctx.ui.setStatus()`
- In `executeTask()`, replace retry `piApi.sendMessage()` with `ctx.ui.notify()`
- Remove `piApi: ExtensionAPI` parameter from all executor functions (replaced by `ctx: ExtensionCommandContext`)
- Remove unused `ExtensionAPI` import from executor.ts
tests:
- Manual: Run a task and verify progress appears in the Pi UI without crash
- Manual: Verify no `child.render is not a function` error
acceptance_criteria:
- No TUI crash during task execution
- Progress messages visible to user via `ctx.ui`
- `sendProgressMessage()` function removed from codebase
- `piApi.sendMessage()` no longer called anywhere in executor
validation:
- Grep for `sendMessage` in executor.ts — should only appear in comments or not at all
- Grep for `customType.*ralph-progress` — should be removed
- Verify `ctx.ui.notify` and `ctx.ui.setStatus` are used instead
notes:
- `ctx.ui.notify(message, type)` shows a notification — use "info" for progress, "error" for failures
- `ctx.ui.setStatus(key, text)` sets footer status text — good for "Running task X" updates
- `ctx.ui.setStatus(key, undefined)` clears the status
- The TUI crash (`child.render is not a function`) happens because `customType: "ralph-progress"` has no registered renderer via `pi.registerMessageRenderer()`

View File

@@ -1,47 +0,0 @@
# 04. Thread `ExtensionCommandContext` through `executeBatch`
meta:
id: ralph-loop-fixes-04
feature: ralph-loop-fixes
priority: P1
depends_on: []
tags: [implementation, plumbing]
objective:
- Pass `ctx: ExtensionCommandContext` from command handlers through to all executor functions that need it, replacing the missing `piApi: ExtensionAPI` parameter
deliverables:
- Modified `index.ts` — all `executeBatch()` calls pass `ctx` as 6th parameter
- Modified `src/executor.ts``executeBatch()`, `executeTask()`, `runTask()`, `executeBatchParallel()` accept `ctx: ExtensionCommandContext`
steps:
- Open `src/executor.ts`
- Add `import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent"`
- Update `executeBatch()` signature: add `ctx: ExtensionCommandContext` as 6th parameter (after `progress`)
- Update `executeTask()` signature: add `ctx: ExtensionCommandContext` parameter
- Update `runTask()` signature: add `ctx: ExtensionCommandContext` parameter
- Update `executeBatchParallel()` signature: add `ctx: ExtensionCommandContext` parameter
- Thread `ctx` through all internal calls (batch → task → run)
- Open `index.ts`
- In `handleRun()`: pass `ctx` to `executeBatch()`
- In `handleResume()`: pass `ctx` to `executeBatch()`
- In `handleNext()`: pass `ctx` to `executeBatch()`
tests:
- Manual: `/ralph run` should execute without "undefined is not a function" errors
- Manual: `/ralph resume` should execute without context-related errors
acceptance_criteria:
- `executeBatch()` receives a valid `ExtensionCommandContext` in all call paths
- No `undefined` access errors when executor calls `ctx.ui.*`
- TypeScript compiles without errors
validation:
- Run `npx tsc --noEmit` in extension directory
- Verify `ctx` parameter exists in all executor function signatures
- Verify all call sites in index.ts pass `ctx`
notes:
- `ExtensionCommandContext` extends `ExtensionContext` and adds session control methods
- Command handlers receive `ExtensionCommandContext`, not bare `ExtensionContext`
- The `piApi` parameter was `ExtensionAPI` which has `sendMessage()` — we're replacing it with `ctx` which has `ctx.ui` for UI access

View File

@@ -1,39 +0,0 @@
# 05. Fix sequential mode batch labels
meta:
id: ralph-loop-fixes-05
feature: ralph-loop-fixes
priority: P2
depends_on: [ralph-loop-fixes-04]
tags: [implementation, ui]
objective:
- Suppress "Batch N:" label for single-task batches; use numbered list format (1., 2., 3.) for sequential task execution to match original behavior
deliverables:
- Modified `src/executor.ts``executeBatch()` console output
steps:
- Open `src/executor.ts` and locate `executeBatch()`
- In the batch header log, check if `tasks.length === 1`
- If single task: log `[ralph] Running task ${task.id}: ${task.title}` (no "Batch N" wrapper)
- If multiple tasks: keep existing `=== Batch N (M tasks) ===` format
- Track global task counter for sequential numbered output if needed
tests:
- Manual: Run a single-task batch — verify no "Batch N" in output
- Manual: Run a multi-task batch — verify "Batch N" still appears
acceptance_criteria:
- Single-task batches do not show "Batch N:" prefix
- Multi-task batches still show batch header
- Output format matches original: `[ralph] Running task 001: Title`
validation:
- Check `console.log` output in executeBatch for conditional formatting
- Verify single-task path uses task-focused label
notes:
- Original behavior: single tasks show numbered list (1., 2., 3.), batches show "Batch N:"
- Current code always shows `[ralph] === Batch N (M tasks) ===` regardless of batch size
- This is cosmetic but matches user preference for compact UI

View File

@@ -1,40 +0,0 @@
# 06. Simplify `parseToolUsage` for plain text output
meta:
id: ralph-loop-fixes-06
feature: ralph-loop-fixes
priority: P2
depends_on: [ralph-loop-fixes-02]
tags: [implementation, utils]
objective:
- Remove NDJSON event parsing from `parseToolUsage()` since `pi --print` returns plain text, not structured event streams
deliverables:
- Modified `src/utils.ts``parseToolUsage()` function
steps:
- Open `src/utils.ts` and locate `parseToolUsage()`
- Remove the NDJSON parsing block (lines that check `line.startsWith("data: ")` and `JSON.parse`)
- Keep only the regex fallback that counts tool mentions in plain text output
- Remove `extractTextFromEvent()` if no longer needed (plain text from `--print` needs no extraction)
- Update `executor.ts` to call `parseToolUsage()` directly on `result.stdout` without `extractTextFromEvent()`
tests:
- Manual: Run a task that uses multiple tools — verify tool counts are captured from plain text output
- Manual: Verify no JSON parse errors in tool usage parsing
acceptance_criteria:
- `parseToolUsage()` works correctly on plain text output
- No JSON parsing logic remains in `parseToolUsage()`
- Tool counts ([read], [write], [edit], [bash]) are still extracted via regex
validation:
- Grep for `JSON.parse` in parseToolUsage — should be removed
- Grep for `data:` prefix check — should be removed
- Verify regex-based tool counting still present and functional
notes:
- `pi --print` returns plain text, not NDJSON event stream
- The regex fallback patterns (`\[read\]`, `read(`, etc.) are sufficient for counting tool mentions
- `extractTextFromEvent()` was only needed for NDJSON — can be removed or simplified to identity function

View File

@@ -1,26 +0,0 @@
# Ralph-Loop Extension Fixes
Objective: Fix critical bugs preventing `/ralph resume` from working — broken CLI flags, unthreaded context, missing config, and TUI crash.
Status legend: [ ] todo, [~] in-progress, [x] done
Tasks
- [x] 01 — Fix `loadConfig` to return defaults gracefully when `.ralph/config.yaml` is missing → `01-fix-loadconfig-graceful-default.md`
- [x] 02 — Replace `spawnPi` with `--print` mode and stdin piping → `02-fix-spawnpi-print-mode.md`
- [x] 03 — Replace `sendMessage` with `ctx.ui` progress API → `03-replace-sendmessage-with-ctx-ui.md`
- [x] 04 — Thread `ExtensionCommandContext` through `executeBatch``04-thread-ctx-through-execute-batch.md`
- [x] 05 — Fix sequential mode batch labels → `05-fix-sequential-mode-labels.md`
- [x] 06 — Simplify `parseToolUsage` for plain text output → `06-simplify-parsertoolsusage.md`
Dependencies
- 02 depends on nothing (standalone utils fix)
- 03 depends on 04 (needs ctx available in executor)
- 04 depends on nothing (standalone plumbing fix)
- 05 depends on 04 (executor changes)
- 06 depends on 02 (output format changes from --print)
Exit criteria
- `/ralph resume` runs without errors in a project with no `.ralph/config.yaml`
- Pi subprocess spawns successfully with `--print` mode
- Progress messages display via `ctx.ui` without TUI crash
- All batch execution paths receive context parameter