@ilya-lesikov/pi-pi 0.7.0 → 0.9.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 (75) hide show
  1. package/3p/pi-plannotator/apps/pi-extension/README.md +6 -5
  2. package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +25 -10
  3. package/3p/pi-plannotator/apps/pi-extension/plannotator-events.code-review.test.ts +172 -0
  4. package/3p/pi-plannotator/apps/pi-extension/plannotator-events.ts +50 -12
  5. package/3p/pi-plannotator/apps/pi-extension/server/project.test.ts +45 -0
  6. package/3p/pi-plannotator/apps/pi-extension/server/project.ts +7 -6
  7. package/3p/pi-plannotator/apps/pi-extension/server/serverReview.ts +41 -25
  8. package/3p/pi-subagents/src/agent-manager.ts +34 -1
  9. package/3p/pi-subagents/src/agent-runner.ts +66 -33
  10. package/3p/pi-subagents/src/index.ts +3 -38
  11. package/3p/pi-subagents/src/types.ts +4 -0
  12. package/extensions/orchestrator/agents/advisor.ts +35 -0
  13. package/extensions/orchestrator/agents/brainstorm-reviewer.ts +7 -7
  14. package/extensions/orchestrator/agents/code-reviewer.ts +27 -11
  15. package/extensions/orchestrator/agents/constraints.test.ts +82 -0
  16. package/extensions/orchestrator/agents/constraints.ts +66 -2
  17. package/extensions/orchestrator/agents/deep-debugger.ts +35 -0
  18. package/extensions/orchestrator/agents/plan-reviewer.ts +7 -7
  19. package/extensions/orchestrator/agents/planner.ts +9 -8
  20. package/extensions/orchestrator/agents/prompts.test.ts +120 -0
  21. package/extensions/orchestrator/agents/reviewer.ts +48 -0
  22. package/extensions/orchestrator/agents/task.ts +6 -20
  23. package/extensions/orchestrator/agents/tool-routing.ts +39 -1
  24. package/extensions/orchestrator/ai-comment-cleanup.test.ts +89 -0
  25. package/extensions/orchestrator/ai-comment-cleanup.ts +73 -0
  26. package/extensions/orchestrator/command-handlers.test.ts +47 -0
  27. package/extensions/orchestrator/command-handlers.ts +44 -28
  28. package/extensions/orchestrator/config.test.ts +40 -1
  29. package/extensions/orchestrator/config.ts +18 -2
  30. package/extensions/orchestrator/context.test.ts +54 -0
  31. package/extensions/orchestrator/context.ts +81 -2
  32. package/extensions/orchestrator/custom-footer.test.ts +91 -0
  33. package/extensions/orchestrator/custom-footer.ts +20 -20
  34. package/extensions/orchestrator/event-handlers.test.ts +412 -2
  35. package/extensions/orchestrator/event-handlers.ts +556 -227
  36. package/extensions/orchestrator/flant-infra.test.ts +25 -0
  37. package/extensions/orchestrator/flant-infra.ts +91 -0
  38. package/extensions/orchestrator/index.ts +1 -1
  39. package/extensions/orchestrator/integration.test.ts +411 -20
  40. package/extensions/orchestrator/messages.test.ts +30 -0
  41. package/extensions/orchestrator/messages.ts +6 -0
  42. package/extensions/orchestrator/model-registry.test.ts +48 -1
  43. package/extensions/orchestrator/model-registry.ts +43 -1
  44. package/extensions/orchestrator/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  45. package/extensions/orchestrator/orchestrator.test.ts +113 -0
  46. package/extensions/orchestrator/orchestrator.ts +197 -60
  47. package/extensions/orchestrator/phases/brainstorm.ts +20 -27
  48. package/extensions/orchestrator/phases/implementation.test.ts +11 -0
  49. package/extensions/orchestrator/phases/implementation.ts +4 -6
  50. package/extensions/orchestrator/phases/machine.test.ts +36 -0
  51. package/extensions/orchestrator/phases/machine.ts +11 -1
  52. package/extensions/orchestrator/phases/planning.test.ts +16 -0
  53. package/extensions/orchestrator/phases/planning.ts +11 -4
  54. package/extensions/orchestrator/phases/review-task.test.ts +20 -0
  55. package/extensions/orchestrator/phases/review-task.ts +47 -22
  56. package/extensions/orchestrator/phases/review.test.ts +34 -0
  57. package/extensions/orchestrator/phases/review.ts +44 -6
  58. package/extensions/orchestrator/plannotator.ts +9 -6
  59. package/extensions/orchestrator/pp-menu.test.ts +207 -1
  60. package/extensions/orchestrator/pp-menu.ts +514 -402
  61. package/extensions/orchestrator/pp-state-tools.test.ts +192 -0
  62. package/extensions/orchestrator/pp-state-tools.ts +249 -0
  63. package/extensions/orchestrator/rate-limit-fallback-flow.test.ts +128 -0
  64. package/extensions/orchestrator/rate-limit-fallback.test.ts +98 -0
  65. package/extensions/orchestrator/rate-limit-fallback.ts +243 -0
  66. package/extensions/orchestrator/state.test.ts +9 -0
  67. package/extensions/orchestrator/state.ts +15 -0
  68. package/extensions/orchestrator/test-helpers.ts +4 -1
  69. package/extensions/orchestrator/transition-controller.test.ts +100 -0
  70. package/extensions/orchestrator/transition-controller.ts +61 -3
  71. package/extensions/orchestrator/usage-tracker.ts +5 -1
  72. package/extensions/orchestrator/validate-artifacts.test.ts +20 -0
  73. package/extensions/orchestrator/validate-artifacts.ts +2 -2
  74. package/package.json +1 -1
  75. package/AGENTS.md +0 -28
