@pi-unipi/unipi 0.1.1
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/README.md +143 -0
- package/package.json +70 -0
- package/packages/btw/README.md +95 -0
- package/packages/btw/extensions/btw.ts +1972 -0
- package/packages/btw/skills/btw/SKILL.md +149 -0
- package/packages/core/README.md +74 -0
- package/packages/core/index.ts +10 -0
- package/packages/info-screen/README.md +115 -0
- package/packages/info-screen/index.ts +165 -0
- package/packages/memory/README.md +94 -0
- package/packages/memory/index.ts +209 -0
- package/packages/memory/skills/memory/SKILL.md +151 -0
- package/packages/ralph/README.md +101 -0
- package/packages/ralph/index.ts +548 -0
- package/packages/subagents/README.md +114 -0
- package/packages/unipi/index.ts +28 -0
- package/packages/workflow/README.md +129 -0
- package/packages/workflow/index.ts +155 -0
- package/packages/workflow/skills/brainstorm/SKILL.md +202 -0
- package/packages/workflow/skills/consolidate/SKILL.md +142 -0
- package/packages/workflow/skills/consultant/SKILL.md +97 -0
- package/packages/workflow/skills/document/SKILL.md +120 -0
- package/packages/workflow/skills/gather-context/SKILL.md +122 -0
- package/packages/workflow/skills/plan/SKILL.md +169 -0
- package/packages/workflow/skills/quick-work/SKILL.md +110 -0
- package/packages/workflow/skills/review-work/SKILL.md +131 -0
- package/packages/workflow/skills/scan-issues/SKILL.md +183 -0
- package/packages/workflow/skills/work/SKILL.md +144 -0
- package/packages/workflow/skills/worktree-create/SKILL.md +69 -0
- package/packages/workflow/skills/worktree-list/SKILL.md +67 -0
- package/packages/workflow/skills/worktree-merge/SKILL.md +79 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @unipi/ralph — Long-running iterative development loops
|
|
3
|
+
*
|
|
4
|
+
* Adapted from pi-ralph-wiggum with unipi event integration.
|
|
5
|
+
* Emits MODULE_READY, RALPH_LOOP_START/END events.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import {
|
|
10
|
+
UNIPI_EVENTS,
|
|
11
|
+
MODULES,
|
|
12
|
+
RALPH_COMPLETE_MARKER,
|
|
13
|
+
RALPH_TOOLS,
|
|
14
|
+
emitEvent,
|
|
15
|
+
getPackageVersion,
|
|
16
|
+
} from "@pi-unipi/core";
|
|
17
|
+
|
|
18
|
+
// Get info registry from global
|
|
19
|
+
function getInfoRegistry() {
|
|
20
|
+
const g = globalThis as any;
|
|
21
|
+
return g.__unipi_info_registry;
|
|
22
|
+
}
|
|
23
|
+
import { RalphLoopManager } from "./ralph-loop.js";
|
|
24
|
+
import { registerRalphTools } from "./tools.js";
|
|
25
|
+
|
|
26
|
+
/** Package version */
|
|
27
|
+
const VERSION = getPackageVersion(new URL(".", import.meta.url).pathname);
|
|
28
|
+
|
|
29
|
+
/** Current loop manager instance (recreated on session reload) */
|
|
30
|
+
let manager: RalphLoopManager | null = null;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get or create the loop manager for the current context.
|
|
34
|
+
*/
|
|
35
|
+
function getManager(ctx: ExtensionContext, pi: ExtensionAPI): RalphLoopManager {
|
|
36
|
+
if (!manager) {
|
|
37
|
+
manager = new RalphLoopManager(ctx, (event, payload) =>
|
|
38
|
+
emitEvent(pi, event, payload),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return manager;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default function (pi: ExtensionAPI) {
|
|
45
|
+
// Register tools
|
|
46
|
+
// (Manager will be created lazily on first use)
|
|
47
|
+
|
|
48
|
+
// Register commands
|
|
49
|
+
registerCommands(pi);
|
|
50
|
+
|
|
51
|
+
// Session lifecycle
|
|
52
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
53
|
+
manager = null; // Force re-creation with new context
|
|
54
|
+
const mgr = getManager(ctx, pi);
|
|
55
|
+
|
|
56
|
+
// Rehydrate from disk
|
|
57
|
+
mgr.rehydrate();
|
|
58
|
+
|
|
59
|
+
// Announce module
|
|
60
|
+
emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
|
|
61
|
+
name: MODULES.RALPH,
|
|
62
|
+
version: VERSION,
|
|
63
|
+
commands: [
|
|
64
|
+
"unipi:ralph-start",
|
|
65
|
+
"unipi:ralph-stop",
|
|
66
|
+
"unipi:ralph-resume",
|
|
67
|
+
"unipi:ralph-status",
|
|
68
|
+
"unipi:ralph-cancel",
|
|
69
|
+
"unipi:ralph-archive",
|
|
70
|
+
"unipi:ralph-clean",
|
|
71
|
+
"unipi:ralph-list",
|
|
72
|
+
"unipi:ralph-nuke",
|
|
73
|
+
],
|
|
74
|
+
tools: [RALPH_TOOLS.START, RALPH_TOOLS.DONE],
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Register info group
|
|
78
|
+
const registry = getInfoRegistry();
|
|
79
|
+
if (registry) {
|
|
80
|
+
registry.registerGroup({
|
|
81
|
+
id: "ralph",
|
|
82
|
+
name: "Ralph Loops",
|
|
83
|
+
icon: "🔄",
|
|
84
|
+
priority: 70,
|
|
85
|
+
config: {
|
|
86
|
+
showByDefault: true,
|
|
87
|
+
stats: [
|
|
88
|
+
{ id: "activeLoops", label: "Active Loops", show: true },
|
|
89
|
+
{ id: "totalIterations", label: "Total Iterations", show: true },
|
|
90
|
+
{ id: "status", label: "Status", show: true },
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
dataProvider: async () => {
|
|
94
|
+
const currentLoop = mgr.getCurrentLoop();
|
|
95
|
+
if (!currentLoop) {
|
|
96
|
+
return {
|
|
97
|
+
activeLoops: { value: "0" },
|
|
98
|
+
totalIterations: { value: "0" },
|
|
99
|
+
status: { value: "idle" },
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const state = mgr.loadState(currentLoop);
|
|
104
|
+
return {
|
|
105
|
+
activeLoops: { value: "1" },
|
|
106
|
+
totalIterations: { value: String(state?.iteration ?? 0) },
|
|
107
|
+
status: { value: state?.status ?? "unknown" },
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Agent lifecycle — check for completion marker
|
|
115
|
+
pi.on("agent_end", async (event, ctx) => {
|
|
116
|
+
const mgr = getManager(ctx, pi);
|
|
117
|
+
const currentLoop = mgr.getCurrentLoop();
|
|
118
|
+
if (!currentLoop) return;
|
|
119
|
+
|
|
120
|
+
const state = mgr.loadState(currentLoop);
|
|
121
|
+
if (!state || state.status !== "active") return;
|
|
122
|
+
|
|
123
|
+
// Check for completion marker in last assistant message
|
|
124
|
+
const lastAssistant = [...event.messages]
|
|
125
|
+
.reverse()
|
|
126
|
+
.find((m) => m.role === "assistant");
|
|
127
|
+
const text =
|
|
128
|
+
lastAssistant && Array.isArray(lastAssistant.content)
|
|
129
|
+
? lastAssistant.content
|
|
130
|
+
.filter(
|
|
131
|
+
(c): c is { type: "text"; text: string } => c.type === "text",
|
|
132
|
+
)
|
|
133
|
+
.map((c) => c.text)
|
|
134
|
+
.join("\n")
|
|
135
|
+
: "";
|
|
136
|
+
|
|
137
|
+
if (text.includes(RALPH_COMPLETE_MARKER)) {
|
|
138
|
+
mgr.completeLoop(
|
|
139
|
+
state,
|
|
140
|
+
`───────────────────────────────────────────────────────────────────────
|
|
141
|
+
✅ RALPH LOOP COMPLETE: ${state.name} | ${state.iteration} iterations
|
|
142
|
+
───────────────────────────────────────────────────────────────────────`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Inject ralph instructions when loop is active
|
|
148
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
149
|
+
const mgr = getManager(ctx, pi);
|
|
150
|
+
const currentLoop = mgr.getCurrentLoop();
|
|
151
|
+
if (!currentLoop) return;
|
|
152
|
+
|
|
153
|
+
const state = mgr.loadState(currentLoop);
|
|
154
|
+
if (!state || state.status !== "active") return;
|
|
155
|
+
|
|
156
|
+
const iterStr = `${state.iteration}${state.maxIterations > 0 ? `/${state.maxIterations}` : ""}`;
|
|
157
|
+
|
|
158
|
+
let instructions = `You are in a Ralph loop working on: ${state.taskFile}\n`;
|
|
159
|
+
if (state.itemsPerIteration > 0) {
|
|
160
|
+
instructions += `- Work on ~${state.itemsPerIteration} items this iteration\n`;
|
|
161
|
+
}
|
|
162
|
+
instructions += `- Update the task file as you progress\n`;
|
|
163
|
+
instructions += `- When FULLY COMPLETE: ${RALPH_COMPLETE_MARKER}\n`;
|
|
164
|
+
instructions += `- Otherwise, call ralph_done tool to proceed to next iteration`;
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
systemPrompt:
|
|
168
|
+
event.systemPrompt +
|
|
169
|
+
`\n[RALPH LOOP - ${state.name} - Iteration ${iterStr}]\n\n${instructions}`,
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Save state on shutdown
|
|
174
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
175
|
+
if (!manager) return;
|
|
176
|
+
const currentLoop = manager.getCurrentLoop();
|
|
177
|
+
if (currentLoop) {
|
|
178
|
+
const state = manager.loadState(currentLoop);
|
|
179
|
+
if (state) manager.saveState(state);
|
|
180
|
+
}
|
|
181
|
+
manager = null;
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Register tools after manager setup
|
|
185
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
186
|
+
const mgr = getManager(ctx, pi);
|
|
187
|
+
registerRalphTools(pi, mgr);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Register ralph commands.
|
|
193
|
+
*/
|
|
194
|
+
function registerCommands(pi: ExtensionAPI): void {
|
|
195
|
+
const HELP = `Ralph Loop — Long-running development loops
|
|
196
|
+
|
|
197
|
+
Commands:
|
|
198
|
+
/unipi:ralph start <name|path> [options] Start a new loop
|
|
199
|
+
/unipi:ralph stop Pause current loop
|
|
200
|
+
/unipi:ralph resume <name> Resume a paused loop
|
|
201
|
+
/unipi:ralph status Show all loops
|
|
202
|
+
/unipi:ralph cancel <name> Delete loop state
|
|
203
|
+
/unipi:ralph archive <name> Move loop to archive
|
|
204
|
+
/unipi:ralph clean [--all] Clean completed loops
|
|
205
|
+
/unipi:ralph list --archived Show archived loops
|
|
206
|
+
/unipi:ralph nuke [--yes] Delete all ralph data
|
|
207
|
+
|
|
208
|
+
Options:
|
|
209
|
+
--items-per-iteration N Suggest N items per turn (prompt hint)
|
|
210
|
+
--reflect-every N Reflect every N iterations
|
|
211
|
+
--max-iterations N Stop after N iterations (default 50)
|
|
212
|
+
|
|
213
|
+
To stop: press ESC to interrupt, then run /unipi:ralph-stop when idle`;
|
|
214
|
+
|
|
215
|
+
pi.registerCommand("unipi:ralph", {
|
|
216
|
+
description: "Ralph loop commands (start, stop, resume, status, etc.)",
|
|
217
|
+
handler: async (args, ctx) => {
|
|
218
|
+
const parts = args.trim().split(/\s+/);
|
|
219
|
+
const cmd = parts[0];
|
|
220
|
+
const rest = parts.slice(1).join(" ");
|
|
221
|
+
|
|
222
|
+
// We need manager for most commands
|
|
223
|
+
// For 'start', we'll handle it specially
|
|
224
|
+
if (cmd === "start") {
|
|
225
|
+
handleStart(rest, ctx, pi);
|
|
226
|
+
} else if (cmd === "stop") {
|
|
227
|
+
handleStop(ctx, pi);
|
|
228
|
+
} else if (cmd === "resume") {
|
|
229
|
+
handleResume(rest, ctx, pi);
|
|
230
|
+
} else if (cmd === "status" || cmd === "list") {
|
|
231
|
+
handleList(rest, ctx, pi);
|
|
232
|
+
} else if (cmd === "cancel") {
|
|
233
|
+
handleCancel(rest, ctx, pi);
|
|
234
|
+
} else if (cmd === "archive") {
|
|
235
|
+
handleArchive(rest, ctx, pi);
|
|
236
|
+
} else if (cmd === "clean") {
|
|
237
|
+
handleClean(rest, ctx, pi);
|
|
238
|
+
} else if (cmd === "nuke") {
|
|
239
|
+
handleNuke(rest, ctx, pi);
|
|
240
|
+
} else {
|
|
241
|
+
if (ctx.hasUI) ctx.ui.notify(HELP, "info");
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Dedicated stop command for idle-only use
|
|
247
|
+
pi.registerCommand("unipi:ralph-stop", {
|
|
248
|
+
description: "Stop active Ralph loop (idle only)",
|
|
249
|
+
handler: async (_args, ctx) => {
|
|
250
|
+
handleStop(ctx, pi);
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function handleStart(rest: string, ctx: ExtensionContext, pi: ExtensionAPI): void {
|
|
256
|
+
const tokens = rest.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
|
|
257
|
+
let name = "";
|
|
258
|
+
let maxIterations = 50;
|
|
259
|
+
let itemsPerIteration = 0;
|
|
260
|
+
let reflectEvery = 0;
|
|
261
|
+
|
|
262
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
263
|
+
const tok = tokens[i];
|
|
264
|
+
const next = tokens[i + 1];
|
|
265
|
+
if (tok === "--max-iterations" && next) {
|
|
266
|
+
maxIterations = parseInt(next, 10) || 0;
|
|
267
|
+
i++;
|
|
268
|
+
} else if (tok === "--items-per-iteration" && next) {
|
|
269
|
+
itemsPerIteration = parseInt(next, 10) || 0;
|
|
270
|
+
i++;
|
|
271
|
+
} else if (tok === "--reflect-every" && next) {
|
|
272
|
+
reflectEvery = parseInt(next, 10) || 0;
|
|
273
|
+
i++;
|
|
274
|
+
} else if (!tok.startsWith("--")) {
|
|
275
|
+
name = tok.replace(/^"|"$/g, "");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (!name) {
|
|
280
|
+
if (ctx.hasUI)
|
|
281
|
+
ctx.ui.notify(
|
|
282
|
+
"Usage: /unipi:ralph start <name|path> [--items-per-iteration N] [--reflect-every N] [--max-iterations N]",
|
|
283
|
+
"warning",
|
|
284
|
+
);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const mgr = getManager(ctx, pi);
|
|
289
|
+
const isPath = name.includes("/") || name.includes("\\");
|
|
290
|
+
const loopName = isPath
|
|
291
|
+
? name.replace(/[^a-zA-Z0-9_-]/g, "_").replace(/_+/g, "_")
|
|
292
|
+
: name;
|
|
293
|
+
const taskFile = isPath ? name : `.unipi/ralph/${loopName}.md`;
|
|
294
|
+
|
|
295
|
+
const existing = mgr.loadState(loopName);
|
|
296
|
+
if (existing?.status === "active") {
|
|
297
|
+
if (ctx.hasUI)
|
|
298
|
+
ctx.ui.notify(
|
|
299
|
+
`Loop "${loopName}" is already active. Use /unipi:ralph resume ${loopName}`,
|
|
300
|
+
"warning",
|
|
301
|
+
);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Check if task file exists, create if not
|
|
306
|
+
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
|
+
const { tryRead } = require("@pi-unipi/core");
|
|
320
|
+
const content = tryRead(fullPath);
|
|
321
|
+
if (!content) {
|
|
322
|
+
if (ctx.hasUI) ctx.ui.notify(`Could not read task file: ${taskFile}`, "error");
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const state = mgr.startLoop(loopName, taskFile, content, {
|
|
327
|
+
maxIterations,
|
|
328
|
+
itemsPerIteration,
|
|
329
|
+
reflectEvery,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
pi.sendUserMessage(mgr.buildPrompt(state, content, false));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function handleStop(ctx: ExtensionContext, pi: ExtensionAPI): void {
|
|
336
|
+
if (!ctx.isIdle()) {
|
|
337
|
+
if (ctx.hasUI) {
|
|
338
|
+
ctx.ui.notify(
|
|
339
|
+
"Agent is busy. Press ESC to interrupt, then run /unipi:ralph-stop.",
|
|
340
|
+
"warning",
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const mgr = getManager(ctx, pi);
|
|
347
|
+
let currentLoop = mgr.getCurrentLoop();
|
|
348
|
+
let state = currentLoop ? mgr.loadState(currentLoop) : null;
|
|
349
|
+
|
|
350
|
+
if (!state) {
|
|
351
|
+
const active = mgr.listLoops().find((l) => l.status === "active");
|
|
352
|
+
if (!active) {
|
|
353
|
+
if (ctx.hasUI) ctx.ui.notify("No active Ralph loop", "warning");
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
state = active;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (state.status !== "active") {
|
|
360
|
+
if (ctx.hasUI)
|
|
361
|
+
ctx.ui.notify(`Loop "${state.name}" is not active`, "warning");
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
mgr.stopLoop(
|
|
366
|
+
state,
|
|
367
|
+
`Stopped Ralph loop: ${state.name} (iteration ${state.iteration})`,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function handleResume(
|
|
372
|
+
rest: string,
|
|
373
|
+
ctx: ExtensionContext,
|
|
374
|
+
pi: ExtensionAPI,
|
|
375
|
+
): void {
|
|
376
|
+
const loopName = rest.trim();
|
|
377
|
+
if (!loopName) {
|
|
378
|
+
if (ctx.hasUI) ctx.ui.notify("Usage: /unipi:ralph resume <name>", "warning");
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const mgr = getManager(ctx, pi);
|
|
383
|
+
const state = mgr.resumeLoop(loopName);
|
|
384
|
+
if (!state) {
|
|
385
|
+
if (ctx.hasUI) ctx.ui.notify(`Loop "${loopName}" not found or completed`, "error");
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (ctx.hasUI)
|
|
390
|
+
ctx.ui.notify(
|
|
391
|
+
`Resumed: ${loopName} (iteration ${state.iteration})`,
|
|
392
|
+
"info",
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
const content = mgr.tryReadTask(state);
|
|
396
|
+
if (!content) {
|
|
397
|
+
if (ctx.hasUI)
|
|
398
|
+
ctx.ui.notify(`Could not read task file: ${state.taskFile}`, "error");
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const needsReflection =
|
|
403
|
+
state.reflectEvery > 0 &&
|
|
404
|
+
state.iteration > 1 &&
|
|
405
|
+
(state.iteration - 1) % state.reflectEvery === 0;
|
|
406
|
+
pi.sendUserMessage(mgr.buildPrompt(state, content, needsReflection));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function handleList(
|
|
410
|
+
rest: string,
|
|
411
|
+
ctx: ExtensionContext,
|
|
412
|
+
pi: ExtensionAPI,
|
|
413
|
+
): void {
|
|
414
|
+
const archived = rest.trim() === "--archived";
|
|
415
|
+
const mgr = getManager(ctx, pi);
|
|
416
|
+
const loops = mgr.listLoops(archived);
|
|
417
|
+
|
|
418
|
+
if (loops.length === 0) {
|
|
419
|
+
if (ctx.hasUI)
|
|
420
|
+
ctx.ui.notify(
|
|
421
|
+
archived
|
|
422
|
+
? "No archived loops"
|
|
423
|
+
: "No loops found. Use /unipi:ralph list --archived for archived.",
|
|
424
|
+
"info",
|
|
425
|
+
);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const label = archived ? "Archived loops" : "Ralph loops";
|
|
430
|
+
if (ctx.hasUI)
|
|
431
|
+
ctx.ui.notify(
|
|
432
|
+
`${label}:\n${loops.map((l) => mgr.formatLoop(l)).join("\n")}`,
|
|
433
|
+
"info",
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function handleCancel(
|
|
438
|
+
rest: string,
|
|
439
|
+
ctx: ExtensionContext,
|
|
440
|
+
pi: ExtensionAPI,
|
|
441
|
+
): void {
|
|
442
|
+
const loopName = rest.trim();
|
|
443
|
+
if (!loopName) {
|
|
444
|
+
if (ctx.hasUI) ctx.ui.notify("Usage: /unipi:ralph cancel <name>", "warning");
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const mgr = getManager(ctx, pi);
|
|
449
|
+
const state = mgr.loadState(loopName);
|
|
450
|
+
if (!state) {
|
|
451
|
+
if (ctx.hasUI) ctx.ui.notify(`Loop "${loopName}" not found`, "error");
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (mgr.getCurrentLoop() === loopName) mgr.setCurrentLoop(null);
|
|
456
|
+
const { tryDelete } = require("@pi-unipi/core");
|
|
457
|
+
tryDelete(
|
|
458
|
+
require("node:path").resolve(
|
|
459
|
+
ctx.cwd,
|
|
460
|
+
`.unipi/ralph/${loopName.replace(/[^a-zA-Z0-9_-]/g, "_")}.state.json`,
|
|
461
|
+
),
|
|
462
|
+
);
|
|
463
|
+
if (ctx.hasUI) ctx.ui.notify(`Cancelled: ${loopName}`, "info");
|
|
464
|
+
mgr.updateUI();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function handleArchive(
|
|
468
|
+
rest: string,
|
|
469
|
+
ctx: ExtensionContext,
|
|
470
|
+
pi: ExtensionAPI,
|
|
471
|
+
): void {
|
|
472
|
+
const loopName = rest.trim();
|
|
473
|
+
if (!loopName) {
|
|
474
|
+
if (ctx.hasUI) ctx.ui.notify("Usage: /unipi:ralph archive <name>", "warning");
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const mgr = getManager(ctx, pi);
|
|
479
|
+
if (mgr.archiveLoop(loopName)) {
|
|
480
|
+
if (ctx.hasUI) ctx.ui.notify(`Archived: ${loopName}`, "info");
|
|
481
|
+
} else {
|
|
482
|
+
if (ctx.hasUI)
|
|
483
|
+
ctx.ui.notify(
|
|
484
|
+
`Cannot archive "${loopName}" — not found or still active`,
|
|
485
|
+
"warning",
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function handleClean(
|
|
491
|
+
rest: string,
|
|
492
|
+
ctx: ExtensionContext,
|
|
493
|
+
pi: ExtensionAPI,
|
|
494
|
+
): void {
|
|
495
|
+
const mgr = getManager(ctx, pi);
|
|
496
|
+
const cleaned = mgr.cleanCompleted(rest.trim() === "--all");
|
|
497
|
+
|
|
498
|
+
if (cleaned.length === 0) {
|
|
499
|
+
if (ctx.hasUI) ctx.ui.notify("No completed loops to clean", "info");
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const suffix = rest.trim() === "--all" ? " (all files)" : " (state only)";
|
|
504
|
+
if (ctx.hasUI)
|
|
505
|
+
ctx.ui.notify(
|
|
506
|
+
`Cleaned ${cleaned.length} loop(s)${suffix}:\n${cleaned.map((n) => ` • ${n}`).join("\n")}`,
|
|
507
|
+
"info",
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function handleNuke(
|
|
512
|
+
rest: string,
|
|
513
|
+
ctx: ExtensionContext,
|
|
514
|
+
pi: ExtensionAPI,
|
|
515
|
+
): void {
|
|
516
|
+
const force = rest.trim() === "--yes";
|
|
517
|
+
|
|
518
|
+
const run = () => {
|
|
519
|
+
const mgr = getManager(ctx, pi);
|
|
520
|
+
if (mgr.nukeAll()) {
|
|
521
|
+
if (ctx.hasUI) ctx.ui.notify("Removed .unipi/ralph directory.", "info");
|
|
522
|
+
} else {
|
|
523
|
+
if (ctx.hasUI) ctx.ui.notify("No .unipi/ralph directory found.", "info");
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
if (!force) {
|
|
528
|
+
if (ctx.hasUI) {
|
|
529
|
+
void ctx.ui
|
|
530
|
+
.confirm(
|
|
531
|
+
"Delete all Ralph loop files?",
|
|
532
|
+
"This deletes all .unipi/ralph state, task, and archive files.",
|
|
533
|
+
)
|
|
534
|
+
.then((confirmed) => {
|
|
535
|
+
if (confirmed) run();
|
|
536
|
+
});
|
|
537
|
+
} else {
|
|
538
|
+
if (ctx.hasUI)
|
|
539
|
+
ctx.ui.notify(
|
|
540
|
+
"Run /unipi:ralph nuke --yes to confirm. This deletes all .unipi/ralph data.",
|
|
541
|
+
"warning",
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
run();
|
|
548
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# @pi-unipi/subagents
|
|
2
|
+
|
|
3
|
+
Parallel sub-agent execution for [Pi coding agent](https://github.com/badlogic/pi-mono). Spawn background or foreground agents, track activity, and manage concurrent work.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pi install npm:@pi-unipi/subagents
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or as part of the full suite:
|
|
12
|
+
```bash
|
|
13
|
+
pi install npm:unipi
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Tools
|
|
17
|
+
|
|
18
|
+
| Tool | Description |
|
|
19
|
+
|------|-------------|
|
|
20
|
+
| `spawn_helper` | Launch a sub-agent for parallel work |
|
|
21
|
+
| `get_helper_result` | Check status and retrieve results from a background agent |
|
|
22
|
+
|
|
23
|
+
## Agent Types
|
|
24
|
+
|
|
25
|
+
| Type | Description |
|
|
26
|
+
|------|-------------|
|
|
27
|
+
| `explore` | Parallel file reads, research, analysis |
|
|
28
|
+
| `work` | Parallel file writes with transparent locking |
|
|
29
|
+
| Custom | Define your own in `~/.unipi/config/agents/<name>.md` |
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
### Foreground (blocks until done)
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
spawn_helper(
|
|
37
|
+
type: "explore",
|
|
38
|
+
prompt: "Find all auth-related files",
|
|
39
|
+
description: "Research auth files"
|
|
40
|
+
)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Background (returns immediately)
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
spawn_helper(
|
|
47
|
+
type: "work",
|
|
48
|
+
prompt: "Fix all lint errors in src/",
|
|
49
|
+
description: "Fix lint errors",
|
|
50
|
+
run_in_background: true
|
|
51
|
+
)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Check Background Result
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
get_helper_result(agent_id: "helper_abc123")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Options
|
|
61
|
+
|
|
62
|
+
| Parameter | Description |
|
|
63
|
+
|-----------|-------------|
|
|
64
|
+
| `type` | Agent type (`explore`, `work`, or custom) |
|
|
65
|
+
| `prompt` | Task for the agent |
|
|
66
|
+
| `description` | Short description (3-5 words) |
|
|
67
|
+
| `run_in_background` | Return immediately, notify on completion |
|
|
68
|
+
| `max_turns` | Max agentic turns before stopping |
|
|
69
|
+
| `model` | Model override (e.g. `"haiku"`, `"sonnet"`) |
|
|
70
|
+
| `thinking` | Thinking level (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`) |
|
|
71
|
+
|
|
72
|
+
## Custom Agent Types
|
|
73
|
+
|
|
74
|
+
Create markdown files defining agent behavior:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Global agents
|
|
78
|
+
~/.unipi/config/agents/reviewer.md
|
|
79
|
+
|
|
80
|
+
# Project agents
|
|
81
|
+
<workspace>/.unipi/config/agents/deployer.md
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Configuration
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
// ~/.unipi/config/subagents.json
|
|
88
|
+
{
|
|
89
|
+
"enabled": true,
|
|
90
|
+
"maxConcurrent": 3,
|
|
91
|
+
"types": {
|
|
92
|
+
"explore": { "enabled": true },
|
|
93
|
+
"work": { "enabled": true }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Features
|
|
99
|
+
|
|
100
|
+
- **Concurrent execution** — run up to N agents simultaneously
|
|
101
|
+
- **File locking** — transparent locking for parallel writes
|
|
102
|
+
- **ESC propagation** — kill all agents with ESC
|
|
103
|
+
- **Activity tracking** — real-time widget showing agent progress
|
|
104
|
+
- **Info screen integration** — agent status in dashboard
|
|
105
|
+
|
|
106
|
+
## Dependencies
|
|
107
|
+
|
|
108
|
+
- `@pi-unipi/core` — shared utilities
|
|
109
|
+
- `@pi-unipi/workflow` — workflow integration
|
|
110
|
+
- `@pi-unipi/info-screen` — dashboard registration
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/unipi — All-in-one extension entry
|
|
3
|
+
*
|
|
4
|
+
* Loads every Unipi module in a single entry point.
|
|
5
|
+
* Think of this as the "oh-my-zsh" for pi — one install mounts all modules.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* pi --no-extensions --no-skills -e packages/unipi/index.ts
|
|
9
|
+
* mise run unipi
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
13
|
+
|
|
14
|
+
import workflow from "@pi-unipi/workflow";
|
|
15
|
+
import ralph from "@pi-unipi/ralph";
|
|
16
|
+
import memory from "@pi-unipi/memory";
|
|
17
|
+
import infoScreen from "@pi-unipi/info-screen";
|
|
18
|
+
import subagents from "../subagents/src/index.js";
|
|
19
|
+
import btw from "@pi-unipi/btw/extensions/btw.js";
|
|
20
|
+
|
|
21
|
+
export default function (pi: ExtensionAPI) {
|
|
22
|
+
workflow(pi);
|
|
23
|
+
ralph(pi);
|
|
24
|
+
memory(pi);
|
|
25
|
+
infoScreen(pi);
|
|
26
|
+
subagents(pi);
|
|
27
|
+
btw(pi);
|
|
28
|
+
}
|