@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 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.trim().split(/\s+/);
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, create if not
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
- const content = tryRead(fullPath);
294
+ let content = tryRead(fullPath);
321
295
  if (!content) {
322
- if (ctx.hasUI) ctx.ui.notify(`Could not read task file: ${taskFile}`, "error");
323
- return;
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.stopLoop(
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.0",
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.0",
34
- "@pi-unipi/info-screen": "2.6.0"
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
- "skills": [],
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, banner: string): void {
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: "completed",
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
- });