@@ -1,11 +1,34 @@
1
1
  import { afterEach, describe, expect, it, vi } from "vitest";
2
2
  import { join } from "path";
3
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "fs";
4
+ import { tmpdir } from "os";
3
5
  import { getDefaultConfig, GLOBAL_CONFIG_PATH, parseDuration } from "./config.js";
4
6
  import * as configModule from "./config.js";
5
7
  import * as flantInfra from "./flant-infra.js";
6
- import { formatDuration, formatSourceTags, getConfigSourceInfo, pickMaxReviewPasses, showUsage } from "./pp-menu.js";
8
+ import { formatDuration, formatSourceTags, getConfigSourceInfo, pickMaxReviewPasses, publishGuard, publishFileCommentsBanner, publishPrCommentsBanner, showActiveTaskMenu, showUsage } from "./pp-menu.js";
7
9
  import { createUsageTracker } from "./usage-tracker.js";
8
10
 
11
+ // Drives showActiveTaskMenu's submenu navigation by scripting selectOption answers.
12
+ const askQueue: string[] = [];
13
+ const askQuestions: string[] = [];
14
+ vi.mock("../../3p/pi-ask-user/index.js", () => ({
15
+ isCancel: (r: any) => r?.__cancel === true,
16
+ askUser: vi.fn(async (_ctx: any, opts: any) => {
17
+ askQuestions.push(opts.question);
18
+ const next = askQueue.shift();
19
+ if (next === undefined || next === "__ESC__") return { __cancel: true, reason: "user" };
20
+ return { kind: "selection", selections: [next] };
21
+ }),
22
+ }));
23
+
24
+ // Scripts per-repo Plannotator outcomes for the #3a interleaved cursor tests.
25
+ const plannotatorResults: Array<{ approved: boolean; feedback?: string }> = [];
26
+ vi.mock("./plannotator.js", () => ({
27
+ cancelPendingPlannotatorWait: () => {},
28
+ openPlannotator: vi.fn(async () => ({ opened: true, reviewId: "rev" })),
29
+ waitForPlannotatorResult: vi.fn(async () => plannotatorResults.shift() ?? { approved: true }),
30
+ }));
31
+
9
32
  const USAGE_TRACKER_SYMBOL = Symbol.for("pi-pi:usage-tracker");
10
33
 
11
34
  afterEach(() => {
@@ -205,3 +228,186 @@ describe("pickMaxReviewPasses", () => {
205
228
  expect(await pickMaxReviewPasses(makeInputCtx([""]), 3)).toBeNull();
206
229
  });
207
230
  });
