@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.
Files changed (31) hide show
  1. package/README.md +143 -0
  2. package/package.json +70 -0
  3. package/packages/btw/README.md +95 -0
  4. package/packages/btw/extensions/btw.ts +1972 -0
  5. package/packages/btw/skills/btw/SKILL.md +149 -0
  6. package/packages/core/README.md +74 -0
  7. package/packages/core/index.ts +10 -0
  8. package/packages/info-screen/README.md +115 -0
  9. package/packages/info-screen/index.ts +165 -0
  10. package/packages/memory/README.md +94 -0
  11. package/packages/memory/index.ts +209 -0
  12. package/packages/memory/skills/memory/SKILL.md +151 -0
  13. package/packages/ralph/README.md +101 -0
  14. package/packages/ralph/index.ts +548 -0
  15. package/packages/subagents/README.md +114 -0
  16. package/packages/unipi/index.ts +28 -0
  17. package/packages/workflow/README.md +129 -0
  18. package/packages/workflow/index.ts +155 -0
  19. package/packages/workflow/skills/brainstorm/SKILL.md +202 -0
  20. package/packages/workflow/skills/consolidate/SKILL.md +142 -0
  21. package/packages/workflow/skills/consultant/SKILL.md +97 -0
  22. package/packages/workflow/skills/document/SKILL.md +120 -0
  23. package/packages/workflow/skills/gather-context/SKILL.md +122 -0
  24. package/packages/workflow/skills/plan/SKILL.md +169 -0
  25. package/packages/workflow/skills/quick-work/SKILL.md +110 -0
  26. package/packages/workflow/skills/review-work/SKILL.md +131 -0
  27. package/packages/workflow/skills/scan-issues/SKILL.md +183 -0
  28. package/packages/workflow/skills/work/SKILL.md +144 -0
  29. package/packages/workflow/skills/worktree-create/SKILL.md +69 -0
  30. package/packages/workflow/skills/worktree-list/SKILL.md +67 -0
  31. 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
+ }