@pi-unipi/ralph 2.6.0 → 2.6.2
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/index.ts +11 -35
- package/package.json +10 -5
- package/ralph-loop.ts +3 -44
- package/reminder.test.ts +0 -66
package/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
RALPH_TOOLS,
|
|
16
16
|
emitEvent,
|
|
17
17
|
getPackageVersion,
|
|
18
|
+
parseArgs,
|
|
18
19
|
} from "@pi-unipi/core";
|
|
19
20
|
|
|
20
21
|
// Get info registry from global
|
|
@@ -67,17 +68,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
67
68
|
emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
|
|
68
69
|
name: MODULES.RALPH,
|
|
69
70
|
version: VERSION,
|
|
70
|
-
commands: [
|
|
71
|
-
"unipi:ralph-start",
|
|
72
|
-
"unipi:ralph-stop",
|
|
73
|
-
"unipi:ralph-resume",
|
|
74
|
-
"unipi:ralph-status",
|
|
75
|
-
"unipi:ralph-cancel",
|
|
76
|
-
"unipi:ralph-archive",
|
|
77
|
-
"unipi:ralph-clean",
|
|
78
|
-
"unipi:ralph-list",
|
|
79
|
-
"unipi:ralph-nuke",
|
|
80
|
-
],
|
|
71
|
+
commands: ["unipi:ralph", "unipi:ralph-stop"],
|
|
81
72
|
tools: [RALPH_TOOLS.START, RALPH_TOOLS.DONE],
|
|
82
73
|
});
|
|
83
74
|
|
|
@@ -142,12 +133,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
142
133
|
: "";
|
|
143
134
|
|
|
144
135
|
if (text.includes(RALPH_COMPLETE_MARKER)) {
|
|
145
|
-
mgr.completeLoop(
|
|
146
|
-
state,
|
|
147
|
-
`───────────────────────────────────────────────────────────────────────
|
|
148
|
-
✅ RALPH LOOP COMPLETE: ${state.name} | ${state.iteration} iterations
|
|
149
|
-
───────────────────────────────────────────────────────────────────────`,
|
|
150
|
-
);
|
|
136
|
+
mgr.completeLoop(state);
|
|
151
137
|
}
|
|
152
138
|
});
|
|
153
139
|
|
|
@@ -215,7 +201,7 @@ To stop: press ESC to interrupt, then run /unipi:ralph-stop when idle`;
|
|
|
215
201
|
pi.registerCommand("unipi:ralph", {
|
|
216
202
|
description: "Ralph loop commands (start, stop, resume, status, etc.)",
|
|
217
203
|
handler: async (args, ctx) => {
|
|
218
|
-
const parts = args
|
|
204
|
+
const parts = parseArgs(args);
|
|
219
205
|
const cmd = parts[0];
|
|
220
206
|
const rest = parts.slice(1).join(" ");
|
|
221
207
|
|
|
@@ -302,25 +288,14 @@ function handleStart(rest: string, ctx: ExtensionContext, pi: ExtensionAPI): voi
|
|
|
302
288
|
return;
|
|
303
289
|
}
|
|
304
290
|
|
|
305
|
-
// Check if task file exists
|
|
291
|
+
// Check if task file exists; startLoop writes the task content to disk.
|
|
306
292
|
const fullPath = require("node:path").resolve(ctx.cwd, taskFile);
|
|
307
|
-
const fs = require("node:fs");
|
|
308
|
-
if (!fs.existsSync(fullPath)) {
|
|
309
|
-
const { ensureDir } = require("@pi-unipi/core");
|
|
310
|
-
ensureDir(fullPath);
|
|
311
|
-
fs.writeFileSync(
|
|
312
|
-
fullPath,
|
|
313
|
-
`# Task\n\nDescribe your task here.\n\n## Goals\n- Goal 1\n\n## Checklist\n- [ ] Item 1\n\n## Notes\n(Update this as you work)\n`,
|
|
314
|
-
"utf-8",
|
|
315
|
-
);
|
|
316
|
-
if (ctx.hasUI) ctx.ui.notify(`Created task file: ${taskFile}`, "info");
|
|
317
|
-
}
|
|
318
|
-
|
|
319
293
|
const { tryRead } = require("@pi-unipi/core");
|
|
320
|
-
|
|
294
|
+
let content = tryRead(fullPath);
|
|
321
295
|
if (!content) {
|
|
322
|
-
|
|
323
|
-
|
|
296
|
+
// File doesn't exist yet — pass the template through so startLoop creates it.
|
|
297
|
+
content = `# Task\n\nDescribe your task here.\n\n## Goals\n- Goal 1\n\n## Checklist\n- [ ] Item 1\n\n## Notes\n(Update this as you work)\n`;
|
|
298
|
+
if (ctx.hasUI) ctx.ui.notify(`Created task file: ${taskFile}`, "info");
|
|
324
299
|
}
|
|
325
300
|
|
|
326
301
|
const state = mgr.startLoop(loopName, taskFile, content, {
|
|
@@ -362,8 +337,9 @@ function handleStop(ctx: ExtensionContext, pi: ExtensionAPI): void {
|
|
|
362
337
|
return;
|
|
363
338
|
}
|
|
364
339
|
|
|
365
|
-
mgr.
|
|
340
|
+
mgr.completeLoop(
|
|
366
341
|
state,
|
|
342
|
+
"cancelled",
|
|
367
343
|
`Stopped Ralph loop: ${state.name} (iteration ${state.iteration})`,
|
|
368
344
|
);
|
|
369
345
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/ralph",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.2",
|
|
4
4
|
"description": "Long-running iterative development loops for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
],
|
|
21
21
|
"files": [
|
|
22
22
|
"*.ts",
|
|
23
|
+
"!*.test.ts",
|
|
23
24
|
"SKILL.md",
|
|
24
25
|
"README.md"
|
|
25
26
|
],
|
|
@@ -30,8 +31,8 @@
|
|
|
30
31
|
"test": "npx tsx --test reminder.test.ts"
|
|
31
32
|
},
|
|
32
33
|
"dependencies": {
|
|
33
|
-
"@pi-unipi/core": "2.6.
|
|
34
|
-
"@pi-unipi/info-screen": "2.6.
|
|
34
|
+
"@pi-unipi/core": "2.6.1",
|
|
35
|
+
"@pi-unipi/info-screen": "2.6.2"
|
|
35
36
|
},
|
|
36
37
|
"peerDependencies": {
|
|
37
38
|
"@earendil-works/pi-ai": "^0.80.0",
|
|
@@ -40,8 +41,12 @@
|
|
|
40
41
|
"typebox": "^1.1.38"
|
|
41
42
|
},
|
|
42
43
|
"pi": {
|
|
43
|
-
"extensions": [
|
|
44
|
-
|
|
44
|
+
"extensions": [
|
|
45
|
+
"./index.ts"
|
|
46
|
+
],
|
|
47
|
+
"skills": [
|
|
48
|
+
"./SKILL.md"
|
|
49
|
+
],
|
|
45
50
|
"prompts": [],
|
|
46
51
|
"themes": []
|
|
47
52
|
}
|
package/ralph-loop.ts
CHANGED
|
@@ -57,23 +57,6 @@ Pause and reflect on your progress:
|
|
|
57
57
|
|
|
58
58
|
Update the task file with your reflection, then continue working.`;
|
|
59
59
|
|
|
60
|
-
/** Default task template */
|
|
61
|
-
export const DEFAULT_TEMPLATE = `# Task
|
|
62
|
-
|
|
63
|
-
Describe your task here.
|
|
64
|
-
|
|
65
|
-
## Goals
|
|
66
|
-
- Goal 1
|
|
67
|
-
- Goal 2
|
|
68
|
-
|
|
69
|
-
## Checklist
|
|
70
|
-
- [ ] Item 1
|
|
71
|
-
- [ ] Item 2
|
|
72
|
-
|
|
73
|
-
## Notes
|
|
74
|
-
(Update this as you work)
|
|
75
|
-
`;
|
|
76
|
-
|
|
77
60
|
/** Ralph loop manager */
|
|
78
61
|
export class RalphLoopManager {
|
|
79
62
|
private currentLoop: string | null = null;
|
|
@@ -144,7 +127,7 @@ export class RalphLoopManager {
|
|
|
144
127
|
if (message && this.ctx.hasUI) this.ctx.ui.notify(message, "info");
|
|
145
128
|
}
|
|
146
129
|
|
|
147
|
-
completeLoop(state: LoopState,
|
|
130
|
+
completeLoop(state: LoopState, reason: "completed" | "cancelled" = "completed", message?: string): void {
|
|
148
131
|
state.status = "completed";
|
|
149
132
|
state.completedAt = now();
|
|
150
133
|
state.active = false;
|
|
@@ -156,26 +139,7 @@ export class RalphLoopManager {
|
|
|
156
139
|
iteration: state.iteration,
|
|
157
140
|
maxIterations: state.maxIterations,
|
|
158
141
|
status: "completed",
|
|
159
|
-
reason
|
|
160
|
-
} satisfies UnipiRalphLoopEvent);
|
|
161
|
-
|
|
162
|
-
this.currentLoop = null;
|
|
163
|
-
this.updateUI();
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
stopLoop(state: LoopState, message?: string): void {
|
|
167
|
-
state.status = "completed";
|
|
168
|
-
state.completedAt = now();
|
|
169
|
-
state.active = false;
|
|
170
|
-
this.saveState(state);
|
|
171
|
-
|
|
172
|
-
// Emit event
|
|
173
|
-
this.emitFn(UNIPI_EVENTS.RALPH_LOOP_END, {
|
|
174
|
-
name: state.name,
|
|
175
|
-
iteration: state.iteration,
|
|
176
|
-
maxIterations: state.maxIterations,
|
|
177
|
-
status: "completed",
|
|
178
|
-
reason: "cancelled",
|
|
142
|
+
reason,
|
|
179
143
|
} satisfies UnipiRalphLoopEvent);
|
|
180
144
|
|
|
181
145
|
this.currentLoop = null;
|
|
@@ -365,12 +329,7 @@ export class RalphLoopManager {
|
|
|
365
329
|
|
|
366
330
|
// Check max iterations
|
|
367
331
|
if (state.maxIterations > 0 && state.iteration > state.maxIterations) {
|
|
368
|
-
this.completeLoop(
|
|
369
|
-
state,
|
|
370
|
-
`───────────────────────────────────────────────────────────────────────
|
|
371
|
-
⚠️ RALPH LOOP STOPPED: ${state.name} | Max iterations (${state.maxIterations}) reached
|
|
372
|
-
───────────────────────────────────────────────────────────────────────`,
|
|
373
|
-
);
|
|
332
|
+
this.completeLoop(state);
|
|
374
333
|
return null;
|
|
375
334
|
}
|
|
376
335
|
|
package/reminder.test.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import {
|
|
4
|
-
buildRalphLoopReminder,
|
|
5
|
-
latestRalphReminder,
|
|
6
|
-
RALPH_REMINDER_TYPE,
|
|
7
|
-
} from "./reminder.ts";
|
|
8
|
-
|
|
9
|
-
const state = {
|
|
10
|
-
name: "cache-rollout",
|
|
11
|
-
iteration: 3,
|
|
12
|
-
maxIterations: 10,
|
|
13
|
-
taskFile: ".unipi/ralph/cache-rollout.md",
|
|
14
|
-
itemsPerIteration: 2,
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
function context(branch: any[]) {
|
|
18
|
-
return { sessionManager: { getBranch: () => branch } } as any;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function custom(content: string) {
|
|
22
|
-
return {
|
|
23
|
-
type: "custom_message",
|
|
24
|
-
customType: RALPH_REMINDER_TYPE,
|
|
25
|
-
content,
|
|
26
|
-
id: "r1",
|
|
27
|
-
parentId: null,
|
|
28
|
-
timestamp: "2026-08-14T00:00:00.000Z",
|
|
29
|
-
display: false,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
describe("Ralph append-only reminder snapshots", () => {
|
|
34
|
-
it("is deterministic and explicitly supersedes older reminders", () => {
|
|
35
|
-
const first = buildRalphLoopReminder(state);
|
|
36
|
-
const second = buildRalphLoopReminder({ ...state });
|
|
37
|
-
assert.equal(first, second);
|
|
38
|
-
assert.match(first, /supersedes all earlier Ralph loop reminders/);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("finds an unchanged retained snapshot for deduplication", () => {
|
|
42
|
-
const content = buildRalphLoopReminder(state);
|
|
43
|
-
assert.equal(latestRalphReminder(context([custom(content)])), content);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("returns the newest retained snapshot when state changes", () => {
|
|
47
|
-
const old = buildRalphLoopReminder({ ...state, iteration: 2 });
|
|
48
|
-
const current = buildRalphLoopReminder(state);
|
|
49
|
-
assert.equal(latestRalphReminder(context([custom(old), { ...custom(current), id: "r2" }])), current);
|
|
50
|
-
assert.notEqual(old, current);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("stops at compaction so current state is reinjected once in the new epoch", () => {
|
|
54
|
-
const old = buildRalphLoopReminder({ ...state, iteration: 2 });
|
|
55
|
-
const compaction = {
|
|
56
|
-
type: "compaction",
|
|
57
|
-
id: "c1",
|
|
58
|
-
parentId: "r1",
|
|
59
|
-
timestamp: "2026-08-14T00:00:01.000Z",
|
|
60
|
-
summary: `folded: ${old}`,
|
|
61
|
-
firstKeptEntryId: "r1",
|
|
62
|
-
tokensBefore: 1000,
|
|
63
|
-
};
|
|
64
|
-
assert.equal(latestRalphReminder(context([custom(old), compaction])), null);
|
|
65
|
-
});
|
|
66
|
-
});
|