@ilya-lesikov/pi-pi 0.9.0 → 0.10.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 (106) hide show
  1. package/3p/pi-ask-user/index.ts +71 -124
  2. package/3p/pi-ask-user/single-select-layout.ts +3 -14
  3. package/3p/pi-subagents/package.json +13 -7
  4. package/3p/pi-subagents/src/agent-manager.ts +243 -79
  5. package/3p/pi-subagents/src/agent-runner.ts +501 -365
  6. package/3p/pi-subagents/src/agent-types.ts +46 -57
  7. package/3p/pi-subagents/src/cross-extension-rpc.ts +52 -46
  8. package/3p/pi-subagents/src/custom-agents.ts +30 -6
  9. package/3p/pi-subagents/src/default-agents.ts +25 -43
  10. package/3p/pi-subagents/src/enabled-models.ts +180 -0
  11. package/3p/pi-subagents/src/index.ts +599 -839
  12. package/3p/pi-subagents/src/model-resolver.ts +26 -7
  13. package/3p/pi-subagents/src/output-file.ts +21 -2
  14. package/3p/pi-subagents/src/prompts.ts +17 -3
  15. package/3p/pi-subagents/src/schedule-store.ts +153 -0
  16. package/3p/pi-subagents/src/schedule.ts +365 -0
  17. package/3p/pi-subagents/src/settings.ts +261 -0
  18. package/3p/pi-subagents/src/skill-loader.ts +77 -54
  19. package/3p/pi-subagents/src/status-note.ts +25 -0
  20. package/3p/pi-subagents/src/types.ts +97 -6
  21. package/3p/pi-subagents/src/ui/agent-widget.ts +94 -21
  22. package/3p/pi-subagents/src/ui/conversation-viewer.ts +147 -35
  23. package/3p/pi-subagents/src/ui/schedule-menu.ts +104 -0
  24. package/3p/pi-subagents/src/ui/viewer-keys.ts +39 -0
  25. package/3p/pi-subagents/src/usage.ts +60 -0
  26. package/3p/pi-subagents/src/worktree.ts +49 -20
  27. package/extensions/orchestrator/agents/advisor.ts +13 -8
  28. package/extensions/orchestrator/agents/brainstorm-reviewer.ts +8 -3
  29. package/extensions/orchestrator/agents/code-reviewer.ts +8 -3
  30. package/extensions/orchestrator/agents/constraints.test.ts +26 -0
  31. package/extensions/orchestrator/agents/constraints.ts +12 -2
  32. package/extensions/orchestrator/agents/deep-debugger.ts +13 -8
  33. package/extensions/orchestrator/agents/explore.ts +12 -6
  34. package/extensions/orchestrator/agents/librarian.ts +13 -5
  35. package/extensions/orchestrator/agents/plan-reviewer.ts +8 -3
  36. package/extensions/orchestrator/agents/planner.ts +8 -3
  37. package/extensions/orchestrator/agents/pools.test.ts +56 -0
  38. package/extensions/orchestrator/agents/prompts.test.ts +52 -10
  39. package/extensions/orchestrator/agents/registry.test.ts +245 -0
  40. package/extensions/orchestrator/agents/registry.ts +145 -10
  41. package/extensions/orchestrator/agents/reviewer.ts +14 -8
  42. package/extensions/orchestrator/agents/task.ts +12 -6
  43. package/extensions/orchestrator/agents/tool-routing.ts +213 -85
  44. package/extensions/orchestrator/agents/wait-for-completion.test.ts +171 -0
  45. package/extensions/orchestrator/ast-search.test.ts +124 -0
  46. package/extensions/orchestrator/billing-spoof.test.ts +67 -0
  47. package/extensions/orchestrator/billing-spoof.ts +95 -0
  48. package/extensions/orchestrator/cbm.more.test.ts +358 -0
  49. package/extensions/orchestrator/command-handlers.ts +6 -0
  50. package/extensions/orchestrator/commands.test.ts +15 -2
  51. package/extensions/orchestrator/commands.ts +1 -1
  52. package/extensions/orchestrator/config.test.ts +89 -1
  53. package/extensions/orchestrator/config.ts +102 -19
  54. package/extensions/orchestrator/context.test.ts +46 -0
  55. package/extensions/orchestrator/context.ts +18 -5
  56. package/extensions/orchestrator/custom-footer.test.ts +24 -10
  57. package/extensions/orchestrator/custom-footer.ts +4 -2
  58. package/extensions/orchestrator/doctor.more.test.ts +315 -0
  59. package/extensions/orchestrator/doctor.ts +6 -2
  60. package/extensions/orchestrator/event-handlers.more.test.ts +561 -0
  61. package/extensions/orchestrator/event-handlers.test.ts +96 -9
  62. package/extensions/orchestrator/event-handlers.ts +344 -151
  63. package/extensions/orchestrator/exa.more.test.ts +118 -0
  64. package/extensions/orchestrator/flant-infra.more.test.ts +326 -0
  65. package/extensions/orchestrator/flant-infra.test.ts +127 -0
  66. package/extensions/orchestrator/flant-infra.ts +113 -39
  67. package/extensions/orchestrator/index.test.ts +76 -0
  68. package/extensions/orchestrator/index.ts +2 -0
  69. package/extensions/orchestrator/integration.test.ts +183 -65
  70. package/extensions/orchestrator/model-registry.test.ts +2 -1
  71. package/extensions/orchestrator/model-registry.ts +12 -2
  72. package/extensions/orchestrator/orchestrator.test.ts +67 -0
  73. package/extensions/orchestrator/orchestrator.ts +119 -27
  74. package/extensions/orchestrator/phases/brainstorm.test.ts +30 -1
  75. package/extensions/orchestrator/phases/brainstorm.ts +43 -6
  76. package/extensions/orchestrator/phases/implementation.ts +2 -0
  77. package/extensions/orchestrator/phases/machine.test.ts +17 -1
  78. package/extensions/orchestrator/phases/machine.ts +4 -1
  79. package/extensions/orchestrator/phases/planning.test.ts +47 -1
  80. package/extensions/orchestrator/phases/planning.ts +18 -3
  81. package/extensions/orchestrator/phases/review-task.ts +5 -0
  82. package/extensions/orchestrator/phases/review.test.ts +10 -0
  83. package/extensions/orchestrator/phases/review.ts +22 -7
  84. package/extensions/orchestrator/phases/spawn-blocking.test.ts +1 -0
  85. package/extensions/orchestrator/phases/verdict.ts +6 -5
  86. package/extensions/orchestrator/plannotator-open-failure.test.ts +67 -0
  87. package/extensions/orchestrator/plannotator.test.ts +38 -1
  88. package/extensions/orchestrator/plannotator.ts +50 -3
  89. package/extensions/orchestrator/pp-menu.leaves.test.ts +590 -0
  90. package/extensions/orchestrator/pp-menu.more.test.ts +524 -0
  91. package/extensions/orchestrator/pp-menu.test.ts +114 -7
  92. package/extensions/orchestrator/pp-menu.ts +579 -90
  93. package/extensions/orchestrator/pp-state-tools.test.ts +80 -0
  94. package/extensions/orchestrator/pp-state-tools.ts +65 -0
  95. package/extensions/orchestrator/rate-limit-fallback.more.test.ts +241 -0
  96. package/extensions/orchestrator/rate-limit-fallback.test.ts +20 -1
  97. package/extensions/orchestrator/rate-limit-fallback.ts +12 -0
  98. package/extensions/orchestrator/review-files.test.ts +26 -0
  99. package/extensions/orchestrator/review-files.ts +3 -0
  100. package/extensions/orchestrator/state.test.ts +73 -1
  101. package/extensions/orchestrator/state.ts +95 -23
  102. package/extensions/orchestrator/suppress-pierre-theme-spam.test.ts +56 -0
  103. package/extensions/orchestrator/suppress-pierre-theme-spam.ts +45 -0
  104. package/extensions/orchestrator/validate-artifacts.ts +1 -1
  105. package/package.json +6 -2
  106. package/scripts/test-3p.sh +52 -0
