drop tasks, drop thrashing bandaid
This commit is contained in:
62
AGENTS.md
Normal file
62
AGENTS.md
Normal 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.
|
||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
27
package.json
27
package.json
@@ -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
160
prompts/task-manager.md
Normal 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 (Arrange–Act–Assert)
|
||||||
|
- 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: $@
|
||||||
@@ -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) {
|
||||||
|
|||||||
27
src/utils.ts
27
src/utils.ts
@@ -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
92
src/widget-batcher.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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()`
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user