231
+
232
+ describe("publishGuard", () => {
233
+ let taskDir: string;
234
+
235
+ afterEach(() => {
236
+ if (taskDir) rmSync(taskDir, { recursive: true, force: true });
237
+ });
238
+
239
+ function makeTask(): string {
240
+ taskDir = mkdtempSync(join(tmpdir(), "pp-guard-"));
241
+ return taskDir;
242
+ }
243
+
244
+ it("blocks publishing when no review has run (no code-reviews dir)", () => {
245
+ const dir = makeTask();
246
+ const msg = publishGuard(dir);
247
+ expect(msg).toBeTruthy();
248
+ expect(msg).toContain("review pass");
249
+ });
250
+
251
+ it("blocks publishing when a final_pass file exists but carries no ANCHORS block", () => {
252
+ const dir = makeTask();
253
+ mkdirSync(join(dir, "code-reviews"));
254
+ writeFileSync(join(dir, "code-reviews", "20260101-000000_final_pass-1.md"), "# Findings\n\nSome prose without anchors.\n");
255
+ expect(publishGuard(dir)).toBeTruthy();
256
+ });
257
+
258
+ it("allows publishing when the latest final_pass file has an ANCHORS block", () => {
259
+ const dir = makeTask();
260
+ mkdirSync(join(dir, "code-reviews"));
261
+ writeFileSync(join(dir, "code-reviews", "20260101-000000_final_pass-1.md"), "ANCHORS:\nsrc/a.ts:10 — bug\n");
262
+ expect(publishGuard(dir)).toBeUndefined();
263
+ });
264
+
265
+ it("resolves the newest final_pass file by name and honors its ANCHORS block", () => {
266
+ const dir = makeTask();
267
+ mkdirSync(join(dir, "code-reviews"));
268
+ writeFileSync(join(dir, "code-reviews", "20260101-000000_final_pass-1.md"), "ANCHORS:\nsrc/a.ts:10 — bug\n");
269
+ writeFileSync(join(dir, "code-reviews", "20260102-000000_final_pass-2.md"), "# Findings\n\nno anchors here\n");
270
+ expect(publishGuard(dir)).toBeTruthy();
271
+ });
272
+ });
273
+
274
+ describe("publish banners", () => {
275
+ it("PR banner posts one bundled COMMENT review per repo, pre-validated against the diff", () => {
276
+ const banner = publishPrCommentsBanner("/tmp/task");
277
+ expect(banner).toContain("pulls/<number>/reviews");
278
+ expect(banner).toContain("event=COMMENT");
279
+ expect(banner).not.toContain("event=APPROVE");
280
+ expect(banner).toContain("never APPROVE or REQUEST_CHANGES");
281
+ expect(banner).toContain("PRE-VALIDATE");
282
+ expect(banner).toContain("all-or-nothing");
283
+ expect(banner).toContain("Findings not anchorable to the diff:");
284
+ expect(banner).toContain("(generated by pi-pi)");
285
+ });
286
+
287
+ it("PR banner idempotency matches on path+line+footer+body", () => {
288
+ const banner = publishPrCommentsBanner("/tmp/task");
289
+ expect(banner).toContain("footer, AND body text all match");
290
+ expect(banner).toContain("Two distinct findings on the same line are NOT duplicates");
291
+ });
292
+
293
+ it("both banners carry the privacy instruction", () => {
294
+ for (const banner of [publishPrCommentsBanner("/tmp/task"), publishFileCommentsBanner("/tmp/task")]) {
295
+ expect(banner).toContain("PRIVACY:");
296
+ expect(banner).toContain("the ticket");
297
+ expect(banner).toContain("self-contained");
298
+ }
299
+ });
300
+ });
301
+
302
+ describe("showActiveTaskMenu Publish/Next Back navigation (#6)", () => {
303
+ let taskDir: string;
304
+
305
+ afterEach(() => {
306
+ askQueue.length = 0;
307
+ askQuestions.length = 0;
308
+ if (taskDir) rmSync(taskDir, { recursive: true, force: true });
309
+ });
310
+
311
+ function makeReviewOrchestrator(): any {
312
+ taskDir = mkdtempSync(join(tmpdir(), "pp-nav-"));
313
+ return {
314
+ active: {
315
+ type: "review",
316
+ dir: taskDir,
317
+ state: { phase: "review", step: "llm_work", mode: "guided" },
318
+ },
319
+ transitionController: {
320
+ isRunning: () => false,
321
+ abortMainAgent: () => {},
322
+ },
323
+ cancelPendingRetry: () => {},
324
+ abortAllSubagents: () => {},
325
+ };
326
+ }
327
+
328
+ const ctx = { ui: { notify: () => {} }, waitForIdle: async () => {}, abort: () => {} };
329
+
330
+ it("Publish 'Back' re-renders the Next submenu instead of the top-level menu", async () => {
331
+ const orchestrator = makeReviewOrchestrator();
332
+ // /pp -> Next -> Publish -> Back (should return to Next) -> Back (to top-level) -> Back (exit).
333
+ askQueue.push("Next", "Publish", "Back", "Back", "Back");
334
+ const result = await showActiveTaskMenu(orchestrator, ctx, "/pp", "tool");
335
+ expect(result).toBe("");
336
+ // After Publish's Back we must see the "Next" submenu rendered again before the
337
+ // top-level menu reappears: questions = [top, Next, Publish, Next(again), top].
338
+ const nextRenders = askQuestions.filter((q) => q === "Next").length;
339
+ expect(nextRenders).toBe(2);
340
+ expect(askQuestions[askQuestions.length - 1]).toContain("/pp");
341
+ });
342
+
343
+ it("Next 'Back' returns straight to the top-level menu", async () => {
344
+ const orchestrator = makeReviewOrchestrator();
345
+ askQueue.push("Next", "Back", "Back");
346
+ const result = await showActiveTaskMenu(orchestrator, ctx, "/pp", "tool");
347
+ expect(result).toBe("");
348
+ expect(askQuestions.filter((q) => q === "Next").length).toBe(1);
349
+ // Top-level rendered twice (initial + after Next's Back), Next once.
350
+ expect(askQuestions.filter((q) => q.startsWith("/pp")).length).toBe(2);
351
+ });
352
+
353
+ it("Review submenu 'Editor review' Back returns to Review, not the top-level menu (#3d)", async () => {
354
+ const orchestrator = makeReviewOrchestrator();
355
+ // /pp -> Review -> Review on my own -> Editor review Back (should return to
356
+ // Review) -> Review Back (to top-level) -> top-level Back (exit).
357
+ askQueue.push("Review", "Review on my own", "Back", "Back", "Back");
358
+ const result = await showActiveTaskMenu(orchestrator, ctx, "/pp", "tool");
359
+ expect(result).toBe("");
360
+ // Review submenu rendered twice (initial + after the Editor-review Back).
361
+ expect(askQuestions.filter((q) => q === "Review").length).toBe(2);
362
+ // Top-level rendered twice (initial + after Review's explicit Back).
363
+ expect(askQuestions.filter((q) => q.startsWith("/pp")).length).toBe(2);
364
+ });
365
+
366
+ it("resumes the per-repo Plannotator cursor and interleaves fixes (#3a)", async () => {
367
+ const orchestrator = makeReviewOrchestrator();
368
+ orchestrator.active.type = "implement";
369
+ orchestrator.active.state.phase = "implement";
370
+ orchestrator.active.state.repos = [
371
+ { path: "/repo/a", isRoot: true },
372
+ { path: "/repo/b", isRoot: false },
373
+ ];
374
+ orchestrator.active.state.plannotatorCursor = { repoPaths: ["/repo/a", "/repo/b"], index: 0 };
375
+ orchestrator.pi = { exec: vi.fn(async () => ({ code: 0, stdout: "", stderr: "" })) };
376
+ orchestrator.config = getDefaultConfig();
377
+
378
+ // First repo: choose a diff scope, Plannotator returns NEEDS_CHANGES → the
379
+ // menu exits with a work instruction and the cursor advances to repo b.
380
+ plannotatorResults.push({ approved: false, feedback: "fix the thing" });
381
+ askQueue.push("Uncommitted changes");
382
+ const first = await showActiveTaskMenu(orchestrator, ctx, "/pp", "tool");
383
+ expect(first).toContain("Plannotator requested changes");
384
+ expect(first).toContain("fix the thing");
385
+ expect(orchestrator.active.state.plannotatorCursor).toEqual({ repoPaths: ["/repo/a", "/repo/b"], index: 1 });
386
+
387
+ // Next /pp resumes at repo b; APPROVED → cursor is cleared and the loop ends,
388
+ // falling through to the normal menu (top-level Back exits).
389
+ plannotatorResults.push({ approved: true });
390
+ askQueue.push("Uncommitted changes", "Back");
391
+ const second = await showActiveTaskMenu(orchestrator, ctx, "/pp", "tool");
392
+ expect(second).toBe("");
393
+ expect(orchestrator.active.state.plannotatorCursor).toBeUndefined();
394
+ });
395
+
396
+ it("blocks a second review while one is already running (#3b)", async () => {
397
+ const orchestrator = makeReviewOrchestrator();
398
+ orchestrator.active.state.reviewCycle = { kind: "auto", step: "await_reviewers", pass: 1 };
399
+ // isRunning() true so the top-level "Review" option is offered.
400
+ orchestrator.transitionController.isRunning = () => true;
401
+ let notified = "";
402
+ const notifyCtx = { ui: { notify: (t: string) => { notified = t; } }, waitForIdle: async () => {}, abort: () => {} };
403
+ // Pick Review (blocked → notify + back to top-level menu), then Back to exit.
404
+ askQueue.push("Review", "Back");
405
+ const result = await showActiveTaskMenu(orchestrator, notifyCtx, "/pp", "tool");
406
+ expect(result).toBe("");
407
+ expect(notified).toBe("A review is already running");
408
+ // The live cycle is untouched (not finalized/nulled) and the top-level menu
409
+ // re-rendered rather than /pp exiting.
410
+ expect(orchestrator.active.state.reviewCycle).toEqual({ kind: "auto", step: "await_reviewers", pass: 1 });
411
+ expect(askQuestions.filter((q) => q.startsWith("/pp")).length).toBe(2);
412
+ });
413
+ });