@pi-unipi/unipi 2.14.2 → 2.16.0

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 (47) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/package.json +23 -23
  3. package/packages/ask-user/package.json +2 -2
  4. package/packages/autocomplete/package.json +1 -1
  5. package/packages/background-tasks/package.json +2 -2
  6. package/packages/btw/package.json +2 -2
  7. package/packages/compactor/package.json +3 -3
  8. package/packages/core/package.json +1 -1
  9. package/packages/footer/README.md +3 -5
  10. package/packages/footer/package.json +3 -3
  11. package/packages/footer/src/glance-editor.ts +35 -66
  12. package/packages/footer/src/rendering/icons.ts +5 -0
  13. package/packages/footer/src/rendering/lolcat.ts +84 -0
  14. package/packages/footer/src/tui/settings-tui.ts +1 -1
  15. package/packages/image/package.json +2 -2
  16. package/packages/info-screen/package.json +2 -2
  17. package/packages/input-shortcuts/package.json +2 -2
  18. package/packages/kanboard/package.json +2 -2
  19. package/packages/mcp/package.json +2 -2
  20. package/packages/memory/index.ts +17 -0
  21. package/packages/memory/mempalace.ts +185 -0
  22. package/packages/memory/package.json +3 -3
  23. package/packages/memory/settings.ts +3 -0
  24. package/packages/memory/tui/settings-tui.ts +14 -0
  25. package/packages/milestone/package.json +2 -2
  26. package/packages/notify/README.md +16 -0
  27. package/packages/notify/activity.ts +100 -0
  28. package/packages/notify/events.ts +9 -1
  29. package/packages/notify/index.ts +20 -0
  30. package/packages/notify/package.json +2 -2
  31. package/packages/notify/settings.ts +21 -7
  32. package/packages/notify/skills/configure-notify/SKILL.md +23 -0
  33. package/packages/notify/tui/settings-overlay.ts +141 -19
  34. package/packages/notify/types.ts +12 -0
  35. package/packages/ralph/README.md +2 -0
  36. package/packages/ralph/SKILL.md +2 -0
  37. package/packages/ralph/completions.ts +170 -0
  38. package/packages/ralph/index.ts +3 -0
  39. package/packages/ralph/package.json +3 -3
  40. package/packages/subagents/package.json +2 -2
  41. package/packages/subagents/src/custom-agents.ts +22 -2
  42. package/packages/trajectory/package.json +1 -1
  43. package/packages/unipi/bundled.js +3472 -9650
  44. package/packages/updater/package.json +2 -2
  45. package/packages/utility/package.json +2 -2
  46. package/packages/web-api/package.json +2 -2
  47. package/packages/workflow/package.json +2 -2
@@ -14,12 +14,28 @@ import {
14
14
  validateConfig,
15
15
  } from "../settings.js";
16
16
  import { loadNtfyConfig, saveNtfyConfig, getNtfyConfigScope } from "../ntfy-config.js";
17
- import type { NotifyConfig, NtfyConfig } from "../types.js";
17
+ import type { NotifyConfig, NotifyPlatform, NtfyConfig } from "../types.js";
18
18
  import { OverlayTheme, boxInnerWidth } from "@pi-unipi/core";
19
19
 
20
20
  /** Section types */
21
21
  type Section = "platforms" | "events" | "recap";
22
22
 
23
+ const PLATFORM_KEYS: NotifyPlatform[] = ["native", "gotify", "telegram", "ntfy"];
24
+ const CHIP_LABELS: Record<NotifyPlatform, string> = {
25
+ native: "Native",
26
+ gotify: "Gotify",
27
+ telegram: "Telegram",
28
+ ntfy: "ntfy",
29
+ };
30
+
31
+ const SUPPRESS_FOCUSED_INDEX = 4;
32
+ const SILENCE_MASTER_INDEX = 5;
33
+ const SILENCE_CHIPS_INDEX = 6;
34
+
35
+ const WINDOW_STEP_MS = 1_000;
36
+ const WINDOW_MIN_MS = 1_000;
37
+ const WINDOW_MAX_MS = 120_000;
38
+
23
39
  /**
24
40
  * Settings overlay component.
25
41
  */
