@ilya-lesikov/pi-pi 0.9.0 → 0.11.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.
- package/3p/pi-ask-user/index.ts +71 -124
- package/3p/pi-ask-user/single-select-layout.ts +3 -14
- package/3p/pi-subagents/package.json +12 -7
- package/3p/pi-subagents/src/agent-manager.ts +243 -79
- package/3p/pi-subagents/src/agent-runner.ts +501 -365
- package/3p/pi-subagents/src/agent-types.ts +46 -57
- package/3p/pi-subagents/src/cross-extension-rpc.ts +52 -46
- package/3p/pi-subagents/src/custom-agents.ts +30 -6
- package/3p/pi-subagents/src/default-agents.ts +25 -43
- package/3p/pi-subagents/src/enabled-models.ts +180 -0
- package/3p/pi-subagents/src/index.ts +599 -839
- package/3p/pi-subagents/src/model-resolver.ts +26 -7
- package/3p/pi-subagents/src/output-file.ts +21 -2
- package/3p/pi-subagents/src/prompts.ts +17 -3
- package/3p/pi-subagents/src/schedule-store.ts +153 -0
- package/3p/pi-subagents/src/schedule.ts +365 -0
- package/3p/pi-subagents/src/settings.ts +261 -0
- package/3p/pi-subagents/src/skill-loader.ts +77 -54
- package/3p/pi-subagents/src/status-note.ts +25 -0
- package/3p/pi-subagents/src/types.ts +97 -6
- package/3p/pi-subagents/src/ui/agent-widget.ts +94 -21
- package/3p/pi-subagents/src/ui/conversation-viewer.ts +147 -35
- package/3p/pi-subagents/src/ui/schedule-menu.ts +104 -0
- package/3p/pi-subagents/src/ui/viewer-keys.ts +39 -0
- package/3p/pi-subagents/src/usage.ts +60 -0
- package/3p/pi-subagents/src/worktree.ts +49 -20
- package/extensions/orchestrator/agents/advisor.ts +13 -8
- package/extensions/orchestrator/agents/brainstorm-reviewer.ts +8 -3
- package/extensions/orchestrator/agents/code-reviewer.ts +8 -3
- package/extensions/orchestrator/agents/constraints.test.ts +26 -0
- package/extensions/orchestrator/agents/constraints.ts +12 -2
- package/extensions/orchestrator/agents/deep-debugger.ts +13 -8
- package/extensions/orchestrator/agents/explore.ts +12 -6
- package/extensions/orchestrator/agents/librarian.ts +13 -5
- package/extensions/orchestrator/agents/plan-reviewer.ts +8 -3
- package/extensions/orchestrator/agents/planner.ts +8 -3
- package/extensions/orchestrator/agents/pools.test.ts +56 -0
- package/extensions/orchestrator/agents/prompts.test.ts +52 -10
- package/extensions/orchestrator/agents/registry.test.ts +245 -0
- package/extensions/orchestrator/agents/registry.ts +145 -10
- package/extensions/orchestrator/agents/reviewer.ts +14 -8
- package/extensions/orchestrator/agents/task.ts +12 -6
- package/extensions/orchestrator/agents/tool-routing.ts +213 -85
- package/extensions/orchestrator/agents/wait-for-completion.test.ts +171 -0
- package/extensions/orchestrator/ast-search.test.ts +124 -0
- package/extensions/orchestrator/billing-spoof.test.ts +67 -0
- package/extensions/orchestrator/billing-spoof.ts +95 -0
- package/extensions/orchestrator/cbm.more.test.ts +358 -0
- package/extensions/orchestrator/command-handlers.ts +6 -0
- package/extensions/orchestrator/commands.test.ts +15 -2
- package/extensions/orchestrator/commands.ts +1 -1
- package/extensions/orchestrator/config.test.ts +89 -1
- package/extensions/orchestrator/config.ts +102 -19
- package/extensions/orchestrator/context.test.ts +46 -0
- package/extensions/orchestrator/context.ts +18 -5
- package/extensions/orchestrator/custom-footer.test.ts +24 -10
- package/extensions/orchestrator/custom-footer.ts +4 -2
- package/extensions/orchestrator/doctor.more.test.ts +315 -0
- package/extensions/orchestrator/doctor.ts +6 -2
- package/extensions/orchestrator/event-handlers.more.test.ts +561 -0
- package/extensions/orchestrator/event-handlers.test.ts +96 -9
- package/extensions/orchestrator/event-handlers.ts +344 -151
- package/extensions/orchestrator/exa.more.test.ts +118 -0
- package/extensions/orchestrator/flant-infra.more.test.ts +381 -0
- package/extensions/orchestrator/flant-infra.test.ts +127 -0
- package/extensions/orchestrator/flant-infra.ts +126 -41
- package/extensions/orchestrator/index.test.ts +76 -0
- package/extensions/orchestrator/index.ts +2 -0
- package/extensions/orchestrator/integration.test.ts +183 -65
- package/extensions/orchestrator/model-registry.test.ts +2 -1
- package/extensions/orchestrator/model-registry.ts +12 -2
- package/extensions/orchestrator/orchestrator.test.ts +67 -0
- package/extensions/orchestrator/orchestrator.ts +119 -27
- package/extensions/orchestrator/phases/brainstorm.test.ts +30 -1
- package/extensions/orchestrator/phases/brainstorm.ts +43 -6
- package/extensions/orchestrator/phases/implementation.ts +2 -0
- package/extensions/orchestrator/phases/machine.test.ts +17 -1
- package/extensions/orchestrator/phases/machine.ts +4 -1
- package/extensions/orchestrator/phases/planning.test.ts +47 -1
- package/extensions/orchestrator/phases/planning.ts +18 -3
- package/extensions/orchestrator/phases/review-task.ts +5 -0
- package/extensions/orchestrator/phases/review.test.ts +10 -0
- package/extensions/orchestrator/phases/review.ts +22 -7
- package/extensions/orchestrator/phases/spawn-blocking.test.ts +1 -0
- package/extensions/orchestrator/phases/verdict.ts +6 -5
- package/extensions/orchestrator/plannotator-open-failure.test.ts +67 -0
- package/extensions/orchestrator/plannotator.test.ts +38 -1
- package/extensions/orchestrator/plannotator.ts +50 -3
- package/extensions/orchestrator/pp-menu.leaves.test.ts +590 -0
- package/extensions/orchestrator/pp-menu.more.test.ts +524 -0
- package/extensions/orchestrator/pp-menu.test.ts +114 -7
- package/extensions/orchestrator/pp-menu.ts +580 -91
- package/extensions/orchestrator/pp-state-tools.test.ts +80 -0
- package/extensions/orchestrator/pp-state-tools.ts +65 -0
- package/extensions/orchestrator/rate-limit-fallback.more.test.ts +241 -0
- package/extensions/orchestrator/rate-limit-fallback.test.ts +20 -1
- package/extensions/orchestrator/rate-limit-fallback.ts +12 -0
- package/extensions/orchestrator/review-files.test.ts +26 -0
- package/extensions/orchestrator/review-files.ts +3 -0
- package/extensions/orchestrator/state.test.ts +73 -1
- package/extensions/orchestrator/state.ts +95 -23
- package/extensions/orchestrator/suppress-pierre-theme-spam.test.ts +56 -0
- package/extensions/orchestrator/suppress-pierre-theme-spam.ts +45 -0
- package/extensions/orchestrator/validate-artifacts.ts +1 -1
- package/package.json +9 -2
- package/scripts/lib/smoke-resolve.mjs +99 -0
- package/scripts/test-3p.sh +52 -0
- package/scripts/test-package.sh +62 -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
|
|
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
|
|
257
|
+
export function getUnlockedTasks(cwd: string, lockStale?: number): TaskInfo[] {
|
|
233
258
|
const tasks = listTasks(cwd);
|
|
234
|
-
if (tasks.length === 0) return
|
|
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
|
-
|
|
250
|
-
|
|
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
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
|
|
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
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.11.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,11 @@
|
|
|
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:package": "bash scripts/test-package.sh",
|
|
41
|
+
"test:all": "vitest run --exclude '3p/**' && bash scripts/test-3p.sh && bash scripts/test-package.sh",
|
|
42
|
+
"coverage": "vitest run --exclude '3p/**' --coverage"
|
|
39
43
|
},
|
|
40
44
|
"pi": {
|
|
41
45
|
"extensions": [
|
|
@@ -50,8 +54,10 @@
|
|
|
50
54
|
"dependencies": {
|
|
51
55
|
"@joplin/turndown-plugin-gfm": "^1.0.64",
|
|
52
56
|
"@pierre/diffs": "^1.2.0",
|
|
57
|
+
"croner": "^10.0.1",
|
|
53
58
|
"effect": "^3.0.0",
|
|
54
59
|
"minimatch": "^10.0.0",
|
|
60
|
+
"nanoid": "^5.0.0",
|
|
55
61
|
"pino": "^10.3.1",
|
|
56
62
|
"proper-lockfile": "^4.1.2",
|
|
57
63
|
"turndown": "^7.2.0",
|
|
@@ -68,6 +74,7 @@
|
|
|
68
74
|
"@earendil-works/pi-coding-agent": "^0.75.5",
|
|
69
75
|
"@earendil-works/pi-tui": "^0.75.5",
|
|
70
76
|
"@sinclair/typebox": "^0.34.0",
|
|
77
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
71
78
|
"typescript": "^5.8.0",
|
|
72
79
|
"vitest": "^4.1.4"
|
|
73
80
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Runs from INSIDE the installed package directory (import.meta.url is under the package), so
|
|
2
|
+
// dynamic import() of a bare specifier resolves through the installed artifact's own
|
|
3
|
+
// node_modules chain — the same resolution pi performs at extension load. Exits non-zero if a
|
|
4
|
+
// registered extension path is missing or a required runtime specifier fails to load.
|
|
5
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
6
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const PKG_DIR = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
|
|
11
|
+
function fail(msg) {
|
|
12
|
+
console.error(` ✗ ${msg}`);
|
|
13
|
+
process.exitCode = 1;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const pkg = JSON.parse(readFileSync(join(PKG_DIR, "package.json"), "utf8"));
|
|
17
|
+
|
|
18
|
+
const extEntries = pkg?.pi?.extensions ?? [];
|
|
19
|
+
if (extEntries.length === 0) fail("package.json has no pi.extensions entries");
|
|
20
|
+
for (const entry of extEntries) {
|
|
21
|
+
if (existsSync(resolve(PKG_DIR, entry))) {
|
|
22
|
+
console.log(` ✓ extension present: ${entry}`);
|
|
23
|
+
} else {
|
|
24
|
+
fail(`registered extension path missing from artifact: ${entry}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Bare specifiers pi itself supplies to loaded extensions — never mirrored into dependencies.
|
|
29
|
+
const peerSupplied = new Set(Object.keys(pkg.peerDependencies ?? {}));
|
|
30
|
+
|
|
31
|
+
function isBuiltin(spec) {
|
|
32
|
+
return spec.startsWith("node:");
|
|
33
|
+
}
|
|
34
|
+
function isRelative(spec) {
|
|
35
|
+
return spec.startsWith(".") || spec.startsWith("/");
|
|
36
|
+
}
|
|
37
|
+
function packageName(spec) {
|
|
38
|
+
const parts = spec.split("/");
|
|
39
|
+
return spec.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function collectTsFiles(dir) {
|
|
43
|
+
const out = [];
|
|
44
|
+
for (const name of readdirSync(dir)) {
|
|
45
|
+
const full = join(dir, name);
|
|
46
|
+
const st = statSync(full);
|
|
47
|
+
if (st.isDirectory()) out.push(...collectTsFiles(full));
|
|
48
|
+
else if (name.endsWith(".ts")) out.push(full);
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Scope the scan to the pi-subagents source only. Other shipped 3p sources reference SDK
|
|
54
|
+
// specifiers behind lazy `await import()` guards that are intentionally NOT root dependencies;
|
|
55
|
+
// scanning them would produce a permanently failing test.
|
|
56
|
+
const SCAN_DIR = join(PKG_DIR, "3p", "pi-subagents", "src");
|
|
57
|
+
if (!existsSync(SCAN_DIR)) {
|
|
58
|
+
fail(`expected shipped source dir missing: 3p/pi-subagents/src`);
|
|
59
|
+
process.exit(process.exitCode ?? 1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Value (non-type) import/export ... from "spec" and dynamic import("spec"). `import type` /
|
|
63
|
+
// `export type` statements are type-only and erased at build, so they carry no runtime requirement.
|
|
64
|
+
const FROM_RE = /(^|\n)\s*(import|export)\s+(?!type\b)(?:[^;]*?\sfrom\s+)?["']([^"']+)["']/g;
|
|
65
|
+
const DYN_RE = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
66
|
+
|
|
67
|
+
const required = new Set();
|
|
68
|
+
for (const file of collectTsFiles(SCAN_DIR)) {
|
|
69
|
+
const src = readFileSync(file, "utf8");
|
|
70
|
+
for (const m of src.matchAll(FROM_RE)) addSpec(m[3]);
|
|
71
|
+
for (const m of src.matchAll(DYN_RE)) addSpec(m[1]);
|
|
72
|
+
}
|
|
73
|
+
function addSpec(spec) {
|
|
74
|
+
if (isBuiltin(spec) || isRelative(spec)) return;
|
|
75
|
+
const name = packageName(spec);
|
|
76
|
+
if (peerSupplied.has(name)) return;
|
|
77
|
+
required.add(name);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Floor: these two are the runtime imports that broke consumer installs; assert them explicitly
|
|
81
|
+
// even if the extractor's shape ever changes.
|
|
82
|
+
required.add("croner");
|
|
83
|
+
required.add("nanoid");
|
|
84
|
+
|
|
85
|
+
const specs = [...required].sort();
|
|
86
|
+
console.log(` runtime specifiers to resolve: ${specs.join(", ")}`);
|
|
87
|
+
|
|
88
|
+
for (const spec of specs) {
|
|
89
|
+
try {
|
|
90
|
+
await import(spec);
|
|
91
|
+
console.log(` ✓ resolved: ${spec}`);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
fail(`cannot resolve '${spec}' from installed package: ${err?.message ?? err}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (process.exitCode) {
|
|
98
|
+
console.error(" → the root package is missing a runtime dependency its shipped extensions need.");
|
|
99
|
+
}
|
|
@@ -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
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Release-layout smoke test: pack the root package, install the tarball into a throwaway
|
|
3
|
+
# directory OUTSIDE this checkout, and verify the shipped extension artifact resolves its
|
|
4
|
+
# runtime imports from the installed tree alone.
|
|
5
|
+
#
|
|
6
|
+
# Why this exists: the root `npm test` excludes 3p/** and the vendored suites run inside
|
|
7
|
+
# 3p/*/node_modules, so a bare import that a vendored extension needs but the root package
|
|
8
|
+
# forgets to declare is invisible in the dev checkout (its node_modules still has the package).
|
|
9
|
+
# It only surfaces after a clean `npm install` by a consumer — exactly the croner/nanoid
|
|
10
|
+
# regression this guards against. Resolving bare specifiers from the installed package tree is
|
|
11
|
+
# the only shape that reproduces that failure.
|
|
12
|
+
#
|
|
13
|
+
# Registry access is required (npm install of the tarball fetches deps, runs the root
|
|
14
|
+
# postinstall, and npm v7+ auto-installs peerDependencies). Offline CI: warm the npm cache and
|
|
15
|
+
# export a --prefer-offline-friendly registry mirror.
|
|
16
|
+
set -uo pipefail
|
|
17
|
+
cd "$(dirname "$0")/.."
|
|
18
|
+
ROOT="$(pwd)"
|
|
19
|
+
|
|
20
|
+
TMPDIR_SMOKE="$(mktemp -d)"
|
|
21
|
+
trap 'rm -rf "$TMPDIR_SMOKE"' EXIT
|
|
22
|
+
|
|
23
|
+
echo "▶ packing root package"
|
|
24
|
+
PACK_JSON="$(npm pack --json --pack-destination "$TMPDIR_SMOKE" 2>/dev/null)"
|
|
25
|
+
TARBALL="$(node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const a=JSON.parse(s);process.stdout.write(a[0].filename)})' <<<"$PACK_JSON")"
|
|
26
|
+
if [ -z "$TARBALL" ] || [ ! -f "$TMPDIR_SMOKE/$TARBALL" ]; then
|
|
27
|
+
echo "ERROR: npm pack did not produce a tarball (got: '$TARBALL')." >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
echo " tarball: $TARBALL"
|
|
31
|
+
|
|
32
|
+
INSTALL_DIR="$TMPDIR_SMOKE/install"
|
|
33
|
+
mkdir -p "$INSTALL_DIR"
|
|
34
|
+
( cd "$INSTALL_DIR" && npm init -y >/dev/null 2>&1 )
|
|
35
|
+
|
|
36
|
+
echo "▶ installing tarball into a clean consumer directory"
|
|
37
|
+
if ! ( cd "$INSTALL_DIR" && npm install "$TMPDIR_SMOKE/$TARBALL" --no-audit --no-fund 2>&1 ); then
|
|
38
|
+
echo "ERROR: installing the packed tarball failed." >&2
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
PKG_NAME="$(node -e 'process.stdout.write(require("./package.json").name)')"
|
|
43
|
+
PKG_DIR="$INSTALL_DIR/node_modules/$PKG_NAME"
|
|
44
|
+
if [ ! -d "$PKG_DIR" ]; then
|
|
45
|
+
echo "ERROR: installed package not found at $PKG_DIR" >&2
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Run the resolution check from a helper placed INSIDE the installed package, so bare-specifier
|
|
50
|
+
# resolution provably walks the artifact's own node_modules hierarchy (not this checkout's).
|
|
51
|
+
CHECK="$PKG_DIR/__smoke-resolve.mjs"
|
|
52
|
+
cp "$ROOT/scripts/lib/smoke-resolve.mjs" "$CHECK"
|
|
53
|
+
|
|
54
|
+
echo "▶ resolving shipped extension runtime imports from the install tree"
|
|
55
|
+
( cd "$PKG_DIR" && node "$CHECK" )
|
|
56
|
+
STATUS=$?
|
|
57
|
+
|
|
58
|
+
if [ $STATUS -ne 0 ]; then
|
|
59
|
+
echo "✗ package smoke test FAILED" >&2
|
|
60
|
+
exit $STATUS
|
|
61
|
+
fi
|
|
62
|
+
echo "✓ package smoke test passed"
|