pi-extensions 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ralph/import-cc-codex.md +31 -0
- package/.ralph/import-cc-codex.state.json +14 -0
- package/.ralph/mario-not-impl.md +69 -0
- package/.ralph/mario-not-impl.state.json +14 -0
- package/.ralph/mario-not-spec.md +163 -0
- package/.ralph/mario-not-spec.state.json +14 -0
- package/LICENSE +21 -0
- package/README.md +65 -0
- package/RELEASING.md +34 -0
- package/agent-guidance/CHANGELOG.md +4 -0
- package/agent-guidance/README.md +102 -0
- package/agent-guidance/agent-guidance.ts +147 -0
- package/agent-guidance/package.json +22 -0
- package/agent-guidance/setup.sh +75 -0
- package/agent-guidance/templates/CLAUDE.md +5 -0
- package/agent-guidance/templates/CODEX.md +92 -0
- package/agent-guidance/templates/GEMINI.md +5 -0
- package/arcade/CHANGELOG.md +4 -0
- package/arcade/README.md +85 -0
- package/arcade/assets/picman.png +0 -0
- package/arcade/assets/ping.png +0 -0
- package/arcade/assets/spice-invaders.png +0 -0
- package/arcade/assets/tetris.png +0 -0
- package/arcade/mario-not/README.md +30 -0
- package/arcade/mario-not/boss.js +103 -0
- package/arcade/mario-not/camera.js +59 -0
- package/arcade/mario-not/collision.js +91 -0
- package/arcade/mario-not/colors.js +36 -0
- package/arcade/mario-not/constants.js +97 -0
- package/arcade/mario-not/core.js +39 -0
- package/arcade/mario-not/death.js +77 -0
- package/arcade/mario-not/effects.js +84 -0
- package/arcade/mario-not/enemies.js +31 -0
- package/arcade/mario-not/engine.js +171 -0
- package/arcade/mario-not/fireballs.js +98 -0
- package/arcade/mario-not/items.js +24 -0
- package/arcade/mario-not/levels.js +403 -0
- package/arcade/mario-not/logic.js +104 -0
- package/arcade/mario-not/mario-not.ts +297 -0
- package/arcade/mario-not/player.js +244 -0
- package/arcade/mario-not/render.js +257 -0
- package/arcade/mario-not/spec.md +548 -0
- package/arcade/mario-not/state.js +246 -0
- package/arcade/mario-not/tests/e2e.test.js +855 -0
- package/arcade/mario-not/tests/engine.test.js +888 -0
- package/arcade/mario-not/tests/fixtures/story0-frame.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story1-camera.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story1-glyphs.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story10-item.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story11-hazards.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story12-used-block.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story13-pipes.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story14-goal.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story15-hud-narrow.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story16-unknown-tile.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story17-mix.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story18-hud-score.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story19-cue.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story2-enemy.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story20-camera-offset.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story21-hud-zero.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story22-big-viewport.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story23-camera-negative.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story24-camera-width.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story25-camera-positive.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story26-hud-lives.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story27-hud-coins.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story28-item-viewport.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story29-enemy-viewport.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story3-hud.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story30-hud-score.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story31-particles-viewport.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story32-paused-frame.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story4-big.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story5-resume-hud.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story6-particles.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story6-paused.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story7-powerup.txt +4 -0
- package/arcade/mario-not/tests/fixtures/story8-hud-time.txt +2 -0
- package/arcade/mario-not/tests/fixtures/story9-hud-level.txt +2 -0
- package/arcade/mario-not/tiles.js +79 -0
- package/arcade/mario-not/tsconfig.json +14 -0
- package/arcade/mario-not/types.js +225 -0
- package/arcade/package.json +26 -0
- package/arcade/picman.ts +328 -0
- package/arcade/ping.ts +594 -0
- package/arcade/spice-invaders.ts +1104 -0
- package/arcade/tetris.ts +662 -0
- package/code-actions/CHANGELOG.md +4 -0
- package/code-actions/README.md +65 -0
- package/code-actions/actions.ts +107 -0
- package/code-actions/index.ts +148 -0
- package/code-actions/package.json +22 -0
- package/code-actions/search.ts +79 -0
- package/code-actions/snippets.ts +179 -0
- package/code-actions/ui.ts +120 -0
- package/files-widget/CHANGELOG.md +90 -0
- package/files-widget/DESIGN.md +452 -0
- package/files-widget/README.md +122 -0
- package/files-widget/TODO.md +141 -0
- package/files-widget/browser.ts +922 -0
- package/files-widget/comment.ts +5 -0
- package/files-widget/constants.ts +18 -0
- package/files-widget/demo.svg +1 -0
- package/files-widget/file-tree.ts +224 -0
- package/files-widget/file-viewer.ts +93 -0
- package/files-widget/git.ts +107 -0
- package/files-widget/index.ts +140 -0
- package/files-widget/input-utils.ts +3 -0
- package/files-widget/package.json +22 -0
- package/files-widget/types.ts +28 -0
- package/files-widget/utils.ts +26 -0
- package/files-widget/viewer.ts +424 -0
- package/import-cc-codex/research/import-chats-from-other-agents.md +135 -0
- package/import-cc-codex/spec.md +79 -0
- package/package.json +29 -0
- package/ralph-wiggum/CHANGELOG.md +7 -0
- package/ralph-wiggum/README.md +96 -0
- package/ralph-wiggum/SKILL.md +73 -0
- package/ralph-wiggum/index.ts +792 -0
- package/ralph-wiggum/package.json +25 -0
- package/raw-paste/CHANGELOG.md +7 -0
- package/raw-paste/README.md +52 -0
- package/raw-paste/index.ts +112 -0
- package/raw-paste/package.json +22 -0
- package/tab-status/CHANGELOG.md +4 -0
- package/tab-status/README.md +61 -0
- package/tab-status/assets/tab-status.png +0 -0
- package/tab-status/package.json +22 -0
- package/tab-status/tab-status.ts +179 -0
- package/usage-extension/CHANGELOG.md +17 -0
- package/usage-extension/README.md +120 -0
- package/usage-extension/index.ts +628 -0
- package/usage-extension/package.json +22 -0
- package/usage-extension/screenshot.png +0 -0
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ralph Wiggum - Long-running agent loops for iterative development.
|
|
3
|
+
* Port of Geoffrey Huntley's approach.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import { Type } from "@sinclair/typebox";
|
|
10
|
+
|
|
11
|
+
const RALPH_DIR = ".ralph";
|
|
12
|
+
const COMPLETE_MARKER = "<promise>COMPLETE</promise>";
|
|
13
|
+
|
|
14
|
+
const DEFAULT_TEMPLATE = `# Task
|
|
15
|
+
|
|
16
|
+
Describe your task here.
|
|
17
|
+
|
|
18
|
+
## Goals
|
|
19
|
+
- Goal 1
|
|
20
|
+
- Goal 2
|
|
21
|
+
|
|
22
|
+
## Checklist
|
|
23
|
+
- [ ] Item 1
|
|
24
|
+
- [ ] Item 2
|
|
25
|
+
|
|
26
|
+
## Notes
|
|
27
|
+
(Update this as you work)
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
const DEFAULT_REFLECT_INSTRUCTIONS = `REFLECTION CHECKPOINT
|
|
31
|
+
|
|
32
|
+
Pause and reflect on your progress:
|
|
33
|
+
1. What has been accomplished so far?
|
|
34
|
+
2. What's working well?
|
|
35
|
+
3. What's not working or blocking progress?
|
|
36
|
+
4. Should the approach be adjusted?
|
|
37
|
+
5. What are the next priorities?
|
|
38
|
+
|
|
39
|
+
Update the task file with your reflection, then continue working.`;
|
|
40
|
+
|
|
41
|
+
type LoopStatus = "active" | "paused" | "completed";
|
|
42
|
+
|
|
43
|
+
interface LoopState {
|
|
44
|
+
name: string;
|
|
45
|
+
taskFile: string;
|
|
46
|
+
iteration: number;
|
|
47
|
+
maxIterations: number;
|
|
48
|
+
itemsPerIteration: number; // Prompt hint only - "process N items per turn"
|
|
49
|
+
reflectEvery: number; // Reflect every N iterations
|
|
50
|
+
reflectInstructions: string;
|
|
51
|
+
active: boolean; // Backwards compat
|
|
52
|
+
status: LoopStatus;
|
|
53
|
+
startedAt: string;
|
|
54
|
+
completedAt?: string;
|
|
55
|
+
lastReflectionAt: number; // Last iteration we reflected at
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const STATUS_ICONS: Record<LoopStatus, string> = { active: "▶", paused: "⏸", completed: "✓" };
|
|
59
|
+
|
|
60
|
+
export default function (pi: ExtensionAPI) {
|
|
61
|
+
let currentLoop: string | null = null;
|
|
62
|
+
|
|
63
|
+
// --- File helpers ---
|
|
64
|
+
|
|
65
|
+
const ralphDir = (ctx: ExtensionContext) => path.resolve(ctx.cwd, RALPH_DIR);
|
|
66
|
+
const archiveDir = (ctx: ExtensionContext) => path.join(ralphDir(ctx), "archive");
|
|
67
|
+
const sanitize = (name: string) => name.replace(/[^a-zA-Z0-9_-]/g, "_").replace(/_+/g, "_");
|
|
68
|
+
|
|
69
|
+
function getPath(ctx: ExtensionContext, name: string, ext: string, archived = false): string {
|
|
70
|
+
const dir = archived ? archiveDir(ctx) : ralphDir(ctx);
|
|
71
|
+
return path.join(dir, `${sanitize(name)}${ext}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function ensureDir(filePath: string): void {
|
|
75
|
+
const dir = path.dirname(filePath);
|
|
76
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function tryDelete(filePath: string): void {
|
|
80
|
+
try {
|
|
81
|
+
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
82
|
+
} catch {
|
|
83
|
+
/* ignore */
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function tryRead(filePath: string): string | null {
|
|
88
|
+
try {
|
|
89
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function tryRemoveDir(dirPath: string): boolean {
|
|
96
|
+
try {
|
|
97
|
+
if (fs.existsSync(dirPath)) {
|
|
98
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// --- State management ---
|
|
107
|
+
|
|
108
|
+
function migrateState(raw: Partial<LoopState> & { name: string }): LoopState {
|
|
109
|
+
if (!raw.status) raw.status = raw.active ? "active" : "paused";
|
|
110
|
+
raw.active = raw.status === "active";
|
|
111
|
+
// Migrate old field names
|
|
112
|
+
if ("reflectEveryItems" in raw && !raw.reflectEvery) {
|
|
113
|
+
raw.reflectEvery = (raw as any).reflectEveryItems;
|
|
114
|
+
}
|
|
115
|
+
if ("lastReflectionAtItems" in raw && raw.lastReflectionAt === undefined) {
|
|
116
|
+
raw.lastReflectionAt = (raw as any).lastReflectionAtItems;
|
|
117
|
+
}
|
|
118
|
+
return raw as LoopState;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function loadState(ctx: ExtensionContext, name: string, archived = false): LoopState | null {
|
|
122
|
+
const content = tryRead(getPath(ctx, name, ".state.json", archived));
|
|
123
|
+
return content ? migrateState(JSON.parse(content)) : null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function saveState(ctx: ExtensionContext, state: LoopState, archived = false): void {
|
|
127
|
+
state.active = state.status === "active";
|
|
128
|
+
const filePath = getPath(ctx, state.name, ".state.json", archived);
|
|
129
|
+
ensureDir(filePath);
|
|
130
|
+
fs.writeFileSync(filePath, JSON.stringify(state, null, 2), "utf-8");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function listLoops(ctx: ExtensionContext, archived = false): LoopState[] {
|
|
134
|
+
const dir = archived ? archiveDir(ctx) : ralphDir(ctx);
|
|
135
|
+
if (!fs.existsSync(dir)) return [];
|
|
136
|
+
return fs
|
|
137
|
+
.readdirSync(dir)
|
|
138
|
+
.filter((f) => f.endsWith(".state.json"))
|
|
139
|
+
.map((f) => {
|
|
140
|
+
const content = tryRead(path.join(dir, f));
|
|
141
|
+
return content ? migrateState(JSON.parse(content)) : null;
|
|
142
|
+
})
|
|
143
|
+
.filter((s): s is LoopState => s !== null);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// --- Loop state transitions ---
|
|
147
|
+
|
|
148
|
+
function pauseLoop(ctx: ExtensionContext, state: LoopState, message?: string): void {
|
|
149
|
+
state.status = "paused";
|
|
150
|
+
state.active = false;
|
|
151
|
+
saveState(ctx, state);
|
|
152
|
+
currentLoop = null;
|
|
153
|
+
updateUI(ctx);
|
|
154
|
+
if (message && ctx.hasUI) ctx.ui.notify(message, "info");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function completeLoop(ctx: ExtensionContext, state: LoopState, banner: string): void {
|
|
158
|
+
state.status = "completed";
|
|
159
|
+
state.completedAt = new Date().toISOString();
|
|
160
|
+
state.active = false;
|
|
161
|
+
saveState(ctx, state);
|
|
162
|
+
currentLoop = null;
|
|
163
|
+
updateUI(ctx);
|
|
164
|
+
pi.sendUserMessage(banner);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function stopLoop(ctx: ExtensionContext, state: LoopState, message?: string): void {
|
|
168
|
+
state.status = "completed";
|
|
169
|
+
state.completedAt = new Date().toISOString();
|
|
170
|
+
state.active = false;
|
|
171
|
+
saveState(ctx, state);
|
|
172
|
+
currentLoop = null;
|
|
173
|
+
updateUI(ctx);
|
|
174
|
+
if (message && ctx.hasUI) ctx.ui.notify(message, "info");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// --- UI ---
|
|
178
|
+
|
|
179
|
+
function formatLoop(l: LoopState): string {
|
|
180
|
+
const status = `${STATUS_ICONS[l.status]} ${l.status}`;
|
|
181
|
+
const iter = l.maxIterations > 0 ? `${l.iteration}/${l.maxIterations}` : `${l.iteration}`;
|
|
182
|
+
return `${l.name}: ${status} (iteration ${iter})`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function updateUI(ctx: ExtensionContext): void {
|
|
186
|
+
if (!ctx.hasUI) return;
|
|
187
|
+
|
|
188
|
+
const state = currentLoop ? loadState(ctx, currentLoop) : null;
|
|
189
|
+
if (!state) {
|
|
190
|
+
ctx.ui.setStatus("ralph", undefined);
|
|
191
|
+
ctx.ui.setWidget("ralph", undefined);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const { theme } = ctx.ui;
|
|
196
|
+
const maxStr = state.maxIterations > 0 ? `/${state.maxIterations}` : "";
|
|
197
|
+
|
|
198
|
+
ctx.ui.setStatus("ralph", theme.fg("accent", `🔄 ${state.name} (${state.iteration}${maxStr})`));
|
|
199
|
+
|
|
200
|
+
const lines = [
|
|
201
|
+
theme.fg("accent", theme.bold("Ralph Wiggum")),
|
|
202
|
+
theme.fg("muted", `Loop: ${state.name}`),
|
|
203
|
+
theme.fg("dim", `Status: ${STATUS_ICONS[state.status]} ${state.status}`),
|
|
204
|
+
theme.fg("dim", `Iteration: ${state.iteration}${maxStr}`),
|
|
205
|
+
theme.fg("dim", `Task: ${state.taskFile}`),
|
|
206
|
+
];
|
|
207
|
+
if (state.reflectEvery > 0) {
|
|
208
|
+
const next = state.reflectEvery - ((state.iteration - 1) % state.reflectEvery);
|
|
209
|
+
lines.push(theme.fg("dim", `Next reflection in: ${next} iterations`));
|
|
210
|
+
}
|
|
211
|
+
// Warning about stopping
|
|
212
|
+
lines.push("");
|
|
213
|
+
lines.push(theme.fg("warning", "ESC pauses the assistant"));
|
|
214
|
+
lines.push(theme.fg("warning", "Send a message to resume; /ralph-stop ends the loop"));
|
|
215
|
+
ctx.ui.setWidget("ralph", lines);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// --- Prompt building ---
|
|
219
|
+
|
|
220
|
+
function buildPrompt(state: LoopState, taskContent: string, isReflection: boolean): string {
|
|
221
|
+
const maxStr = state.maxIterations > 0 ? `/${state.maxIterations}` : "";
|
|
222
|
+
const header = `───────────────────────────────────────────────────────────────────────
|
|
223
|
+
🔄 RALPH LOOP: ${state.name} | Iteration ${state.iteration}${maxStr}${isReflection ? " | 🪞 REFLECTION" : ""}
|
|
224
|
+
───────────────────────────────────────────────────────────────────────`;
|
|
225
|
+
|
|
226
|
+
const parts = [header, ""];
|
|
227
|
+
if (isReflection) parts.push(state.reflectInstructions, "\n---\n");
|
|
228
|
+
|
|
229
|
+
parts.push(`## Current Task (from ${state.taskFile})\n\n${taskContent}\n\n---`);
|
|
230
|
+
parts.push(`\n## Instructions\n`);
|
|
231
|
+
parts.push("User controls: ESC pauses the assistant. Send a message to resume. Run /ralph-stop when idle to stop the loop.\n");
|
|
232
|
+
parts.push(
|
|
233
|
+
`You are in a Ralph loop (iteration ${state.iteration}${state.maxIterations > 0 ? ` of ${state.maxIterations}` : ""}).\n`,
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
if (state.itemsPerIteration > 0) {
|
|
237
|
+
parts.push(`**THIS ITERATION: Process approximately ${state.itemsPerIteration} items, then call ralph_done.**\n`);
|
|
238
|
+
parts.push(`1. Work on the next ~${state.itemsPerIteration} items from your checklist`);
|
|
239
|
+
} else {
|
|
240
|
+
parts.push(`1. Continue working on the task`);
|
|
241
|
+
}
|
|
242
|
+
parts.push(`2. Update the task file (${state.taskFile}) with your progress`);
|
|
243
|
+
parts.push(`3. When FULLY COMPLETE, respond with: ${COMPLETE_MARKER}`);
|
|
244
|
+
parts.push(`4. Otherwise, call the ralph_done tool to proceed to next iteration`);
|
|
245
|
+
|
|
246
|
+
return parts.join("\n");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// --- Arg parsing ---
|
|
250
|
+
|
|
251
|
+
function parseArgs(argsStr: string) {
|
|
252
|
+
const tokens = argsStr.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
|
|
253
|
+
const result = {
|
|
254
|
+
name: "",
|
|
255
|
+
maxIterations: 50,
|
|
256
|
+
itemsPerIteration: 0,
|
|
257
|
+
reflectEvery: 0,
|
|
258
|
+
reflectInstructions: DEFAULT_REFLECT_INSTRUCTIONS,
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
262
|
+
const tok = tokens[i];
|
|
263
|
+
const next = tokens[i + 1];
|
|
264
|
+
if (tok === "--max-iterations" && next) {
|
|
265
|
+
result.maxIterations = parseInt(next, 10) || 0;
|
|
266
|
+
i++;
|
|
267
|
+
} else if (tok === "--items-per-iteration" && next) {
|
|
268
|
+
result.itemsPerIteration = parseInt(next, 10) || 0;
|
|
269
|
+
i++;
|
|
270
|
+
} else if (tok === "--reflect-every" && next) {
|
|
271
|
+
result.reflectEvery = parseInt(next, 10) || 0;
|
|
272
|
+
i++;
|
|
273
|
+
} else if (tok === "--reflect-instructions" && next) {
|
|
274
|
+
result.reflectInstructions = next.replace(/^"|"$/g, "");
|
|
275
|
+
i++;
|
|
276
|
+
} else if (!tok.startsWith("--")) {
|
|
277
|
+
result.name = tok;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// --- Commands ---
|
|
284
|
+
|
|
285
|
+
const commands: Record<string, (rest: string, ctx: ExtensionContext) => void> = {
|
|
286
|
+
start(rest, ctx) {
|
|
287
|
+
const args = parseArgs(rest);
|
|
288
|
+
if (!args.name) {
|
|
289
|
+
ctx.ui.notify(
|
|
290
|
+
"Usage: /ralph start <name|path> [--items-per-iteration N] [--reflect-every N] [--max-iterations N]",
|
|
291
|
+
"warning",
|
|
292
|
+
);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const isPath = args.name.includes("/") || args.name.includes("\\");
|
|
297
|
+
const loopName = isPath ? sanitize(path.basename(args.name, path.extname(args.name))) : args.name;
|
|
298
|
+
const taskFile = isPath ? args.name : path.join(RALPH_DIR, `${loopName}.md`);
|
|
299
|
+
|
|
300
|
+
const existing = loadState(ctx, loopName);
|
|
301
|
+
if (existing?.status === "active") {
|
|
302
|
+
ctx.ui.notify(`Loop "${loopName}" is already active. Use /ralph resume ${loopName}`, "warning");
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const fullPath = path.resolve(ctx.cwd, taskFile);
|
|
307
|
+
if (!fs.existsSync(fullPath)) {
|
|
308
|
+
ensureDir(fullPath);
|
|
309
|
+
fs.writeFileSync(fullPath, DEFAULT_TEMPLATE, "utf-8");
|
|
310
|
+
ctx.ui.notify(`Created task file: ${taskFile}`, "info");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const state: LoopState = {
|
|
314
|
+
name: loopName,
|
|
315
|
+
taskFile,
|
|
316
|
+
iteration: 1,
|
|
317
|
+
maxIterations: args.maxIterations,
|
|
318
|
+
itemsPerIteration: args.itemsPerIteration,
|
|
319
|
+
reflectEvery: args.reflectEvery,
|
|
320
|
+
reflectInstructions: args.reflectInstructions,
|
|
321
|
+
active: true,
|
|
322
|
+
status: "active",
|
|
323
|
+
startedAt: existing?.startedAt || new Date().toISOString(),
|
|
324
|
+
lastReflectionAt: 0,
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
saveState(ctx, state);
|
|
328
|
+
currentLoop = loopName;
|
|
329
|
+
updateUI(ctx);
|
|
330
|
+
|
|
331
|
+
const content = tryRead(fullPath);
|
|
332
|
+
if (!content) {
|
|
333
|
+
ctx.ui.notify(`Could not read task file: ${taskFile}`, "error");
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
pi.sendUserMessage(buildPrompt(state, content, false));
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
stop(_rest, ctx) {
|
|
340
|
+
if (!currentLoop) {
|
|
341
|
+
// Check persisted state for any active loop
|
|
342
|
+
const active = listLoops(ctx).find((l) => l.status === "active");
|
|
343
|
+
if (active) {
|
|
344
|
+
pauseLoop(ctx, active, `Paused Ralph loop: ${active.name} (iteration ${active.iteration})`);
|
|
345
|
+
} else {
|
|
346
|
+
ctx.ui.notify("No active Ralph loop", "warning");
|
|
347
|
+
}
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const state = loadState(ctx, currentLoop);
|
|
351
|
+
if (state) {
|
|
352
|
+
pauseLoop(ctx, state, `Paused Ralph loop: ${currentLoop} (iteration ${state.iteration})`);
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
resume(rest, ctx) {
|
|
357
|
+
const loopName = rest.trim();
|
|
358
|
+
if (!loopName) {
|
|
359
|
+
ctx.ui.notify("Usage: /ralph resume <name>", "warning");
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const state = loadState(ctx, loopName);
|
|
364
|
+
if (!state) {
|
|
365
|
+
ctx.ui.notify(`Loop "${loopName}" not found`, "error");
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (state.status === "completed") {
|
|
369
|
+
ctx.ui.notify(`Loop "${loopName}" is completed. Use /ralph start ${loopName} to restart`, "warning");
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Pause current loop if different
|
|
374
|
+
if (currentLoop && currentLoop !== loopName) {
|
|
375
|
+
const curr = loadState(ctx, currentLoop);
|
|
376
|
+
if (curr) pauseLoop(ctx, curr);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
state.status = "active";
|
|
380
|
+
state.active = true;
|
|
381
|
+
state.iteration++;
|
|
382
|
+
saveState(ctx, state);
|
|
383
|
+
currentLoop = loopName;
|
|
384
|
+
updateUI(ctx);
|
|
385
|
+
|
|
386
|
+
ctx.ui.notify(`Resumed: ${loopName} (iteration ${state.iteration})`, "info");
|
|
387
|
+
|
|
388
|
+
const content = tryRead(path.resolve(ctx.cwd, state.taskFile));
|
|
389
|
+
if (!content) {
|
|
390
|
+
ctx.ui.notify(`Could not read task file: ${state.taskFile}`, "error");
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const needsReflection =
|
|
395
|
+
state.reflectEvery > 0 && state.iteration > 1 && (state.iteration - 1) % state.reflectEvery === 0;
|
|
396
|
+
pi.sendUserMessage(buildPrompt(state, content, needsReflection));
|
|
397
|
+
},
|
|
398
|
+
|
|
399
|
+
status(_rest, ctx) {
|
|
400
|
+
const loops = listLoops(ctx);
|
|
401
|
+
if (loops.length === 0) {
|
|
402
|
+
ctx.ui.notify("No Ralph loops found.", "info");
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
ctx.ui.notify(`Ralph loops:\n${loops.map((l) => formatLoop(l)).join("\n")}`, "info");
|
|
406
|
+
},
|
|
407
|
+
|
|
408
|
+
cancel(rest, ctx) {
|
|
409
|
+
const loopName = rest.trim();
|
|
410
|
+
if (!loopName) {
|
|
411
|
+
ctx.ui.notify("Usage: /ralph cancel <name>", "warning");
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (!loadState(ctx, loopName)) {
|
|
415
|
+
ctx.ui.notify(`Loop "${loopName}" not found`, "error");
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (currentLoop === loopName) currentLoop = null;
|
|
419
|
+
tryDelete(getPath(ctx, loopName, ".state.json"));
|
|
420
|
+
ctx.ui.notify(`Cancelled: ${loopName}`, "info");
|
|
421
|
+
updateUI(ctx);
|
|
422
|
+
},
|
|
423
|
+
|
|
424
|
+
archive(rest, ctx) {
|
|
425
|
+
const loopName = rest.trim();
|
|
426
|
+
if (!loopName) {
|
|
427
|
+
ctx.ui.notify("Usage: /ralph archive <name>", "warning");
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const state = loadState(ctx, loopName);
|
|
431
|
+
if (!state) {
|
|
432
|
+
ctx.ui.notify(`Loop "${loopName}" not found`, "error");
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (state.status === "active") {
|
|
436
|
+
ctx.ui.notify("Cannot archive active loop. Stop it first.", "warning");
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (currentLoop === loopName) currentLoop = null;
|
|
441
|
+
|
|
442
|
+
const srcState = getPath(ctx, loopName, ".state.json");
|
|
443
|
+
const dstState = getPath(ctx, loopName, ".state.json", true);
|
|
444
|
+
ensureDir(dstState);
|
|
445
|
+
if (fs.existsSync(srcState)) fs.renameSync(srcState, dstState);
|
|
446
|
+
|
|
447
|
+
const srcTask = path.resolve(ctx.cwd, state.taskFile);
|
|
448
|
+
if (srcTask.startsWith(ralphDir(ctx)) && !srcTask.startsWith(archiveDir(ctx))) {
|
|
449
|
+
const dstTask = getPath(ctx, loopName, ".md", true);
|
|
450
|
+
if (fs.existsSync(srcTask)) fs.renameSync(srcTask, dstTask);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
ctx.ui.notify(`Archived: ${loopName}`, "info");
|
|
454
|
+
updateUI(ctx);
|
|
455
|
+
},
|
|
456
|
+
|
|
457
|
+
clean(rest, ctx) {
|
|
458
|
+
const all = rest.trim() === "--all";
|
|
459
|
+
const completed = listLoops(ctx).filter((l) => l.status === "completed");
|
|
460
|
+
|
|
461
|
+
if (completed.length === 0) {
|
|
462
|
+
ctx.ui.notify("No completed loops to clean", "info");
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
for (const loop of completed) {
|
|
467
|
+
tryDelete(getPath(ctx, loop.name, ".state.json"));
|
|
468
|
+
if (all) tryDelete(getPath(ctx, loop.name, ".md"));
|
|
469
|
+
if (currentLoop === loop.name) currentLoop = null;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const suffix = all ? " (all files)" : " (state only)";
|
|
473
|
+
ctx.ui.notify(
|
|
474
|
+
`Cleaned ${completed.length} loop(s)${suffix}:\n${completed.map((l) => ` • ${l.name}`).join("\n")}`,
|
|
475
|
+
"info",
|
|
476
|
+
);
|
|
477
|
+
updateUI(ctx);
|
|
478
|
+
},
|
|
479
|
+
|
|
480
|
+
list(rest, ctx) {
|
|
481
|
+
const archived = rest.trim() === "--archived";
|
|
482
|
+
const loops = listLoops(ctx, archived);
|
|
483
|
+
|
|
484
|
+
if (loops.length === 0) {
|
|
485
|
+
ctx.ui.notify(
|
|
486
|
+
archived ? "No archived loops" : "No loops found. Use /ralph list --archived for archived.",
|
|
487
|
+
"info",
|
|
488
|
+
);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const label = archived ? "Archived loops" : "Ralph loops";
|
|
493
|
+
ctx.ui.notify(`${label}:\n${loops.map((l) => formatLoop(l)).join("\n")}`, "info");
|
|
494
|
+
},
|
|
495
|
+
|
|
496
|
+
nuke(rest, ctx) {
|
|
497
|
+
const force = rest.trim() === "--yes";
|
|
498
|
+
const warning =
|
|
499
|
+
"This deletes all .ralph state, task, and archive files. External task files are not removed.";
|
|
500
|
+
|
|
501
|
+
const run = () => {
|
|
502
|
+
const dir = ralphDir(ctx);
|
|
503
|
+
if (!fs.existsSync(dir)) {
|
|
504
|
+
if (ctx.hasUI) ctx.ui.notify("No .ralph directory found.", "info");
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
currentLoop = null;
|
|
509
|
+
const ok = tryRemoveDir(dir);
|
|
510
|
+
if (ctx.hasUI) {
|
|
511
|
+
ctx.ui.notify(ok ? "Removed .ralph directory." : "Failed to remove .ralph directory.", ok ? "info" : "error");
|
|
512
|
+
}
|
|
513
|
+
updateUI(ctx);
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
if (!force) {
|
|
517
|
+
if (ctx.hasUI) {
|
|
518
|
+
void ctx.ui.confirm("Delete all Ralph loop files?", warning).then((confirmed) => {
|
|
519
|
+
if (confirmed) run();
|
|
520
|
+
});
|
|
521
|
+
} else {
|
|
522
|
+
ctx.ui.notify(`Run /ralph nuke --yes to confirm. ${warning}`, "warning");
|
|
523
|
+
}
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (ctx.hasUI) ctx.ui.notify(warning, "warning");
|
|
528
|
+
run();
|
|
529
|
+
},
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
const HELP = `Ralph Wiggum - Long-running development loops
|
|
533
|
+
|
|
534
|
+
Commands:
|
|
535
|
+
/ralph start <name|path> [options] Start a new loop
|
|
536
|
+
/ralph stop Pause current loop
|
|
537
|
+
/ralph resume <name> Resume a paused loop
|
|
538
|
+
/ralph status Show all loops
|
|
539
|
+
/ralph cancel <name> Delete loop state
|
|
540
|
+
/ralph archive <name> Move loop to archive
|
|
541
|
+
/ralph clean [--all] Clean completed loops
|
|
542
|
+
/ralph list --archived Show archived loops
|
|
543
|
+
/ralph nuke [--yes] Delete all .ralph data
|
|
544
|
+
/ralph-stop Stop active loop (idle only)
|
|
545
|
+
|
|
546
|
+
Options:
|
|
547
|
+
--items-per-iteration N Suggest N items per turn (prompt hint)
|
|
548
|
+
--reflect-every N Reflect every N iterations
|
|
549
|
+
--max-iterations N Stop after N iterations (default 50)
|
|
550
|
+
|
|
551
|
+
To stop: press ESC to interrupt, then run /ralph-stop when idle
|
|
552
|
+
|
|
553
|
+
Examples:
|
|
554
|
+
/ralph start my-feature
|
|
555
|
+
/ralph start review --items-per-iteration 5 --reflect-every 10`;
|
|
556
|
+
|
|
557
|
+
pi.registerCommand("ralph", {
|
|
558
|
+
description: "Ralph Wiggum - long-running development loops",
|
|
559
|
+
handler: async (args, ctx) => {
|
|
560
|
+
const [cmd] = args.trim().split(/\s+/);
|
|
561
|
+
const handler = commands[cmd];
|
|
562
|
+
if (handler) {
|
|
563
|
+
handler(args.slice(cmd.length).trim(), ctx);
|
|
564
|
+
} else {
|
|
565
|
+
ctx.ui.notify(HELP, "info");
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
pi.registerCommand("ralph-stop", {
|
|
571
|
+
description: "Stop active Ralph loop (idle only)",
|
|
572
|
+
handler: async (_args, ctx) => {
|
|
573
|
+
if (!ctx.isIdle()) {
|
|
574
|
+
if (ctx.hasUI) {
|
|
575
|
+
ctx.ui.notify("Agent is busy. Press ESC to interrupt, then run /ralph-stop.", "warning");
|
|
576
|
+
}
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
let state = currentLoop ? loadState(ctx, currentLoop) : null;
|
|
581
|
+
if (!state) {
|
|
582
|
+
const active = listLoops(ctx).find((l) => l.status === "active");
|
|
583
|
+
if (!active) {
|
|
584
|
+
if (ctx.hasUI) ctx.ui.notify("No active Ralph loop", "warning");
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
state = active;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (state.status !== "active") {
|
|
591
|
+
if (ctx.hasUI) ctx.ui.notify(`Loop "${state.name}" is not active`, "warning");
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
stopLoop(ctx, state, `Stopped Ralph loop: ${state.name} (iteration ${state.iteration})`);
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
// --- Tool for agent self-invocation ---
|
|
600
|
+
|
|
601
|
+
pi.registerTool({
|
|
602
|
+
name: "ralph_start",
|
|
603
|
+
label: "Start Ralph Loop",
|
|
604
|
+
description: "Start a long-running development loop. Use for complex multi-iteration tasks.",
|
|
605
|
+
parameters: Type.Object({
|
|
606
|
+
name: Type.String({ description: "Loop name (e.g., 'refactor-auth')" }),
|
|
607
|
+
taskContent: Type.String({ description: "Task in markdown with goals and checklist" }),
|
|
608
|
+
itemsPerIteration: Type.Optional(Type.Number({ description: "Suggest N items per turn (0 = no limit)" })),
|
|
609
|
+
reflectEvery: Type.Optional(Type.Number({ description: "Reflect every N iterations" })),
|
|
610
|
+
maxIterations: Type.Optional(Type.Number({ description: "Max iterations (default: 50)", default: 50 })),
|
|
611
|
+
}),
|
|
612
|
+
async execute(_toolCallId, params, _onUpdate, ctx) {
|
|
613
|
+
const loopName = sanitize(params.name);
|
|
614
|
+
const taskFile = path.join(RALPH_DIR, `${loopName}.md`);
|
|
615
|
+
|
|
616
|
+
if (loadState(ctx, loopName)?.status === "active") {
|
|
617
|
+
return { content: [{ type: "text", text: `Loop "${loopName}" already active.` }], details: {} };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const fullPath = path.resolve(ctx.cwd, taskFile);
|
|
621
|
+
ensureDir(fullPath);
|
|
622
|
+
fs.writeFileSync(fullPath, params.taskContent, "utf-8");
|
|
623
|
+
|
|
624
|
+
const state: LoopState = {
|
|
625
|
+
name: loopName,
|
|
626
|
+
taskFile,
|
|
627
|
+
iteration: 1,
|
|
628
|
+
maxIterations: params.maxIterations ?? 50,
|
|
629
|
+
itemsPerIteration: params.itemsPerIteration ?? 0,
|
|
630
|
+
reflectEvery: params.reflectEvery ?? 0,
|
|
631
|
+
reflectInstructions: DEFAULT_REFLECT_INSTRUCTIONS,
|
|
632
|
+
active: true,
|
|
633
|
+
status: "active",
|
|
634
|
+
startedAt: new Date().toISOString(),
|
|
635
|
+
lastReflectionAt: 0,
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
saveState(ctx, state);
|
|
639
|
+
currentLoop = loopName;
|
|
640
|
+
updateUI(ctx);
|
|
641
|
+
|
|
642
|
+
pi.sendUserMessage(buildPrompt(state, params.taskContent, false), { deliverAs: "followUp" });
|
|
643
|
+
|
|
644
|
+
return {
|
|
645
|
+
content: [{ type: "text", text: `Started loop "${loopName}" (max ${state.maxIterations} iterations).` }],
|
|
646
|
+
details: {},
|
|
647
|
+
};
|
|
648
|
+
},
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
// Tool for agent to signal iteration complete and request next
|
|
652
|
+
pi.registerTool({
|
|
653
|
+
name: "ralph_done",
|
|
654
|
+
label: "Ralph Iteration Done",
|
|
655
|
+
description: "Signal that you've completed this iteration of the Ralph loop. Call this after making progress to get the next iteration prompt. Do NOT call this if you've output the completion marker.",
|
|
656
|
+
parameters: Type.Object({}),
|
|
657
|
+
async execute(_toolCallId, _params, _onUpdate, ctx) {
|
|
658
|
+
if (!currentLoop) {
|
|
659
|
+
return { content: [{ type: "text", text: "No active Ralph loop." }], details: {} };
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const state = loadState(ctx, currentLoop);
|
|
663
|
+
if (!state || state.status !== "active") {
|
|
664
|
+
return { content: [{ type: "text", text: "Ralph loop is not active." }], details: {} };
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (ctx.hasPendingMessages()) {
|
|
668
|
+
return {
|
|
669
|
+
content: [{ type: "text", text: "Pending messages already queued. Skipping ralph_done." }],
|
|
670
|
+
details: {},
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Increment iteration
|
|
675
|
+
state.iteration++;
|
|
676
|
+
|
|
677
|
+
// Check max iterations
|
|
678
|
+
if (state.maxIterations > 0 && state.iteration > state.maxIterations) {
|
|
679
|
+
completeLoop(
|
|
680
|
+
ctx,
|
|
681
|
+
state,
|
|
682
|
+
`───────────────────────────────────────────────────────────────────────
|
|
683
|
+
⚠️ RALPH LOOP STOPPED: ${state.name} | Max iterations (${state.maxIterations}) reached
|
|
684
|
+
───────────────────────────────────────────────────────────────────────`,
|
|
685
|
+
);
|
|
686
|
+
return { content: [{ type: "text", text: "Max iterations reached. Loop stopped." }], details: {} };
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const needsReflection = state.reflectEvery > 0 && (state.iteration - 1) % state.reflectEvery === 0;
|
|
690
|
+
if (needsReflection) state.lastReflectionAt = state.iteration;
|
|
691
|
+
|
|
692
|
+
saveState(ctx, state);
|
|
693
|
+
updateUI(ctx);
|
|
694
|
+
|
|
695
|
+
const content = tryRead(path.resolve(ctx.cwd, state.taskFile));
|
|
696
|
+
if (!content) {
|
|
697
|
+
pauseLoop(ctx, state);
|
|
698
|
+
return { content: [{ type: "text", text: `Error: Could not read task file: ${state.taskFile}` }], details: {} };
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Queue next iteration - use followUp so user can still interrupt
|
|
702
|
+
pi.sendUserMessage(buildPrompt(state, content, needsReflection), { deliverAs: "followUp" });
|
|
703
|
+
|
|
704
|
+
return {
|
|
705
|
+
content: [{ type: "text", text: `Iteration ${state.iteration - 1} complete. Next iteration queued.` }],
|
|
706
|
+
details: {},
|
|
707
|
+
};
|
|
708
|
+
},
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
// --- Event handlers ---
|
|
712
|
+
|
|
713
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
714
|
+
if (!currentLoop) return;
|
|
715
|
+
const state = loadState(ctx, currentLoop);
|
|
716
|
+
if (!state || state.status !== "active") return;
|
|
717
|
+
|
|
718
|
+
const iterStr = `${state.iteration}${state.maxIterations > 0 ? `/${state.maxIterations}` : ""}`;
|
|
719
|
+
|
|
720
|
+
let instructions = `You are in a Ralph loop working on: ${state.taskFile}\n`;
|
|
721
|
+
if (state.itemsPerIteration > 0) {
|
|
722
|
+
instructions += `- Work on ~${state.itemsPerIteration} items this iteration\n`;
|
|
723
|
+
}
|
|
724
|
+
instructions += `- Update the task file as you progress\n`;
|
|
725
|
+
instructions += `- When FULLY COMPLETE: ${COMPLETE_MARKER}\n`;
|
|
726
|
+
instructions += `- Otherwise, call ralph_done tool to proceed to next iteration`;
|
|
727
|
+
|
|
728
|
+
return {
|
|
729
|
+
systemPromptAppend: `\n[RALPH LOOP - ${state.name} - Iteration ${iterStr}]\n\n${instructions}`,
|
|
730
|
+
};
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
pi.on("agent_end", async (event, ctx) => {
|
|
734
|
+
if (!currentLoop) return;
|
|
735
|
+
const state = loadState(ctx, currentLoop);
|
|
736
|
+
if (!state || state.status !== "active") return;
|
|
737
|
+
|
|
738
|
+
// Check for completion marker
|
|
739
|
+
const lastAssistant = [...event.messages].reverse().find((m) => m.role === "assistant");
|
|
740
|
+
const text =
|
|
741
|
+
lastAssistant && Array.isArray(lastAssistant.content)
|
|
742
|
+
? lastAssistant.content
|
|
743
|
+
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
744
|
+
.map((c) => c.text)
|
|
745
|
+
.join("\n")
|
|
746
|
+
: "";
|
|
747
|
+
|
|
748
|
+
if (text.includes(COMPLETE_MARKER)) {
|
|
749
|
+
completeLoop(
|
|
750
|
+
ctx,
|
|
751
|
+
state,
|
|
752
|
+
`───────────────────────────────────────────────────────────────────────
|
|
753
|
+
✅ RALPH LOOP COMPLETE: ${state.name} | ${state.iteration} iterations
|
|
754
|
+
───────────────────────────────────────────────────────────────────────`,
|
|
755
|
+
);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Check max iterations
|
|
760
|
+
if (state.maxIterations > 0 && state.iteration >= state.maxIterations) {
|
|
761
|
+
completeLoop(
|
|
762
|
+
ctx,
|
|
763
|
+
state,
|
|
764
|
+
`───────────────────────────────────────────────────────────────────────
|
|
765
|
+
⚠️ RALPH LOOP STOPPED: ${state.name} | Max iterations (${state.maxIterations}) reached
|
|
766
|
+
───────────────────────────────────────────────────────────────────────`,
|
|
767
|
+
);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Don't auto-continue - let the agent call ralph_done to proceed
|
|
772
|
+
// This allows user's "stop" message to be processed first
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
776
|
+
const active = listLoops(ctx).filter((l) => l.status === "active");
|
|
777
|
+
if (active.length > 0 && ctx.hasUI) {
|
|
778
|
+
const lines = active.map(
|
|
779
|
+
(l) => ` • ${l.name} (iteration ${l.iteration}${l.maxIterations > 0 ? `/${l.maxIterations}` : ""})`,
|
|
780
|
+
);
|
|
781
|
+
ctx.ui.notify(`Active Ralph loops:\n${lines.join("\n")}\n\nUse /ralph resume <name> to continue`, "info");
|
|
782
|
+
}
|
|
783
|
+
updateUI(ctx);
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
787
|
+
if (currentLoop) {
|
|
788
|
+
const state = loadState(ctx, currentLoop);
|
|
789
|
+
if (state) saveState(ctx, state);
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
}
|