@ilya-lesikov/pi-pi 0.8.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.
- 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 +13 -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 +29 -8
- package/extensions/orchestrator/agents/constraints.test.ts +65 -1
- package/extensions/orchestrator/agents/constraints.ts +73 -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 -69
- package/extensions/orchestrator/agents/wait-for-completion.test.ts +171 -0
- package/extensions/orchestrator/ai-comment-cleanup.test.ts +89 -0
- package/extensions/orchestrator/ai-comment-cleanup.ts +73 -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.test.ts +47 -0
- package/extensions/orchestrator/command-handlers.ts +49 -27
- package/extensions/orchestrator/commands.test.ts +15 -2
- package/extensions/orchestrator/commands.ts +1 -1
- package/extensions/orchestrator/config.test.ts +129 -2
- package/extensions/orchestrator/config.ts +115 -19
- package/extensions/orchestrator/context.test.ts +46 -0
- package/extensions/orchestrator/context.ts +34 -5
- package/extensions/orchestrator/custom-footer.test.ts +105 -0
- package/extensions/orchestrator/custom-footer.ts +22 -20
- 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 +411 -10
- package/extensions/orchestrator/event-handlers.ts +738 -349
- package/extensions/orchestrator/exa.more.test.ts +118 -0
- package/extensions/orchestrator/flant-infra.more.test.ts +326 -0
- package/extensions/orchestrator/flant-infra.test.ts +127 -0
- package/extensions/orchestrator/flant-infra.ts +113 -39
- package/extensions/orchestrator/index.test.ts +76 -0
- package/extensions/orchestrator/index.ts +2 -0
- package/extensions/orchestrator/integration.test.ts +488 -74
- package/extensions/orchestrator/model-registry.test.ts +2 -1
- package/extensions/orchestrator/model-registry.ts +12 -2
- package/extensions/orchestrator/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/extensions/orchestrator/orchestrator.test.ts +67 -0
- package/extensions/orchestrator/orchestrator.ts +165 -85
- package/extensions/orchestrator/phases/brainstorm.test.ts +30 -1
- package/extensions/orchestrator/phases/brainstorm.ts +53 -12
- package/extensions/orchestrator/phases/implementation.ts +2 -0
- package/extensions/orchestrator/phases/machine.test.ts +53 -1
- package/extensions/orchestrator/phases/machine.ts +15 -2
- package/extensions/orchestrator/phases/planning.test.ts +47 -1
- package/extensions/orchestrator/phases/planning.ts +18 -3
- package/extensions/orchestrator/phases/review-task.test.ts +20 -0
- package/extensions/orchestrator/phases/review-task.ts +47 -14
- package/extensions/orchestrator/phases/review.test.ts +36 -0
- package/extensions/orchestrator/phases/review.ts +62 -10
- 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 +314 -1
- package/extensions/orchestrator/pp-menu.ts +937 -450
- 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 +82 -1
- package/extensions/orchestrator/state.ts +110 -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/transition-controller.test.ts +100 -0
- package/extensions/orchestrator/transition-controller.ts +61 -3
- package/extensions/orchestrator/validate-artifacts.ts +1 -1
- package/package.json +6 -2
- package/scripts/test-3p.sh +52 -0
- package/AGENTS.md +0 -28
|
@@ -1,11 +1,45 @@
|
|
|
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; error?: string }> = [];
|
|
26
|
+
const plannotatorOpenCwds: string[] = [];
|
|
27
|
+
const annotateReviewResults: Array<{ feedback: string; approved?: boolean; exit?: boolean } | null> = [];
|
|
28
|
+
const annotateReviewPayloads: any[] = [];
|
|
29
|
+
vi.mock("./plannotator.js", () => ({
|
|
30
|
+
cancelPendingPlannotatorWait: () => {},
|
|
31
|
+
openPlannotator: vi.fn(async (_pi: any, _action: string, payload: any) => {
|
|
32
|
+
plannotatorOpenCwds.push(payload?.cwd);
|
|
33
|
+
return { opened: true, reviewId: "rev" };
|
|
34
|
+
}),
|
|
35
|
+
openAnnotateReview: vi.fn(async (_pi: any, payload: any) => {
|
|
36
|
+
annotateReviewPayloads.push(payload);
|
|
37
|
+
const result = annotateReviewResults.shift() ?? { feedback: "", approved: true };
|
|
38
|
+
return { opened: true, result };
|
|
39
|
+
}),
|
|
40
|
+
waitForPlannotatorResult: vi.fn(async () => plannotatorResults.shift() ?? { approved: true }),
|
|
41
|
+
}));
|
|
42
|
+
|
|
9
43
|
const USAGE_TRACKER_SYMBOL = Symbol.for("pi-pi:usage-tracker");
|
|
10
44
|
|
|
11
45
|
afterEach(() => {
|
|
@@ -205,3 +239,282 @@ describe("pickMaxReviewPasses", () => {
|
|
|
205
239
|
expect(await pickMaxReviewPasses(makeInputCtx([""]), 3)).toBeNull();
|
|
206
240
|
});
|
|
207
241
|
});
|
|
242
|
+
|
|
243
|
+
describe("publishGuard", () => {
|
|
244
|
+
let taskDir: string;
|
|
245
|
+
|
|
246
|
+
afterEach(() => {
|
|
247
|
+
if (taskDir) rmSync(taskDir, { recursive: true, force: true });
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
function makeTask(): string {
|
|
251
|
+
taskDir = mkdtempSync(join(tmpdir(), "pp-guard-"));
|
|
252
|
+
return taskDir;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
it("blocks publishing when no review has run (no code-reviews dir)", () => {
|
|
256
|
+
const dir = makeTask();
|
|
257
|
+
const msg = publishGuard(dir);
|
|
258
|
+
expect(msg).toBeTruthy();
|
|
259
|
+
expect(msg).toContain("review pass");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("blocks publishing when a final_pass file exists but carries no ANCHORS block", () => {
|
|
263
|
+
const dir = makeTask();
|
|
264
|
+
mkdirSync(join(dir, "code-reviews"));
|
|
265
|
+
writeFileSync(join(dir, "code-reviews", "20260101-000000_final_pass-1.md"), "# Findings\n\nSome prose without anchors.\n");
|
|
266
|
+
expect(publishGuard(dir)).toBeTruthy();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("allows publishing when the latest final_pass file has an ANCHORS block", () => {
|
|
270
|
+
const dir = makeTask();
|
|
271
|
+
mkdirSync(join(dir, "code-reviews"));
|
|
272
|
+
writeFileSync(join(dir, "code-reviews", "20260101-000000_final_pass-1.md"), "ANCHORS:\nsrc/a.ts:10 — bug\n");
|
|
273
|
+
expect(publishGuard(dir)).toBeUndefined();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("resolves the newest final_pass file by name and honors its ANCHORS block", () => {
|
|
277
|
+
const dir = makeTask();
|
|
278
|
+
mkdirSync(join(dir, "code-reviews"));
|
|
279
|
+
writeFileSync(join(dir, "code-reviews", "20260101-000000_final_pass-1.md"), "ANCHORS:\nsrc/a.ts:10 — bug\n");
|
|
280
|
+
writeFileSync(join(dir, "code-reviews", "20260102-000000_final_pass-2.md"), "# Findings\n\nno anchors here\n");
|
|
281
|
+
expect(publishGuard(dir)).toBeTruthy();
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe("publish banners", () => {
|
|
286
|
+
it("PR banner posts one bundled COMMENT review per repo, pre-validated against the diff", () => {
|
|
287
|
+
const banner = publishPrCommentsBanner("/tmp/task");
|
|
288
|
+
expect(banner).toContain("pulls/<number>/reviews");
|
|
289
|
+
expect(banner).toContain("event=COMMENT");
|
|
290
|
+
expect(banner).not.toContain("event=APPROVE");
|
|
291
|
+
expect(banner).toContain("never APPROVE or REQUEST_CHANGES");
|
|
292
|
+
expect(banner).toContain("PRE-VALIDATE");
|
|
293
|
+
expect(banner).toContain("all-or-nothing");
|
|
294
|
+
expect(banner).toContain("Findings not anchorable to the diff:");
|
|
295
|
+
expect(banner).toContain("(generated by pi-pi)");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("PR banner idempotency matches on path+line+footer+body", () => {
|
|
299
|
+
const banner = publishPrCommentsBanner("/tmp/task");
|
|
300
|
+
expect(banner).toContain("footer, AND body text all match");
|
|
301
|
+
expect(banner).toContain("Two distinct findings on the same line are NOT duplicates");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("both banners carry the privacy instruction", () => {
|
|
305
|
+
for (const banner of [publishPrCommentsBanner("/tmp/task"), publishFileCommentsBanner("/tmp/task")]) {
|
|
306
|
+
expect(banner).toContain("PRIVACY:");
|
|
307
|
+
expect(banner).toContain("the ticket");
|
|
308
|
+
expect(banner).toContain("self-contained");
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
describe("showActiveTaskMenu Publish/Next Back navigation (#6)", () => {
|
|
314
|
+
let taskDir: string;
|
|
315
|
+
|
|
316
|
+
afterEach(() => {
|
|
317
|
+
askQueue.length = 0;
|
|
318
|
+
askQuestions.length = 0;
|
|
319
|
+
if (taskDir) rmSync(taskDir, { recursive: true, force: true });
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
function makeReviewOrchestrator(): any {
|
|
323
|
+
taskDir = mkdtempSync(join(tmpdir(), "pp-nav-"));
|
|
324
|
+
return {
|
|
325
|
+
active: {
|
|
326
|
+
type: "review",
|
|
327
|
+
dir: taskDir,
|
|
328
|
+
state: { phase: "review", step: "llm_work", mode: "guided" },
|
|
329
|
+
},
|
|
330
|
+
transitionController: {
|
|
331
|
+
isRunning: () => false,
|
|
332
|
+
abortMainAgent: () => {},
|
|
333
|
+
},
|
|
334
|
+
cancelPendingRetry: () => {},
|
|
335
|
+
abortAllSubagents: () => {},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const ctx = { ui: { notify: () => {} }, waitForIdle: async () => {}, abort: () => {} };
|
|
340
|
+
|
|
341
|
+
it("Publish 'Back' re-renders the Next submenu instead of the top-level menu", async () => {
|
|
342
|
+
const orchestrator = makeReviewOrchestrator();
|
|
343
|
+
// /pp -> Next -> Publish -> Back (should return to Next) -> Back (to top-level) -> Back (exit).
|
|
344
|
+
askQueue.push("Next", "Publish", "Back", "Back", "Back to prompt");
|
|
345
|
+
const result = await showActiveTaskMenu(orchestrator, ctx, "/pp", "tool");
|
|
346
|
+
expect(result).toBe("");
|
|
347
|
+
// After Publish's Back we must see the "Next" submenu rendered again before the
|
|
348
|
+
// top-level menu reappears: questions = [top, Next, Publish, Next(again), top].
|
|
349
|
+
const nextRenders = askQuestions.filter((q) => q === "Next").length;
|
|
350
|
+
expect(nextRenders).toBe(2);
|
|
351
|
+
expect(askQuestions[askQuestions.length - 1]).toContain("/pp");
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("Next 'Back' returns straight to the top-level menu", async () => {
|
|
355
|
+
const orchestrator = makeReviewOrchestrator();
|
|
356
|
+
askQueue.push("Next", "Back", "Back to prompt");
|
|
357
|
+
const result = await showActiveTaskMenu(orchestrator, ctx, "/pp", "tool");
|
|
358
|
+
expect(result).toBe("");
|
|
359
|
+
expect(askQuestions.filter((q) => q === "Next").length).toBe(1);
|
|
360
|
+
// Top-level rendered twice (initial + after Next's Back), Next once.
|
|
361
|
+
expect(askQuestions.filter((q) => q.startsWith("/pp")).length).toBe(2);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("Review submenu 'Editor review' Back returns to Review, not the top-level menu (#3d)", async () => {
|
|
365
|
+
const orchestrator = makeReviewOrchestrator();
|
|
366
|
+
// /pp -> Review -> Review on my own -> Editor review Back (should return to
|
|
367
|
+
// Review) -> Review Back (to top-level) -> top-level Back (exit).
|
|
368
|
+
askQueue.push("Review", "Review on my own", "Back", "Back", "Back to prompt");
|
|
369
|
+
const result = await showActiveTaskMenu(orchestrator, ctx, "/pp", "tool");
|
|
370
|
+
expect(result).toBe("");
|
|
371
|
+
// Review submenu rendered twice (initial + after the Editor-review Back).
|
|
372
|
+
expect(askQuestions.filter((q) => q === "Review").length).toBe(2);
|
|
373
|
+
// Top-level rendered twice (initial + after Review's explicit Back).
|
|
374
|
+
expect(askQuestions.filter((q) => q.startsWith("/pp")).length).toBe(2);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("resumes the per-repo Plannotator cursor and interleaves fixes (#3a)", async () => {
|
|
378
|
+
const orchestrator = makeReviewOrchestrator();
|
|
379
|
+
orchestrator.active.type = "implement";
|
|
380
|
+
orchestrator.active.state.phase = "implement";
|
|
381
|
+
orchestrator.active.state.repos = [
|
|
382
|
+
{ path: "/repo/a", isRoot: true },
|
|
383
|
+
{ path: "/repo/b", isRoot: false },
|
|
384
|
+
];
|
|
385
|
+
orchestrator.active.state.plannotatorCursor = { repoPaths: ["/repo/a", "/repo/b"], index: 0 };
|
|
386
|
+
orchestrator.pi = { exec: vi.fn(async () => ({ code: 0, stdout: "", stderr: "" })) };
|
|
387
|
+
orchestrator.config = getDefaultConfig();
|
|
388
|
+
|
|
389
|
+
// First repo: choose a diff scope, Plannotator returns NEEDS_CHANGES → the
|
|
390
|
+
// menu exits with a work instruction and the cursor advances to repo b.
|
|
391
|
+
plannotatorResults.push({ approved: false, feedback: "fix the thing" });
|
|
392
|
+
askQueue.push("Uncommitted changes");
|
|
393
|
+
const first = await showActiveTaskMenu(orchestrator, ctx, "/pp", "tool");
|
|
394
|
+
expect(first).toContain("Plannotator requested changes");
|
|
395
|
+
expect(first).toContain("fix the thing");
|
|
396
|
+
expect(orchestrator.active.state.plannotatorCursor).toEqual({ repoPaths: ["/repo/a", "/repo/b"], index: 1 });
|
|
397
|
+
|
|
398
|
+
// Next /pp resumes at repo b; APPROVED → cursor is cleared and the loop ends,
|
|
399
|
+
// falling through to the normal menu (top-level Back exits).
|
|
400
|
+
plannotatorResults.push({ approved: true });
|
|
401
|
+
askQueue.push("Uncommitted changes", "Back to prompt");
|
|
402
|
+
const second = await showActiveTaskMenu(orchestrator, ctx, "/pp", "tool");
|
|
403
|
+
expect(second).toBe("");
|
|
404
|
+
expect(orchestrator.active.state.plannotatorCursor).toBeUndefined();
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
function makeCursorOrchestrator(): any {
|
|
408
|
+
const orchestrator = makeReviewOrchestrator();
|
|
409
|
+
orchestrator.active.type = "implement";
|
|
410
|
+
orchestrator.active.state.phase = "implement";
|
|
411
|
+
orchestrator.active.state.repos = [{ path: "/repo/a", isRoot: true }, { path: "/repo/b", isRoot: false }];
|
|
412
|
+
orchestrator.active.state.plannotatorCursor = { repoPaths: ["/repo/a", "/repo/b"], index: 0 };
|
|
413
|
+
orchestrator.pi = { exec: vi.fn(async () => ({ code: 0, stdout: "", stderr: "" })) };
|
|
414
|
+
orchestrator.config = getDefaultConfig();
|
|
415
|
+
return orchestrator;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
it("Plannotator error + Retry leaves the cursor on the same repo (#3, error retention)", async () => {
|
|
419
|
+
const orchestrator = makeCursorOrchestrator();
|
|
420
|
+
plannotatorOpenCwds.length = 0;
|
|
421
|
+
// Repo a errors; user chooses Retry → cursor stays at index 0. Then a second
|
|
422
|
+
// attempt on repo a approves, advancing to b, which approves and clears.
|
|
423
|
+
plannotatorResults.push({ approved: false, error: "boom" });
|
|
424
|
+
plannotatorResults.push({ approved: true });
|
|
425
|
+
plannotatorResults.push({ approved: true });
|
|
426
|
+
askQueue.push("Uncommitted changes", "Retry", "Uncommitted changes", "Uncommitted changes", "Back to prompt");
|
|
427
|
+
const result = await showActiveTaskMenu(orchestrator, ctx, "/pp", "tool");
|
|
428
|
+
expect(result).toBe("");
|
|
429
|
+
// Retry re-opened repo a (opened twice) before advancing to b; none dropped.
|
|
430
|
+
expect(plannotatorOpenCwds).toEqual(["/repo/a", "/repo/a", "/repo/b"]);
|
|
431
|
+
// Both repos ultimately reviewed and the cursor cleared (none silently dropped).
|
|
432
|
+
expect(orchestrator.active.state.plannotatorCursor).toBeUndefined();
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it("Plannotator error + Skip advances the cursor past the failed repo (#3)", async () => {
|
|
436
|
+
const orchestrator = makeCursorOrchestrator();
|
|
437
|
+
plannotatorOpenCwds.length = 0;
|
|
438
|
+
// Repo a errors; Skip advances to repo b, which approves and clears the cursor.
|
|
439
|
+
plannotatorResults.push({ approved: false, error: "boom" });
|
|
440
|
+
plannotatorResults.push({ approved: true });
|
|
441
|
+
askQueue.push("Uncommitted changes", "Skip this repo", "Uncommitted changes", "Back to prompt");
|
|
442
|
+
await showActiveTaskMenu(orchestrator, ctx, "/pp", "tool");
|
|
443
|
+
// Skip moved on to repo b (each repo opened exactly once — a not retried, b reached).
|
|
444
|
+
expect(plannotatorOpenCwds).toEqual(["/repo/a", "/repo/b"]);
|
|
445
|
+
expect(orchestrator.active.state.plannotatorCursor).toBeUndefined();
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("Plannotator error + Done stops and clears the cursor (#3)", async () => {
|
|
449
|
+
const orchestrator = makeCursorOrchestrator();
|
|
450
|
+
plannotatorResults.push({ approved: false, error: "boom" });
|
|
451
|
+
askQueue.push("Uncommitted changes", "Done (stop reviewing)");
|
|
452
|
+
await showActiveTaskMenu(orchestrator, ctx, "/pp", "tool");
|
|
453
|
+
expect(orchestrator.active.state.plannotatorCursor).toBeUndefined();
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it("blocks a second review while one is already running (#3b)", async () => {
|
|
457
|
+
const orchestrator = makeReviewOrchestrator();
|
|
458
|
+
orchestrator.active.state.reviewCycle = { kind: "auto", step: "await_reviewers", pass: 1 };
|
|
459
|
+
// isRunning() true so the top-level "Review" option is offered.
|
|
460
|
+
orchestrator.transitionController.isRunning = () => true;
|
|
461
|
+
let notified = "";
|
|
462
|
+
const notifyCtx = { ui: { notify: (t: string) => { notified = t; } }, waitForIdle: async () => {}, abort: () => {} };
|
|
463
|
+
// Pick Review (blocked → notify + back to top-level menu), then Back to exit.
|
|
464
|
+
askQueue.push("Review", "Back to prompt");
|
|
465
|
+
const result = await showActiveTaskMenu(orchestrator, notifyCtx, "/pp", "tool");
|
|
466
|
+
expect(result).toBe("");
|
|
467
|
+
expect(notified).toBe("A review is already running");
|
|
468
|
+
// The live cycle is untouched (not finalized/nulled) and the top-level menu
|
|
469
|
+
// re-rendered rather than /pp exiting.
|
|
470
|
+
expect(orchestrator.active.state.reviewCycle).toEqual({ kind: "auto", step: "await_reviewers", pass: 1 });
|
|
471
|
+
expect(askQuestions.filter((q) => q.startsWith("/pp")).length).toBe(2);
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
describe("brainstorm/debug Review in Plannotator (annotate-folder) (#4)", () => {
|
|
476
|
+
let taskDir: string;
|
|
477
|
+
|
|
478
|
+
afterEach(() => {
|
|
479
|
+
askQueue.length = 0;
|
|
480
|
+
askQuestions.length = 0;
|
|
481
|
+
annotateReviewResults.length = 0;
|
|
482
|
+
annotateReviewPayloads.length = 0;
|
|
483
|
+
if (taskDir) rmSync(taskDir, { recursive: true, force: true });
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
function makeBrainstormOrchestrator(): any {
|
|
487
|
+
taskDir = mkdtempSync(join(tmpdir(), "pp-annotate-"));
|
|
488
|
+
return {
|
|
489
|
+
active: { type: "brainstorm", dir: taskDir, state: { phase: "brainstorm", step: "llm_work", mode: "guided" } },
|
|
490
|
+
transitionController: { isRunning: () => false, abortMainAgent: () => {} },
|
|
491
|
+
pi: {},
|
|
492
|
+
cancelPendingRetry: () => {},
|
|
493
|
+
abortAllSubagents: () => {},
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const ctx = { ui: { notify: () => {} }, waitForIdle: async () => {}, abort: () => {} };
|
|
498
|
+
|
|
499
|
+
it("opens an annotate-folder review over the task dir and injects feedback", async () => {
|
|
500
|
+
const orchestrator = makeBrainstormOrchestrator();
|
|
501
|
+
annotateReviewResults.push({ feedback: "tighten the scope section" });
|
|
502
|
+
askQueue.push("Review", "Review in Plannotator");
|
|
503
|
+
const result = await showActiveTaskMenu(orchestrator, ctx, "/pp", "tool");
|
|
504
|
+
expect(result).toContain("tighten the scope section");
|
|
505
|
+
expect(annotateReviewPayloads[0]).toMatchObject({
|
|
506
|
+
filePath: taskDir,
|
|
507
|
+
folderPath: taskDir,
|
|
508
|
+
mode: "annotate-folder",
|
|
509
|
+
gate: true,
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it("stays in the menu when the review is approved", async () => {
|
|
514
|
+
const orchestrator = makeBrainstormOrchestrator();
|
|
515
|
+
annotateReviewResults.push({ feedback: "", approved: true });
|
|
516
|
+
askQueue.push("Review", "Review in Plannotator", "Back", "Back to prompt");
|
|
517
|
+
const result = await showActiveTaskMenu(orchestrator, ctx, "/pp", "tool");
|
|
518
|
+
expect(result).toBe("");
|
|
519
|
+
});
|
|
520
|
+
});
|