@@ -56,6 +56,20 @@ export interface TaskState {
56
56
  mode?: TaskMode;
57
57
  effectiveMode?: TaskMode;
58
58
  autonomousConfig?: AutonomousConfig;
59
+ // Manual auto-review-until-approved (#5/#7): drives the same review-cycle loop
60
+ // as autonomous mode over ONE phase, independently of effectiveMode (so it
61
+ // works in force-guided brainstorm/debug/review). advanceOnComplete=false
62
+ // (item 5) stops in-phase on approve/max-passes; =true (item 7) finalizes and
63
+ // transitions to the next phase. deferredAdvance carries the next-phase inputs
64
+ // the plain-continue path would have collected, captured up front because the
65
+ // headless pp_phase_complete branch cannot prompt.
66
+ manualAutoReview?: {
67
+ phase: string;
68
+ preset: string;
69
+ maxPasses: number;
70
+ advanceOnComplete: boolean;
71
+ deferredAdvance?: { mode?: TaskMode; autonomousConfig?: AutonomousConfig; plannerPreset?: string };
72
+ };
59
73
  plannerFailureAutoRetried?: boolean;
60
74
  reviewerFailureAutoRetried?: boolean;
61
75
  // Set once afterImplement has run for the current implement phase (either at
@@ -66,6 +80,10 @@ export interface TaskState {
66
80
  // set of repos to review; index is the next repo. Persisted so the loop resumes
67
81
  // on the next /pp after the agent fixes one repo's feedback. Undefined = no loop.
68
82
  plannotatorCursor?: { repoPaths: string[]; index: number };
83
+ // The phase the task was in when it was marked done. Recorded so the Resume
84
+ // "reopen a done task" flow (#2) can restore the actual last working phase
85
+ // (done itself carries no phase history). Absent on legacy done tasks.
86
+ completedFrom?: Phase;
69
87
  }
70
88
 
71
89
  export interface TaskInfo {
@@ -174,11 +192,18 @@ export function saveTask(taskDir: string, state: TaskState): void {
174
192
  writeFileSync(taskStatePath(taskDir), JSON.stringify(state, null, 2) + "\n", "utf-8");
175
193
  }
176
194
 
177
- export function listTasks(cwd: string, type?: TaskType): TaskInfo[] {
195
+ export interface ListTasksOptions {
196
+ type?: TaskType;
197
+ includeDone?: boolean;
198
+ }
199
+
200
+ export function listTasks(cwd: string, typeOrOptions?: TaskType | ListTasksOptions): TaskInfo[] {
201
+ const options: ListTasksOptions =
202
+ typeof typeOrOptions === "string" ? { type: typeOrOptions } : typeOrOptions ?? {};
178
203
  const base = stateDir(cwd);
179
204
  if (!existsSync(base)) return [];
180
205
 
181
- const types: TaskType[] = type ? [type] : ["implement", "debug", "brainstorm", "review", "quick"];
206
+ const types: TaskType[] = options.type ? [options.type] : ["implement", "debug", "brainstorm", "review", "quick"];
182
207
  const results: TaskInfo[] = [];
183
208
 
184
209
  for (const t of types) {
@@ -193,7 +218,7 @@ export function listTasks(cwd: string, type?: TaskType): TaskInfo[] {
193
218
 
194
219
  try {
195
220
  const state = loadTask(dir);
196
- if (state.phase !== "done") {
221
+ if (state.phase !== "done" || options.includeDone) {
197
222
  results.push({ dir, state, type: t });
198
223
  }
199
224
  } catch {
@@ -229,9 +254,9 @@ export async function lockTask(taskDir: string, timeouts: TimeoutConfig): Promis
229
254
  return release;
230
255
  }
231
256
 
232
- export function getActiveTask(cwd: string, lockStale?: number): TaskInfo | null {
257
+ export function getUnlockedTasks(cwd: string, lockStale?: number): TaskInfo[] {
233
258
  const tasks = listTasks(cwd);
234
- if (tasks.length === 0) return null;
259
+ if (tasks.length === 0) return [];
235
260
 
236
261
  const stale = lockStale ?? 600000;
237
262
  const unlocked: TaskInfo[] = [];
@@ -245,9 +270,24 @@ export function getActiveTask(cwd: string, lockStale?: number): TaskInfo | null
245
270
  getLogger().warn({ s: "state", dir: task.dir }, "failed to check lock");
246
271
  }
247
272
  }
273
+ return unlocked;
274
+ }
248
275
 
249
- if (unlocked.length !== 1) return null;
250
- return unlocked[0];
276
+ export type ActiveTaskStatus =
277
+ | { kind: "none" }
278
+ | { kind: "single"; task: TaskInfo }
279
+ | { kind: "ambiguous"; tasks: TaskInfo[] };
280
+
281
+ export function getActiveTaskStatus(cwd: string, lockStale?: number): ActiveTaskStatus {
282
+ const unlocked = getUnlockedTasks(cwd, lockStale);
283
+ if (unlocked.length === 0) return { kind: "none" };
284
+ if (unlocked.length === 1) return { kind: "single", task: unlocked[0]! };
285
+ return { kind: "ambiguous", tasks: unlocked };
286
+ }
287
+
288
+ export function getActiveTask(cwd: string, lockStale?: number): TaskInfo | null {
289
+ const status = getActiveTaskStatus(cwd, lockStale);
290
+ return status.kind === "single" ? status.task : null;
251
291
  }
252
292
 
253
293
  export function validateFromPath(cwd: string, fromPath: string): { ok: true; dir: string } | { ok: false; reason: string } {
@@ -284,28 +324,60 @@ export function taskName(taskDir: string): string {
284
324
  }
285
325
  }
286
326
 
287
- // Same as taskName but reuses an already-loaded TaskState (avoids re-reading
288
- // state.json per resume-menu entry). The only per-entry disk read is the
289
- // USER_REQUEST.md fallback, which is unavoidable for generic descriptions.
290
- export function taskNameFromState(taskDir: string, state: TaskState): string {
327
+ // Non-heading, non-empty content of a markdown file, collapsed onto one line
328
+ // (multiple content lines joined with a space), or null. Spanning multiple
329
+ // lines lets a multi-line request survive the later truncate-with-ellipsis
330
+ // instead of being cut off at the first content line. Bounded so a large file
331
+ // (e.g. RESEARCH.md) never builds an unbounded string.
332
+ function firstMarkdownContent(path: string): string | null {
333
+ if (!existsSync(path)) return null;
334
+ const content = readFileSync(path, "utf-8");
335
+ const lines = content.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
336
+ if (lines.length === 0) return null;
337
+ return lines.join(" ").slice(0, 700);
338
+ }
339
+
340
+ // The full, untrimmed task name (used for the Resume right-pane description).
341
+ // Resolves a real intent for generic-description tasks via a fallback chain:
342
+ // USER_REQUEST.md → RESEARCH.md → first artifact → dir slug.
343
+ export function taskFullName(taskDir: string, state: TaskState): string {
291
344
  let desc = state.description ?? "";
292
345
 
293
346
  if (["implement", "debug", "brainstorm", "review", "quick"].includes(desc)) {
294
- const urPath = join(taskDir, "USER_REQUEST.md");
295
- if (existsSync(urPath)) {
296
- const content = readFileSync(urPath, "utf-8");
297
- const lines = content.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
298
- const firstContent = lines.find((l) => !l.startsWith("#"));
299
- if (firstContent) desc = firstContent;
300
- }
347
+ const fallback =
348
+ firstMarkdownContent(join(taskDir, "USER_REQUEST.md")) ??
349
+ firstMarkdownContent(join(taskDir, "RESEARCH.md")) ??
350
+ firstArtifactTitle(taskDir);
351
+ if (fallback) desc = fallback;
301
352
  }
302
353
 
303
- if (desc) {
304
- desc = desc.replace(/\s+/g, " ").trim();
305
- if (desc.length > 60) desc = desc.slice(0, 57) + "...";
306
- return desc;
354
+ desc = desc.replace(/\s+/g, " ").trim();
355
+ return desc || dirSlugName(taskDir);
356
+ }
357
+
358
+ function firstArtifactTitle(taskDir: string): string | null {
359
+ const artifactsDir = join(taskDir, "artifacts");
360
+ if (!existsSync(artifactsDir)) return null;
361
+ const files = readdirSync(artifactsDir).filter((f) => f.endsWith(".md")).sort();
362
+ for (const file of files) {
363
+ const content = readFileSync(join(artifactsDir, file), "utf-8");
364
+ const lines = content.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
365
+ const heading = lines.find((l) => l.startsWith("# "));
366
+ if (heading) return heading.replace(/^#\s+/, "");
367
+ const firstContent = lines.find((l) => !l.startsWith("#"));
368
+ if (firstContent) return firstContent;
307
369
  }
308
- return dirSlugName(taskDir);
370
+ return null;
371
+ }
372
+
373
+ // Same as taskName but reuses an already-loaded TaskState (avoids re-reading
374
+ // state.json per resume-menu entry). Returns a TITLE trimmed to fit one line.
375
+ // The cap is conservative (fits within ~80 cols after the `→ N. ` selection
376
+ // prefix and a ` — <age>` suffix are added) so a Resume item never wraps.
377
+ export function taskNameFromState(taskDir: string, state: TaskState): string {
378
+ const desc = taskFullName(taskDir, state);
379
+ if (desc.length > 60) return desc.slice(0, 57) + "...";
380
+ return desc;
309
381
  }
310
382
 
311
383
  function dirSlugName(taskDir: string): string {
@@ -0,0 +1,56 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { suppressPierreThemeSpam } from "./suppress-pierre-theme-spam.js";
3
+
4
+ const INSTALLED_KEY = Symbol.for("pi-pi:pierre-theme-spam-suppressed");
5
+ const DUP = "SharedHighlight.registerCustomTheme: theme name already registered";
6
+
7
+ describe("suppressPierreThemeSpam", () => {
8
+ let original: typeof console.error;
9
+
10
+ beforeEach(() => {
11
+ original = console.error;
12
+ (globalThis as any)[INSTALLED_KEY] = undefined;
13
+ });
14
+
15
+ afterEach(() => {
16
+ console.error = original;
17
+ (globalThis as any)[INSTALLED_KEY] = undefined;
18
+ });
19
+
20
+ it("drops the benign duplicate-theme error for each known pierre theme", () => {
21
+ const spy = vi.fn();
22
+ console.error = spy;
23
+ suppressPierreThemeSpam();
24
+
25
+ for (const name of ["pierre-dark", "pierre-dark-soft", "pierre-light", "pierre-light-soft"]) {
26
+ console.error(DUP, name);
27
+ }
28
+ expect(spy).not.toHaveBeenCalled();
29
+ });
30
+
31
+ it("passes through unrelated console.error calls untouched", () => {
32
+ const spy = vi.fn();
33
+ console.error = spy;
34
+ suppressPierreThemeSpam();
35
+
36
+ console.error("something else entirely");
37
+ console.error(DUP, "some-other-theme"); // same prefix, unknown theme → not ours
38
+ console.error("real error", new Error("boom"));
39
+
40
+ expect(spy).toHaveBeenCalledTimes(3);
41
+ expect(spy).toHaveBeenNthCalledWith(1, "something else entirely");
42
+ expect(spy).toHaveBeenNthCalledWith(2, DUP, "some-other-theme");
43
+ });
44
+
45
+ it("installs the filter only once (idempotent, no wrapper stacking)", () => {
46
+ const spy = vi.fn();
47
+ console.error = spy;
48
+ suppressPierreThemeSpam();
49
+ const afterFirst = console.error;
50
+ suppressPierreThemeSpam();
51
+ expect(console.error).toBe(afterFirst);
52
+
53
+ console.error("passthrough");
54
+ expect(spy).toHaveBeenCalledTimes(1);
55
+ });
56
+ });
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Silences a benign, TUI-corrupting `console.error` from `@pierre/diffs`.
3
+ *
4
+ * `@pierre/diffs/dist/highlighter/shared_highlighter.js` registers four custom
5
+ * themes (`pierre-dark`, `pierre-dark-soft`, `pierre-light`, `pierre-light-soft`)
6
+ * as an import-time side effect, against a module-singleton Map. When that module
7
+ * graph is evaluated more than once — which it is here, because pi loads
8
+ * extensions through jiti with `moduleCache: false` and the /pp menu is pulled in
9
+ * via a dynamic `import()` — each re-registration trips a
10
+ * `console.error("SharedHighlight.registerCustomTheme: theme name already registered", name)`.
11
+ *
12
+ * pi takes over stdout during the interactive TUI (its output-guard reroutes
13
+ * stdout→stderr) but does not guard stderr, so that stray error visibly corrupts
14
+ * the rendered UI (seen when opening/closing /pp).
15
+ *
16
+ * We can't dedupe the registration (jiti's cache is off) and there's no
17
+ * patch-package infra in this repo, so we drop exactly this one third-party
18
+ * message and delegate everything else untouched. Installed once, process-wide.
19
+ */
20
+
21
+ const DUP_THEME_PREFIX = "SharedHighlight.registerCustomTheme: theme name already registered";
22
+ const PIERRE_THEME_NAMES = new Set(["pierre-dark", "pierre-dark-soft", "pierre-light", "pierre-light-soft"]);
23
+
24
+ // Marked on globalThis rather than a module-local flag: pi loads extensions via
25
+ // jiti with `moduleCache: false`, so this module can be re-evaluated (resetting
26
+ // module scope) — without a process-wide marker we'd stack console.error wrappers.
27
+ const INSTALLED_KEY = Symbol.for("pi-pi:pierre-theme-spam-suppressed");
28
+
29
+ export function suppressPierreThemeSpam(): void {
30
+ if ((globalThis as any)[INSTALLED_KEY]) return;
31
+ (globalThis as any)[INSTALLED_KEY] = true;
32
+
33
+ const original = console.error.bind(console);
34
+ console.error = (...args: unknown[]) => {
35
+ if (
36
+ args.length >= 2 &&
37
+ args[0] === DUP_THEME_PREFIX &&
38
+ typeof args[1] === "string" &&
39
+ PIERRE_THEME_NAMES.has(args[1])
40
+ ) {
41
+ return;
42
+ }
43
+ original(...args);
44
+ };
45
+ }
@@ -5,7 +5,7 @@ const USER_REQUEST_ALLOWED_SECTIONS = ["Problem", "Constraints"] as const;
5
5
  const RESEARCH_REQUIRED_SECTIONS = ["Affected Code", "Architecture Context", "Constraints & Edge Cases"] as const;
6
6
  const RESEARCH_ALLOWED_SECTIONS = ["Affected Code", "Architecture Context", "Constraints & Edge Cases", "Open Questions"] as const;
7
7
  const PLAN_REQUIRED_SECTIONS = ["Scope", "Checklist"] as const;
8
- const PLAN_ALLOWED_SECTIONS = ["Scope", "Checklist", "Pattern constraints", "Blockers"] as const;
8
+ export const PLAN_ALLOWED_SECTIONS = ["Scope", "Checklist", "Pattern constraints", "Blockers"] as const;
9
9
  const PLACEHOLDER_PATTERNS = /^(?:[-*.…—]|tbd|todo|n\/a|na|none|\.{2,})$/i;
10
10
 
11
11
  function splitLines(content: string): string[] {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ilya-lesikov/pi-pi",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Pi-Pi Coding Agent: based on Pi Coding Agent, but twice as good",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -35,7 +35,10 @@
35
35
  "scripts": {
36
36
  "postinstall": "bash scripts/postinstall.sh",
37
37
  "test": "vitest run --exclude '3p/**'",
38
- "test:watch": "vitest --exclude '3p/**'"
38
+ "test:watch": "vitest --exclude '3p/**'",
39
+ "test:3p": "bash scripts/test-3p.sh",
40
+ "test:all": "vitest run --exclude '3p/**' && bash scripts/test-3p.sh",
41
+ "coverage": "vitest run --exclude '3p/**' --coverage"
39
42
  },
40
43
  "pi": {
41
44
  "extensions": [
@@ -68,6 +71,7 @@
68
71
  "@earendil-works/pi-coding-agent": "^0.75.5",
69
72
  "@earendil-works/pi-tui": "^0.75.5",
70
73
  "@sinclair/typebox": "^0.34.0",
74
+ "@vitest/coverage-v8": "^4.1.10",
71
75
  "typescript": "^5.8.0",
72
76
  "vitest": "^4.1.4"
73
77
  }
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bash
2
+ # Aggregates the test suites of the vendored 3p/ extensions, each run with its OWN native runner
3
+ # (vitest for pi-subagents/pi-tasks, bun for pi-lsp/pi-ask-user/pi-plannotator). The root
4
+ # `npm test` excludes 3p/**, so without this the fork-delta tests never gate CI.
5
+ #
6
+ # Exit non-zero if any package's suite fails. Runs every package even if an earlier one fails, so
7
+ # one invocation reports all breakage.
8
+ set -uo pipefail
9
+ cd "$(dirname "$0")/.."
10
+ ROOT="$(pwd)"
11
+
12
+ if ! command -v bun >/dev/null 2>&1; then
13
+ echo "ERROR: 'bun' is not on PATH — pi-lsp, pi-ask-user and pi-plannotator gate on it." >&2
14
+ echo "Install bun (https://bun.sh) and re-run, or run the vitest 3p suites individually." >&2
15
+ exit 127
16
+ fi
17
+
18
+ failures=()
19
+
20
+ run() {
21
+ local name="$1"; shift
22
+ local dir="$1"; shift
23
+ echo ""
24
+ echo "──────────────────────────────────────────────"
25
+ echo "▶ 3p/$name ($*)"
26
+ echo "──────────────────────────────────────────────"
27
+ ( cd "$ROOT/3p/$dir" && "$@" )
28
+ if [ $? -ne 0 ]; then
29
+ failures+=("$name")
30
+ fi
31
+ }
32
+
33
+ # vitest packages (use the package's own vitest.config.ts via npx vitest run).
34
+ run "pi-subagents" "pi-subagents" npx vitest run
35
+ run "pi-tasks" "pi-tasks" npx vitest run
36
+
37
+ # bun packages. pi-ask-user has no `test` npm script, so invoke `bun test` directly.
38
+ run "pi-lsp" "pi-lsp" bun test
39
+ run "pi-ask-user" "pi-ask-user" bun test
40
+
41
+ # pi-plannotator: scope explicitly to the shipped pi-extension app + the fork-parity guards.
42
+ # Bare `bun test` would reach unshipped upstream workspace packages that need alias setup.
43
+ run "pi-plannotator" "pi-plannotator" bun test apps/pi-extension tests/parity
44
+
45
+ echo ""
46
+ echo "──────────────────────────────────────────────"
47
+ if [ ${#failures[@]} -eq 0 ]; then
48
+ echo "✓ all 3p suites passed"
49
+ exit 0
50
+ fi
51
+ echo "✗ 3p suites FAILED: ${failures[*]}"
52
+ exit 1