@@ -29,6 +45,8 @@ export class NotifySettingsOverlay implements Component {
29
45
  private ntfyScope: "project" | "global" | "none";
30
46
  private section: Section = "platforms";
31
47
  private selectedIndex = 0;
48
+ /** Which silence-after-input chip is focused (0–3). */
49
+ private chipIndex = 0;
32
50
  private error: string | null = null;
33
51
  private saved = false;
34
52
  onClose?: () => void;
@@ -70,6 +88,26 @@ export class NotifySettingsOverlay implements Component {
70
88
  this.selectedIndex = Math.min(this.maxItems - 1, this.selectedIndex + 1);
71
89
  return;
72
90
  }
91
+ if (this.section === "platforms" && this.selectedIndex === SILENCE_CHIPS_INDEX) {
92
+ if (matchesKey(data, "left") || data === "h") {
93
+ this.chipIndex = Math.max(0, this.chipIndex - 1);
94
+ return;
95
+ }
96
+ if (matchesKey(data, "right") || data === "l") {
97
+ this.chipIndex = Math.min(PLATFORM_KEYS.length - 1, this.chipIndex + 1);
98
+ return;
99
+ }
100
+ }
101
+ if (this.section === "platforms" && this.selectedIndex === SILENCE_MASTER_INDEX) {
102
+ if (data === "+" || data === "=") {
103
+ this.nudgeWindow(WINDOW_STEP_MS);
104
+ return;
105
+ }
106
+ if (data === "-" || data === "_") {
107
+ this.nudgeWindow(-WINDOW_STEP_MS);
108
+ return;
109
+ }
110
+ }
73
111
  if (matchesKey(data, "space")) {
74
112
  this.toggleCurrent();
75
113
  return;
@@ -99,30 +137,57 @@ export class NotifySettingsOverlay implements Component {
99
137
  }
100
138
 
101
139
  private get maxItems(): number {
102
- if (this.section === "platforms") return 5; // native, gotify, telegram, ntfy + suppress option
140
+ if (this.section === "platforms") return 7; // 4 platforms + focused + silence master + chips
103
141
  if (this.section === "recap") return 1; // toggle
104
142
  return Object.keys(this.config.events).length;
105
143
  }
106
144
 
145
+ private nudgeWindow(delta: number): void {
146
+ const current = this.config.silenceAfterInput.windowMs;
147
+ this.config.silenceAfterInput.windowMs = Math.min(
148
+ WINDOW_MAX_MS,
149
+ Math.max(WINDOW_MIN_MS, current + delta),
150
+ );
151
+ }
152
+
153
+ private chipOn(key: NotifyPlatform): boolean {
154
+ const listed = this.config.silenceAfterInput.platforms;
155
+ if (listed.length === 0) return true;
156
+ return listed.includes(key);
157
+ }
158
+
159
+ private toggleSilenceChip(key: NotifyPlatform): void {
160
+ const listed = this.config.silenceAfterInput.platforms;
161
+ const effective = listed.length === 0 ? PLATFORM_KEYS.slice() : listed.slice();
162
+ const idx = effective.indexOf(key);
163
+ if (idx >= 0) {
164
+ if (effective.length === 1) return;
165
+ effective.splice(idx, 1);
166
+ } else {
167
+ effective.push(key);
168
+ }
169
+ const ordered = PLATFORM_KEYS.filter((p) => effective.includes(p));
170
+ this.config.silenceAfterInput.platforms =
171
+ ordered.length === PLATFORM_KEYS.length ? [] : ordered;
172
+ }
173
+
107
174
  private toggleCurrent(): void {
108
175
  if (this.section === "platforms") {
109
- const platforms: Array<"native" | "gotify" | "telegram" | "ntfy"> = [
110
- "native",
111
- "gotify",
112
- "telegram",
113
- "ntfy",
114
- ];
115
- if (this.selectedIndex < platforms.length) {
116
- const key = platforms[this.selectedIndex];
176
+ if (this.selectedIndex < PLATFORM_KEYS.length) {
177
+ const key = PLATFORM_KEYS[this.selectedIndex];
117
178
  if (key === "ntfy") {
118
179
  // ntfy toggle updates the resolved ntfy config
119
180
  this.ntfyConfig.enabled = !this.ntfyConfig.enabled;
120
181
  } else if (key) {
121
182
  this.config[key].enabled = !this.config[key].enabled;
122
183
  }
123
- } else {
124
- // suppressWhenFocused toggle (index 4)
184
+ } else if (this.selectedIndex === SUPPRESS_FOCUSED_INDEX) {
125
185
  this.config.native.suppressWhenFocused = !this.config.native.suppressWhenFocused;
186
+ } else if (this.selectedIndex === SILENCE_MASTER_INDEX) {
187
+ this.config.silenceAfterInput.enabled = !this.config.silenceAfterInput.enabled;
188
+ } else if (this.selectedIndex === SILENCE_CHIPS_INDEX) {
189
+ const key = PLATFORM_KEYS[this.chipIndex];
190
+ if (key) this.toggleSilenceChip(key);
126
191
  }
127
192
  } else if (this.section === "recap") {
128
193
  this.config.recap.enabled = !this.config.recap.enabled;
@@ -196,18 +261,35 @@ export class NotifySettingsOverlay implements Component {
196
261
 
197
262
  // Footer
198
263
  lines.push(this.overlay.ruleLine(innerWidth));
199
- const footerHint = this.section === "recap"
200
- ? "↑↓ navigate · Space toggle · M change model · Tab switch · Enter save · Esc cancel"
201
- : "↑↓ navigate · Space toggle · Tab switch · Enter save · Esc cancel";
202
- lines.push(this.overlay.frameLine(this.overlay.fg("dim", footerHint), innerWidth));
264
+ lines.push(this.overlay.frameLine(this.overlay.fg("dim", this.footerHint()), innerWidth));
203
265
  lines.push(this.overlay.borderLine(innerWidth, "bottom"));
204
266
 
205
267
  return lines;
206
268
  }
207
269
 
270
+ private footerHint(): string {
271
+ if (this.section === "recap") {
272
+ return "↑↓ navigate · Space toggle · M change model · Tab switch · Enter save · Esc cancel";
273
+ }
274
+ if (this.section === "platforms" && this.selectedIndex === SILENCE_MASTER_INDEX) {
275
+ return "↑↓ navigate · Space toggle · +/− window · Tab switch · Enter save · Esc cancel";
276
+ }
277
+ if (this.section === "platforms" && this.selectedIndex === SILENCE_CHIPS_INDEX) {
278
+ return "↑↓ navigate · ←→ channel · Space toggle · Tab switch · Enter save · Esc cancel";
279
+ }
280
+ return "↑↓ navigate · Space toggle · Tab switch · Enter save · Esc cancel";
281
+ }
282
+
283
+ private silenceSummary(): string {
284
+ const seconds = Math.round(this.config.silenceAfterInput.windowMs / 1000);
285
+ const listed = this.config.silenceAfterInput.platforms;
286
+ const scope = listed.length === 0 ? "all enabled" : listed.join(", ");
287
+ return `${seconds}s · ${scope}`;
288
+ }
289
+
208
290
  private renderPlatforms(lines: string[], innerWidth: number): void {
209
291
  const platforms: Array<{
210
- key: "native" | "gotify" | "telegram" | "ntfy";
292
+ key: NotifyPlatform;
211
293
  label: string;
212
294
  detail: string;
213
295
  }> = [
@@ -259,8 +341,7 @@ export class NotifySettingsOverlay implements Component {
259
341
 
260
342
  // suppressWhenFocused toggle (index 4)
261
343
  {
262
- const i = platforms.length;
263
- const isSelected = i === this.selectedIndex;
344
+ const isSelected = this.selectedIndex === SUPPRESS_FOCUSED_INDEX;
264
345
  const isEnabled = this.config.native.suppressWhenFocused === true;
265
346
  const toggleOn = this.overlay.fg("success", "●");
266
347
  const toggleOff = this.overlay.fg("dim", "○");
@@ -277,6 +358,47 @@ export class NotifySettingsOverlay implements Component {
277
358
  )
278
359
  );
279
360
  }
361
+
362
+ this.renderSilenceAfterInput(lines, innerWidth);
363
+ }
364
+
365
+ private renderSilenceAfterInput(lines: string[], innerWidth: number): void {
366
+ const masterOn = this.config.silenceAfterInput.enabled;
367
+ const masterSelected = this.selectedIndex === SILENCE_MASTER_INDEX;
368
+ const toggle = masterOn
369
+ ? this.overlay.fg("success", "●")
370
+ : this.overlay.fg("dim", "○");
371
+ const label = masterSelected
372
+ ? this.overlay.bold("Quiet after activity")
373
+ : this.overlay.fg("dim", "Quiet after activity");
374
+ const detail = this.overlay.fg("dim", this.silenceSummary());
375
+ lines.push(
376
+ this.overlay.frameLine(
377
+ `${masterSelected ? this.overlay.fg("accent", "▸") : " "} ${toggle} ${label} ${detail}`,
378
+ innerWidth,
379
+ ),
380
+ );
381
+
382
+ const chipsSelected = this.selectedIndex === SILENCE_CHIPS_INDEX;
383
+ const chips = PLATFORM_KEYS.map((key, i) => {
384
+ const on = this.chipOn(key);
385
+ const mark = on ? "●" : "○";
386
+ const text = `${mark} ${CHIP_LABELS[key]}`;
387
+ const focused = chipsSelected && i === this.chipIndex;
388
+ if (focused) {
389
+ return this.overlay.fg("accent", this.overlay.bold(`[${text}]`));
390
+ }
391
+ const painted = on
392
+ ? `${this.overlay.fg("success", mark)} ${CHIP_LABELS[key]}`
393
+ : this.overlay.fg("dim", text);
394
+ return masterOn ? painted : this.overlay.fg("dim", text);
395
+ });
396
+ lines.push(
397
+ this.overlay.frameLine(
398
+ `${chipsSelected ? this.overlay.fg("accent", "▸") : " "} ${chips.join(" ")}`,
399
+ innerWidth,
400
+ ),
401
+ );
280
402
  }
281
403
 
282
404
  private renderEvents(lines: string[], innerWidth: number): void {
@@ -71,6 +71,16 @@ export interface RecapConfig {
71
71
  model: string;
72
72
  }
73
73
 
74
+ /** Quiet listed platforms after recent terminal input */
75
+ export interface SilenceAfterInputConfig {
76
+ /** Master switch */
77
+ enabled: boolean;
78
+ /** Quiet window after the last keypress, in milliseconds */
79
+ windowMs: number;
80
+ /** Platforms to suppress (empty = all enabled platforms) */
81
+ platforms: NotifyPlatform[];
82
+ }
83
+
74
84
  /** Full notification configuration */
75
85
  export interface NotifyConfig {
76
86
  /** Global default platforms for all events */
@@ -85,6 +95,8 @@ export interface NotifyConfig {
85
95
  telegram: TelegramConfig;
86
96
  /** Recap summarization settings */
87
97
  recap: RecapConfig;
98
+ /** Suppress listed platforms after recent terminal input */
99
+ silenceAfterInput: SilenceAfterInputConfig;
88
100
  }
89
101
 
90
102
  /** Parameters for the notify_user agent tool */
@@ -18,6 +18,8 @@ Ralph is for work that takes more than one pass — migrating a codebase, implem
18
18
  | `/unipi:ralph list --archived` | Show archived loops |
19
19
  | `/unipi:ralph nuke [--yes]` | Delete all ralph data |
20
20
 
21
+ Command arguments autocomplete: subcommands with descriptions, live loop names with status, and `start` flags are suggested as you type.
22
+
21
23
  ### Options
22
24
 
23
25
  | Flag | Description |
@@ -24,6 +24,8 @@ Long-running iterative development loops. Run complex tasks across multiple iter
24
24
  | `/unipi:ralph list --archived` | Show archived loops |
25
25
  | `/unipi:ralph nuke [--yes]` | Delete all ralph data |
26
26
 
27
+ Arguments autocomplete: typing `/unipi:ralph ` suggests subcommands with descriptions; `resume`/`cancel`/`archive` suggest loop names with live status; `start` suggests its flags.
28
+
27
29
  ## Tools
28
30
 
29
31
  | Tool | Description |
@@ -0,0 +1,170 @@
1
+ /**
2
+ * @unipi/ralph — Argument completions for /unipi:ralph
3
+ *
4
+ * Pure, testable completion builder. index.ts wires it into
5
+ * registerCommand via getArgumentCompletions.
6
+ *
7
+ * NOTE: pi passes the ENTIRE argument text (everything after the command
8
+ * name, up to the cursor) and replaces ALL of it with item.value on accept.
9
+ * Nested suggestions must therefore return the full replacement string,
10
+ * e.g. "resume myloop" or "start mytask --max-iterations".
11
+ */
12
+
13
+ import { RALPH_STATUS_ICONS } from "@pi-unipi/core";
14
+ import type { LoopStatus } from "./ralph-loop.js";
15
+
16
+ export interface CompletionItem {
17
+ value: string;
18
+ label: string;
19
+ description?: string;
20
+ }
21
+
22
+ /** Minimal loop shape the completion builder needs. */
23
+ export interface LoopSummary {
24
+ name: string;
25
+ status: LoopStatus;
26
+ iteration: number;
27
+ maxIterations: number;
28
+ }
29
+
30
+ /** Subcommands, shown when no subcommand has been typed yet. */
31
+ const SUBCOMMANDS: CompletionItem[] = [
32
+ { value: "start", label: "start", description: "Start a new loop" },
33
+ { value: "stop", label: "stop", description: "Stop the current loop" },
34
+ { value: "resume", label: "resume", description: "Resume a paused loop" },
35
+ { value: "status", label: "status", description: "Show all loops" },
36
+ { value: "list", label: "list", description: "Show loops (--archived for archived)" },
37
+ { value: "cancel", label: "cancel", description: "Delete loop state" },
38
+ { value: "archive", label: "archive", description: "Move a loop to the archive" },
39
+ {
40
+ value: "clean",
41
+ label: "clean",
42
+ description: "Clean completed loops (--all also deletes task files)",
43
+ },
44
+ { value: "nuke", label: "nuke", description: "Delete all ralph data (--yes skips confirm)" },
45
+ ];
46
+
47
+ /** Flags accepted by `start` that take a numeric value. */
48
+ const START_VALUE_FLAGS = ["--max-iterations", "--items-per-iteration", "--reflect-every"];
49
+
50
+ const START_FLAGS: CompletionItem[] = [
51
+ {
52
+ value: "--max-iterations",
53
+ label: "--max-iterations",
54
+ description: "Stop after N iterations (default 50)",
55
+ },
56
+ {
57
+ value: "--items-per-iteration",
58
+ label: "--items-per-iteration",
59
+ description: "Process N items per iteration",
60
+ },
61
+ {
62
+ value: "--reflect-every",
63
+ label: "--reflect-every",
64
+ description: "Reflection checkpoint every N iterations",
65
+ },
66
+ ];
67
+
68
+ function describeLoop(l: LoopSummary): string {
69
+ const maxStr = l.maxIterations > 0 ? `/${l.maxIterations}` : "";
70
+ const icon = RALPH_STATUS_ICONS[l.status] ?? "•";
71
+ return `${icon} ${l.status} · iter ${l.iteration}${maxStr}`;
72
+ }
73
+
74
+ function suggestLoopNames(
75
+ cmd: string,
76
+ partial: string,
77
+ listLoops: () => LoopSummary[],
78
+ ): CompletionItem[] {
79
+ return listLoops()
80
+ .filter((l) => {
81
+ if (cmd === "resume") return l.status !== "completed";
82
+ if (cmd === "archive") return l.status !== "active";
83
+ return true; // cancel: any non-archived loop
84
+ })
85
+ .filter((l) => l.name.startsWith(partial) && l.name !== partial)
86
+ .sort((a, b) => a.name.localeCompare(b.name))
87
+ .map((l) => ({
88
+ value: `${cmd} ${l.name}`,
89
+ label: l.name,
90
+ description: describeLoop(l),
91
+ }));
92
+ }
93
+
94
+ /**
95
+ * Build completion items for the argument text after `/unipi:ralph`.
96
+ * `listLoops` supplies non-archived loop state (empty before session_start).
97
+ * Returns null when there is nothing to suggest.
98
+ */
99
+ export function buildRalphArgumentCompletions(
100
+ argumentText: string,
101
+ listLoops: () => LoopSummary[],
102
+ ): CompletionItem[] | null {
103
+ const endsWithSpace = /\s$/.test(argumentText);
104
+ const tokens = argumentText.trim().split(/\s+/).filter((t) => t.length > 0);
105
+ const complete = endsWithSpace ? tokens : tokens.slice(0, -1);
106
+ const partial = endsWithSpace ? "" : (tokens[tokens.length - 1] ?? "");
107
+ // Everything typed before the in-flight token — every suggestion must
108
+ // preserve it, because pi replaces the whole argument text with item.value.
109
+ const head = endsWithSpace
110
+ ? argumentText
111
+ : argumentText.slice(0, argumentText.length - partial.length);
112
+
113
+ // No subcommand yet → suggest subcommands
114
+ if (complete.length === 0) {
115
+ const items = SUBCOMMANDS.filter((s) => s.value.startsWith(partial) && s.value !== partial).map(
116
+ (s) => ({ ...s, value: head + s.value }),
117
+ );
118
+ return items.length > 0 ? items : null;
119
+ }
120
+
121
+ const cmd = complete[0];
122
+
123
+ // Typing the numeric value of a start flag → no suggestions
124
+ if (START_VALUE_FLAGS.includes(complete[complete.length - 1])) return null;
125
+
126
+ switch (cmd) {
127
+ case "resume":
128
+ case "cancel":
129
+ case "archive": {
130
+ if (complete.length > 1) return null; // name chosen, nothing more
131
+ const items = suggestLoopNames(cmd, partial, listLoops).map((i) => ({
132
+ ...i,
133
+ value: head + i.label,
134
+ }));
135
+ return items.length > 0 ? items : null;
136
+ }
137
+
138
+ case "start": {
139
+ if (complete.length === 1) return null; // loop name is free-form
140
+ const used = complete.slice(1).filter((t) => t.startsWith("--"));
141
+ const items = START_FLAGS.filter(
142
+ (f) => !used.includes(f.value) && f.value.startsWith(partial) && f.value !== partial,
143
+ ).map((f) => ({
144
+ value: head + f.value,
145
+ label: f.label,
146
+ description: f.description,
147
+ }));
148
+ return items.length > 0 ? items : null;
149
+ }
150
+
151
+ case "list":
152
+ case "clean":
153
+ case "nuke": {
154
+ const flag = cmd === "list" ? "--archived" : cmd === "clean" ? "--all" : "--yes";
155
+ const desc =
156
+ cmd === "list"
157
+ ? "Show archived loops"
158
+ : cmd === "clean"
159
+ ? "Also delete task files"
160
+ : "Skip confirmation prompt";
161
+ if (complete.slice(1).includes(flag) || flag === partial || !flag.startsWith(partial)) {
162
+ return null;
163
+ }
164
+ return [{ value: head + flag, label: flag, description: desc }];
165
+ }
166
+
167
+ default:
168
+ return null; // stop / status take no arguments
169
+ }
170
+ }
@@ -24,6 +24,7 @@ function getInfoRegistry() {
24
24
  }
25
25
  import { RalphLoopManager } from "./ralph-loop.js";
26
26
  import { registerRalphTools } from "./tools.js";
27
+ import { buildRalphArgumentCompletions } from "./completions.js";
27
28
 
28
29
  /** Package version */
29
30
  const VERSION = getPackageVersion(dirname(fileURLToPath(import.meta.url)));
@@ -200,6 +201,8 @@ To stop: press ESC to interrupt, then run /unipi:ralph-stop when idle`;
200
201
 
201
202
  pi.registerCommand("unipi:ralph", {
202
203
  description: "Ralph loop commands (start, stop, resume, status, etc.)",
204
+ getArgumentCompletions: (argumentText: string) =>
205
+ buildRalphArgumentCompletions(argumentText, () => manager?.listLoops(false) ?? []),
203
206
  handler: async (args, ctx) => {
204
207
  const parts = parseArgs(args);
205
208
  const cmd = parts[0];
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/ralph",
3
- "version": "2.14.2",
3
+ "version": "2.16.0",
4
4
  "description": "Long-running iterative development loops for Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -31,8 +31,8 @@
31
31
  "test": "npx tsx --test reminder.test.ts"
32
32
  },
33
33
  "dependencies": {
34
- "@pi-unipi/core": "2.14.2",
35
- "@pi-unipi/info-screen": "2.14.2"
34
+ "@pi-unipi/core": "2.16.0",
35
+ "@pi-unipi/info-screen": "2.16.0"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@earendil-works/pi-ai": "^0.84.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/subagents",
3
- "version": "2.14.2",
3
+ "version": "2.16.0",
4
4
  "description": "Subagents for UniPi — parallel execution, file locking, workflow integration",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -9,7 +9,7 @@
9
9
  "test": "npx tsx --test src/__tests__/*.test.ts"
10
10
  },
11
11
  "dependencies": {
12
- "@pi-unipi/core": "2.14.2",
12
+ "@pi-unipi/core": "2.16.0",
13
13
  "@earendil-works/pi-agent-core": "^0.84.0",
14
14
  "acorn": "8.18.0"
15
15
  },
@@ -151,8 +151,28 @@ function listAgentFilesRecursive(dir: string): string[] {
151
151
  return files;
152
152
  }
153
153
 
154
- /** Builtin agent definition files shipped in packages/subagents/agents/. */
155
- const BUILTIN_AGENTS_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "agents");
154
+ /**
155
+ * Builtin agent definition files shipped in packages/subagents/agents/.
156
+ *
157
+ * Layout-aware (issue #35): when loaded from source (packages/subagents/src/)
158
+ * the agents dir is one level up; when loaded from the umbrella bundle
159
+ * (packages/unipi/bundled.js — the file npm ships and pi actually executes)
160
+ * `import.meta.url` points at packages/unipi/, so the files are two levels up
161
+ * under ../subagents/agents. The first candidate that exists wins, so both
162
+ * layouts resolve without build-time path injection.
163
+ */
164
+ export function resolveBuiltinAgentsDir(fromUrl: string = import.meta.url): string {
165
+ const here = dirname(fileURLToPath(fromUrl));
166
+ for (const candidate of [
167
+ join(here, "..", "agents"), // source layout: src/ → agents/
168
+ join(here, "..", "subagents", "agents"), // bundle layout: packages/unipi/bundled.js → packages/subagents/agents
169
+ ]) {
170
+ if (existsSync(candidate)) return candidate;
171
+ }
172
+ return join(here, "..", "agents");
173
+ }
174
+
175
+ const BUILTIN_AGENTS_DIR = resolveBuiltinAgentsDir();
156
176
 
157
177
  /** Frontmatter fields stored as-is on the config for later phases. */
158
178
  function collectExtraFields(frontmatter: Record<string, unknown>): Record<string, string> | undefined {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/trajectory",
3
- "version": "2.14.2",
3
+ "version": "2.16.0",
4
4
  "description": "Live UniPi trajectory inspector for Pi sessions",
5
5
  "type": "module",
6
6
  "main": "index.ts",