@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.
- 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 +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 +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 +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 +579 -90
- 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 +6 -2
- package/scripts/test-3p.sh +52 -0
|
@@ -40,13 +40,18 @@ vi.mock("./cbm.js", () => ({ registerCbmTools: vi.fn() }));
|
|
|
40
40
|
vi.mock("./exa.js", () => ({ registerExaTools: vi.fn() }));
|
|
41
41
|
vi.mock("./ast-search.js", () => ({ registerAstSearchTool: vi.fn() }));
|
|
42
42
|
vi.mock("./doctor.js", () => ({ runDoctor: vi.fn(async () => undefined) }));
|
|
43
|
-
vi.mock("./agents/registry.js", () =>
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
vi.mock("./agents/registry.js", async (importOriginal) => {
|
|
44
|
+
const actual = await importOriginal<typeof import("./agents/registry.js")>();
|
|
45
|
+
return {
|
|
46
|
+
...actual,
|
|
47
|
+
registerAgentDefinitions: vi.fn(),
|
|
48
|
+
unregisterAgentDefinitions: vi.fn(),
|
|
49
|
+
setExtensionOnlyMode: vi.fn(),
|
|
50
|
+
getAgentConfigSnapshot: vi.fn(() => undefined),
|
|
51
|
+
spawnViaRpc: vi.fn(async (_pi: any, _type: string) => ({ id: `mock-${Math.random().toString(36).slice(2)}` })),
|
|
52
|
+
waitForCompletion: vi.fn(async () => undefined),
|
|
53
|
+
};
|
|
54
|
+
});
|
|
50
55
|
|
|
51
56
|
vi.mock("./config.js", async (importOriginal) => {
|
|
52
57
|
const original = await importOriginal<typeof import("./config.js")>();
|
|
@@ -58,6 +63,7 @@ vi.mock("./config.js", async (importOriginal) => {
|
|
|
58
63
|
logLevel: "info",
|
|
59
64
|
},
|
|
60
65
|
agents: {
|
|
66
|
+
maxConcurrentSubagents: 7,
|
|
61
67
|
orchestrators: {
|
|
62
68
|
implement: { model: "test/model", thinking: "high" },
|
|
63
69
|
plan: { model: "test/model", thinking: "high" },
|
|
@@ -71,9 +77,14 @@ vi.mock("./config.js", async (importOriginal) => {
|
|
|
71
77
|
explore: { model: "test/explore", thinking: "low" },
|
|
72
78
|
librarian: { model: "test/librarian", thinking: "medium" },
|
|
73
79
|
task: { model: "test/task", thinking: "medium" },
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
80
|
+
},
|
|
81
|
+
pools: {
|
|
82
|
+
advisors: [
|
|
83
|
+
{ enabled: true, model: "test/advisor-fable", thinking: "high" },
|
|
84
|
+
{ enabled: true, model: "test/advisor-gpt", thinking: "high" },
|
|
85
|
+
],
|
|
86
|
+
reviewers: [{ enabled: true, model: "test/reviewer", thinking: "high" }],
|
|
87
|
+
deepDebuggers: [{ enabled: true, model: "test/deep-debugger", thinking: "high" }],
|
|
77
88
|
},
|
|
78
89
|
presetGroups: {
|
|
79
90
|
planners: {
|
|
@@ -215,6 +226,7 @@ function makeConfig() {
|
|
|
215
226
|
logLevel: "info",
|
|
216
227
|
},
|
|
217
228
|
agents: {
|
|
229
|
+
maxConcurrentSubagents: 7,
|
|
218
230
|
orchestrators: {
|
|
219
231
|
implement: { model: "test/model", thinking: "high" },
|
|
220
232
|
plan: { model: "test/model", thinking: "high" },
|
|
@@ -228,9 +240,14 @@ function makeConfig() {
|
|
|
228
240
|
explore: { model: "test/explore", thinking: "low" },
|
|
229
241
|
librarian: { model: "test/librarian", thinking: "medium" },
|
|
230
242
|
task: { model: "test/task", thinking: "medium" },
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
243
|
+
},
|
|
244
|
+
pools: {
|
|
245
|
+
advisors: [
|
|
246
|
+
{ enabled: true, model: "test/advisor-fable", thinking: "high" },
|
|
247
|
+
{ enabled: true, model: "test/advisor-gpt", thinking: "high" },
|
|
248
|
+
],
|
|
249
|
+
reviewers: [{ enabled: true, model: "test/reviewer", thinking: "high" }],
|
|
250
|
+
deepDebuggers: [{ enabled: true, model: "test/deep-debugger", thinking: "high" }],
|
|
234
251
|
},
|
|
235
252
|
presetGroups: {
|
|
236
253
|
planners: {
|
|
@@ -1317,6 +1334,49 @@ describe("edge cases and regressions", () => {
|
|
|
1317
1334
|
expect(finalState.reviewCycle).toBeNull();
|
|
1318
1335
|
});
|
|
1319
1336
|
|
|
1337
|
+
it("manual auto-review (item 5) loops in a guided phase then stops in-phase on clean approve", async () => {
|
|
1338
|
+
const cwd = makeTempDir();
|
|
1339
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
1340
|
+
const ctx = makeCtx();
|
|
1341
|
+
|
|
1342
|
+
// Guided implement task, parked in the implement phase.
|
|
1343
|
+
await orchestrator.startTask(ctx as any, "implement", "Manual auto-review test", undefined, undefined, "guided");
|
|
1344
|
+
const taskDir = orchestrator.active!.dir;
|
|
1345
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
1346
|
+
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
1347
|
+
const plansDir = join(taskDir, "plans");
|
|
1348
|
+
mkdirSync(plansDir, { recursive: true });
|
|
1349
|
+
writeFileSync(
|
|
1350
|
+
join(plansDir, `${Math.floor(Date.now() / 1000)}_synthesized.md`),
|
|
1351
|
+
makeValidPlan(["- [x] P1. Done item — Done when: synthesized plan is fully checked"]),
|
|
1352
|
+
"utf-8",
|
|
1353
|
+
);
|
|
1354
|
+
orchestrator.active!.state.phase = "implement";
|
|
1355
|
+
orchestrator.active!.state.step = "llm_work";
|
|
1356
|
+
// Arm the item-5 flag (advanceOnComplete=false): stop in-phase on approve.
|
|
1357
|
+
orchestrator.active!.state.manualAutoReview = { phase: "implement", preset: "regular", maxPasses: 3, advanceOnComplete: false };
|
|
1358
|
+
saveTask(taskDir, orchestrator.active!.state);
|
|
1359
|
+
|
|
1360
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
1361
|
+
// First call: enters the review cycle (spawns reviewers).
|
|
1362
|
+
const first = await ppPhaseComplete.execute("m-1", { summary: "implemented" }, undefined, undefined, ctx);
|
|
1363
|
+
expect(first.content[0].text).toContain("pass 1/3");
|
|
1364
|
+
expect(orchestrator.active!.state.reviewCycle).not.toBeNull();
|
|
1365
|
+
|
|
1366
|
+
// Reviewer approves.
|
|
1367
|
+
const reviewsDir = join(taskDir, "code-reviews");
|
|
1368
|
+
mkdirSync(reviewsDir, { recursive: true });
|
|
1369
|
+
writeFileSync(join(reviewsDir, `${Math.floor(Date.now() / 1000)}_test_round-1.md`), "VERDICT: APPROVE", "utf-8");
|
|
1370
|
+
emitSubagentCreated(pi, "reviewer-1", "Code reviewer (test)");
|
|
1371
|
+
emitSubagentCompleted(pi, "reviewer-1", "Code reviewer (test)");
|
|
1372
|
+
|
|
1373
|
+
// Second call: finalizes the clean pass; item 5 stops in-phase and clears the flag.
|
|
1374
|
+
const second = await ppPhaseComplete.execute("m-2", { summary: "fixes applied" }, undefined, undefined, ctx);
|
|
1375
|
+
expect(second.content[0].text).toContain("Staying in the implement phase");
|
|
1376
|
+
expect(orchestrator.active!.state.phase).toBe("implement");
|
|
1377
|
+
expect(orchestrator.active!.state.manualAutoReview).toBeUndefined();
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1320
1380
|
it("continue brainstorming sets step back to llm_work", async () => {
|
|
1321
1381
|
const cwd = makeTempDir();
|
|
1322
1382
|
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
@@ -1678,7 +1738,7 @@ describe("task modes and quick task", () => {
|
|
|
1678
1738
|
const ctx = makeCtx();
|
|
1679
1739
|
|
|
1680
1740
|
await orchestrator.startTask(ctx as any, "quick", "quick");
|
|
1681
|
-
menu.expect({ question: m.taskMenu("quick", "quick"), options: { include: ["Complete", "Pause"], exclude: ["Next", "Review"] }, choose: "Back" });
|
|
1741
|
+
menu.expect({ question: m.taskMenu("quick", "quick"), options: { include: ["Complete", "Pause"], exclude: ["Next", "Review"] }, choose: "Back to prompt" });
|
|
1682
1742
|
const pp = getCommand(pi, "pp");
|
|
1683
1743
|
await pp(undefined, ctx);
|
|
1684
1744
|
});
|
|
@@ -1796,7 +1856,7 @@ describe("task modes and quick task", () => {
|
|
|
1796
1856
|
expect(orchestrator.transitionController.isRunning()).toBe(true);
|
|
1797
1857
|
|
|
1798
1858
|
// Explicit "Back" navigation (not an ESC): the reminder must be preserved.
|
|
1799
|
-
menu.expect({ question: m.anyTaskMenu, options: { include: ["Back"] }, choose: "Back" });
|
|
1859
|
+
menu.expect({ question: m.anyTaskMenu, options: { include: ["Back to prompt"] }, choose: "Back to prompt" });
|
|
1800
1860
|
|
|
1801
1861
|
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
1802
1862
|
const result = await ppPhaseComplete.execute("back-1", { summary: "done" }, undefined, undefined, ctx);
|
|
@@ -2130,7 +2190,7 @@ describe("task modes and quick task", () => {
|
|
|
2130
2190
|
.expect({ question: "Implement", options: { include: ["Resume", "Back"] }, choose: "Resume" })
|
|
2131
2191
|
.expect({ question: "Implement", options: { include: ["Back"] }, choose: "Back" })
|
|
2132
2192
|
.expect({ question: "Task", options: { include: ["Back"] }, choose: "Back" })
|
|
2133
|
-
.expect({ question: "/pp", options: { include: ["Back"] }, choose: "Back" });
|
|
2193
|
+
.expect({ question: "/pp", options: { include: ["Back to prompt"] }, choose: "Back to prompt" });
|
|
2134
2194
|
const pp = getCommand(pi, "pp");
|
|
2135
2195
|
await pp(undefined, ctx);
|
|
2136
2196
|
|
|
@@ -2194,7 +2254,8 @@ describe("task modes and quick task", () => {
|
|
|
2194
2254
|
const beforeStart = pi._handlers.get("before_agent_start")!;
|
|
2195
2255
|
const result = await beforeStart({ systemPrompt: "HARNESS_BASE_PROMPT" }, ctx);
|
|
2196
2256
|
const prompt = result?.systemPrompt ?? "";
|
|
2197
|
-
expect(prompt.startsWith("<
|
|
2257
|
+
expect(prompt.startsWith("<identity>")).toBe(true);
|
|
2258
|
+
expect(prompt).toContain("<constraints>");
|
|
2198
2259
|
expect(prompt).not.toContain("HARNESS_BASE_PROMPT");
|
|
2199
2260
|
expect(prompt).toContain("The moment its work is complete, call pp_phase_complete");
|
|
2200
2261
|
// No interactive '/pp menu' advance guidance in autonomous mode.
|
|
@@ -2213,7 +2274,8 @@ describe("task modes and quick task", () => {
|
|
|
2213
2274
|
|
|
2214
2275
|
const beforeStart = pi._handlers.get("before_agent_start")!;
|
|
2215
2276
|
const prompt = (await beforeStart({ systemPrompt: "HARNESS_BASE_PROMPT" }, ctx))?.systemPrompt ?? "";
|
|
2216
|
-
expect(prompt.startsWith("<
|
|
2277
|
+
expect(prompt.startsWith("<identity>")).toBe(true);
|
|
2278
|
+
expect(prompt).toContain("<constraints>");
|
|
2217
2279
|
expect(prompt).toContain("ACTIVE PHASE: plan (READ-ONLY)");
|
|
2218
2280
|
expect(prompt).toContain("<principles>");
|
|
2219
2281
|
expect(prompt).toContain("<tools>");
|
|
@@ -2351,6 +2413,52 @@ describe("task modes and quick task", () => {
|
|
|
2351
2413
|
expect(state.reviewerFailureAutoRetried).toBe(false);
|
|
2352
2414
|
});
|
|
2353
2415
|
|
|
2416
|
+
it("captures the initiating prompt into a generic-description task name (#7)", async () => {
|
|
2417
|
+
const cwd = makeTempDir();
|
|
2418
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
2419
|
+
const ctx = makeCtx({ cwd });
|
|
2420
|
+
|
|
2421
|
+
await orchestrator.startTask({ ...ctx, cwd } as any, "review", "review", undefined, undefined, "guided");
|
|
2422
|
+
const beforeStart = pi._handlers.get("before_agent_start")!;
|
|
2423
|
+
await beforeStart({ systemPrompt: "base", prompt: " Review the payment refactor PR " }, ctx);
|
|
2424
|
+
expect(orchestrator.active!.state.description).toBe("Review the payment refactor PR");
|
|
2425
|
+
expect(orchestrator.active!.description).toBe("Review the payment refactor PR");
|
|
2426
|
+
});
|
|
2427
|
+
|
|
2428
|
+
it("does not overwrite a non-generic description or capture a [PI-PI] injection", async () => {
|
|
2429
|
+
const cwd = makeTempDir();
|
|
2430
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
2431
|
+
const ctx = makeCtx({ cwd });
|
|
2432
|
+
|
|
2433
|
+
await orchestrator.startTask({ ...ctx, cwd } as any, "review", "review", undefined, undefined, "guided");
|
|
2434
|
+
const beforeStart = pi._handlers.get("before_agent_start")!;
|
|
2435
|
+
await beforeStart({ systemPrompt: "base", prompt: "[PI-PI] Continue working on the current phase" }, ctx);
|
|
2436
|
+
expect(orchestrator.active!.state.description).toBe("review");
|
|
2437
|
+
});
|
|
2438
|
+
|
|
2439
|
+
it("collapses a multi-line initiating prompt into one line (#8), bounded by the 700-char cap", async () => {
|
|
2440
|
+
const cwd = makeTempDir();
|
|
2441
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
2442
|
+
const ctx = makeCtx({ cwd });
|
|
2443
|
+
|
|
2444
|
+
await orchestrator.startTask({ ...ctx, cwd } as any, "review", "review", undefined, undefined, "guided");
|
|
2445
|
+
const beforeStart = pi._handlers.get("before_agent_start")!;
|
|
2446
|
+
await beforeStart({ systemPrompt: "base", prompt: "Review the auth refactor\nand the token store\nfor race conditions" }, ctx);
|
|
2447
|
+
expect(orchestrator.active!.state.description).toBe("Review the auth refactor and the token store for race conditions");
|
|
2448
|
+
});
|
|
2449
|
+
|
|
2450
|
+
it("caps the collapsed multi-line description at 700 chars (#8)", async () => {
|
|
2451
|
+
const cwd = makeTempDir();
|
|
2452
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
2453
|
+
const ctx = makeCtx({ cwd });
|
|
2454
|
+
|
|
2455
|
+
await orchestrator.startTask({ ...ctx, cwd } as any, "review", "review", undefined, undefined, "guided");
|
|
2456
|
+
const beforeStart = pi._handlers.get("before_agent_start")!;
|
|
2457
|
+
const longPrompt = "line one\n" + "x".repeat(2000);
|
|
2458
|
+
await beforeStart({ systemPrompt: "base", prompt: longPrompt }, ctx);
|
|
2459
|
+
expect(orchestrator.active!.state.description.length).toBe(700);
|
|
2460
|
+
});
|
|
2461
|
+
|
|
2354
2462
|
it("blocks ask_user in autonomous mode after first phase", async () => {
|
|
2355
2463
|
const cwd = makeTempDir();
|
|
2356
2464
|
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
@@ -3462,10 +3570,10 @@ describe("tool blocking", () => {
|
|
|
3462
3570
|
});
|
|
3463
3571
|
|
|
3464
3572
|
describe("error retry", () => {
|
|
3465
|
-
//
|
|
3466
|
-
//
|
|
3467
|
-
//
|
|
3468
|
-
//
|
|
3573
|
+
// Both SDK-retryable and non-SDK-retryable errors now exercise pi-pi's OWN
|
|
3574
|
+
// idle-gated retry. By the time turn_end fires, the SDK's short in-prompt
|
|
3575
|
+
// retry budget is already exhausted, so pi-pi's longer retry (8×, up to ~4min)
|
|
3576
|
+
// takes over instead of the session silently dying after the SDK's ~14s budget.
|
|
3469
3577
|
it("turn_end with a non-SDK-retryable error uses pi-pi's idle-gated retry", async () => {
|
|
3470
3578
|
vi.useFakeTimers();
|
|
3471
3579
|
const cwd = makeTempDir();
|
|
@@ -3478,28 +3586,33 @@ describe("error retry", () => {
|
|
|
3478
3586
|
await turnEnd({ message: { stopReason: "error", errorMessage: "invalid tool arguments", content: [] } }, ctx);
|
|
3479
3587
|
|
|
3480
3588
|
expect(orchestrator.errorRetryCount).toBe(1);
|
|
3481
|
-
expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("Retrying in 2s"), "
|
|
3589
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("Retrying in 2s"), "info");
|
|
3482
3590
|
|
|
3483
3591
|
await vi.advanceTimersByTimeAsync(2000);
|
|
3484
|
-
expect(pi.sendUserMessage).toHaveBeenCalledWith(expect.stringContaining("
|
|
3592
|
+
expect(pi.sendUserMessage).toHaveBeenCalledWith(expect.stringContaining("Transient API error"), { deliverAs: "followUp" });
|
|
3485
3593
|
vi.useRealTimers();
|
|
3486
3594
|
});
|
|
3487
3595
|
|
|
3488
|
-
it("turn_end
|
|
3596
|
+
it("turn_end retries SDK-retryable errors via pi-pi's idle-gated retry", async () => {
|
|
3597
|
+
vi.useFakeTimers();
|
|
3489
3598
|
const cwd = makeTempDir();
|
|
3490
3599
|
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
3491
3600
|
const ctx = makeCtx();
|
|
3492
3601
|
|
|
3493
|
-
await orchestrator.startTask(ctx as any, "implement", "sdk
|
|
3602
|
+
await orchestrator.startTask(ctx as any, "implement", "sdk retryable test");
|
|
3494
3603
|
const turnEnd = pi._handlers.get("turn_end")!;
|
|
3495
3604
|
|
|
3496
|
-
// "stream ended before message_stop" is SDK-retryable
|
|
3497
|
-
//
|
|
3605
|
+
// "stream ended before message_stop" is SDK-retryable, but by turn_end the
|
|
3606
|
+
// SDK's own short retry budget is exhausted, so pi-pi's longer retry takes
|
|
3607
|
+
// over rather than letting the autonomous session die.
|
|
3498
3608
|
await turnEnd({ message: { stopReason: "error", errorMessage: "Anthropic stream ended before message_stop", content: [] } }, ctx);
|
|
3499
3609
|
|
|
3500
|
-
expect(orchestrator.errorRetryCount).toBe(
|
|
3501
|
-
expect(
|
|
3502
|
-
|
|
3610
|
+
expect(orchestrator.errorRetryCount).toBe(1);
|
|
3611
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("Retrying in 2s"), "info");
|
|
3612
|
+
|
|
3613
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
3614
|
+
expect(pi.sendUserMessage).toHaveBeenCalledWith(expect.stringContaining("Transient API error"), { deliverAs: "followUp" });
|
|
3615
|
+
vi.useRealTimers();
|
|
3503
3616
|
});
|
|
3504
3617
|
|
|
3505
3618
|
it("turn_end stops retrying after max retries (non-SDK-retryable)", async () => {
|
|
@@ -3511,7 +3624,8 @@ describe("error retry", () => {
|
|
|
3511
3624
|
await orchestrator.startTask(ctx as any, "implement", "retry max test");
|
|
3512
3625
|
const turnEnd = pi._handlers.get("turn_end")!;
|
|
3513
3626
|
|
|
3514
|
-
|
|
3627
|
+
// 8 retries then a 9th error turn that trips the halt.
|
|
3628
|
+
for (let i = 0; i < 9; i++) {
|
|
3515
3629
|
await turnEnd({ message: { stopReason: "error", errorMessage: "invalid tool arguments", content: [] } }, ctx);
|
|
3516
3630
|
}
|
|
3517
3631
|
|
|
@@ -4038,7 +4152,7 @@ describe("session lifecycle", () => {
|
|
|
4038
4152
|
expect(toolNames).toContain("pp_register_repo");
|
|
4039
4153
|
});
|
|
4040
4154
|
|
|
4041
|
-
it("session_start
|
|
4155
|
+
it("session_start does not notify about paused tasks (#1: all hints removed)", async () => {
|
|
4042
4156
|
const cwd = makeTempDir();
|
|
4043
4157
|
createTask(cwd, "implement", "Paused task");
|
|
4044
4158
|
const pi = makePi();
|
|
@@ -4050,7 +4164,7 @@ describe("session lifecycle", () => {
|
|
|
4050
4164
|
const sessionStart = pi._handlers.get("session_start")!;
|
|
4051
4165
|
await sessionStart({}, ctx);
|
|
4052
4166
|
|
|
4053
|
-
expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("Paused task"), "info");
|
|
4167
|
+
expect(ctx.ui.notify).not.toHaveBeenCalledWith(expect.stringContaining("Paused task"), "info");
|
|
4054
4168
|
});
|
|
4055
4169
|
|
|
4056
4170
|
it("session_start loads config", async () => {
|
|
@@ -4082,29 +4196,31 @@ describe("menu contracts", () => {
|
|
|
4082
4196
|
|
|
4083
4197
|
menu
|
|
4084
4198
|
.expect({ question: "/pp", options: { include: ["Settings"] }, choose: "Settings" })
|
|
4085
|
-
.expect({ question: "Settings", options: { exact: ["General", "Agents", "Commands", "Performance", "LSP", "Back"] }, choose: "Back" })
|
|
4086
|
-
.expect({ question: "/pp", options: { include: ["Back"] }, choose: "Back" });
|
|
4199
|
+
.expect({ question: "Settings", options: { exact: ["General", "Agents", "Commands", "Performance", "LSP", "Flant", "Info", "Back"] }, choose: "Back" })
|
|
4200
|
+
.expect({ question: "/pp", options: { include: ["Back to prompt"] }, choose: "Back to prompt" });
|
|
4087
4201
|
|
|
4088
4202
|
const pp = getCommand(pi, "pp");
|
|
4089
4203
|
await pp(undefined, ctx);
|
|
4090
4204
|
});
|
|
4091
4205
|
|
|
4092
|
-
it("info menu shows Doctor and hides LSP", async () => {
|
|
4206
|
+
it("info menu (under Settings) shows Doctor and hides LSP", async () => {
|
|
4093
4207
|
const cwd = makeTempDir();
|
|
4094
4208
|
const { pi } = await setupOrchestrator(cwd);
|
|
4095
4209
|
const ctx = makeCtx();
|
|
4096
4210
|
|
|
4097
4211
|
menu
|
|
4098
|
-
.expect({ question: "/pp", options: { include: ["
|
|
4212
|
+
.expect({ question: "/pp", options: { include: ["Settings"] }, choose: "Settings" })
|
|
4213
|
+
.expect({ question: "Settings", options: { include: ["Info"] }, choose: "Info" })
|
|
4099
4214
|
.expect({
|
|
4100
4215
|
question: "Info",
|
|
4101
4216
|
options: {
|
|
4102
|
-
include: ["
|
|
4103
|
-
exclude: ["LSP"],
|
|
4217
|
+
include: ["Usage", "Doctor", "Back"],
|
|
4218
|
+
exclude: ["LSP", "Subagents"],
|
|
4104
4219
|
},
|
|
4105
4220
|
choose: "Back",
|
|
4106
4221
|
})
|
|
4107
|
-
.expect({ question: "
|
|
4222
|
+
.expect({ question: "Settings", options: { include: ["Back"] }, choose: "Back" })
|
|
4223
|
+
.expect({ question: "/pp", options: { include: ["Back to prompt"] }, choose: "Back to prompt" });
|
|
4108
4224
|
|
|
4109
4225
|
const pp = getCommand(pi, "pp");
|
|
4110
4226
|
await pp(undefined, ctx);
|
|
@@ -4117,10 +4233,12 @@ describe("menu contracts", () => {
|
|
|
4117
4233
|
const runDoctorSpy = vi.mocked(doctorModule.runDoctor);
|
|
4118
4234
|
|
|
4119
4235
|
menu
|
|
4120
|
-
.expect({ question: "/pp", options: { include: ["
|
|
4236
|
+
.expect({ question: "/pp", options: { include: ["Settings"] }, choose: "Settings" })
|
|
4237
|
+
.expect({ question: "Settings", options: { include: ["Info"] }, choose: "Info" })
|
|
4121
4238
|
.expect({ question: "Info", options: { include: ["Doctor", "Back"] }, choose: "Doctor" })
|
|
4122
4239
|
.expect({ question: "Info", options: { include: ["Back"] }, choose: "Back" })
|
|
4123
|
-
.expect({ question: "
|
|
4240
|
+
.expect({ question: "Settings", options: { include: ["Back"] }, choose: "Back" })
|
|
4241
|
+
.expect({ question: "/pp", options: { include: ["Back to prompt"] }, choose: "Back to prompt" });
|
|
4124
4242
|
|
|
4125
4243
|
const pp = getCommand(pi, "pp");
|
|
4126
4244
|
await pp(undefined, ctx);
|
|
@@ -4142,7 +4260,7 @@ describe("menu contracts", () => {
|
|
|
4142
4260
|
.expect({ question: "LSP", options: { exact: ["Restart all servers", "Back"] }, choose: "Restart all servers" })
|
|
4143
4261
|
.expect({ question: "LSP", options: { include: ["Back"] }, choose: "Back" })
|
|
4144
4262
|
.expect({ question: "Settings", options: { include: ["Back"] }, choose: "Back" })
|
|
4145
|
-
.expect({ question: "/pp", options: { include: ["Back"] }, choose: "Back" });
|
|
4263
|
+
.expect({ question: "/pp", options: { include: ["Back to prompt"] }, choose: "Back to prompt" });
|
|
4146
4264
|
|
|
4147
4265
|
const pp = getCommand(pi, "pp");
|
|
4148
4266
|
await pp(undefined, ctx);
|
|
@@ -4162,7 +4280,7 @@ describe("menu contracts", () => {
|
|
|
4162
4280
|
.expect({ question: "LSP", options: { exact: ["Restart all servers", "Back"] }, choose: "Restart all servers" })
|
|
4163
4281
|
.expect({ question: "LSP", options: { include: ["Back"] }, choose: "Back" })
|
|
4164
4282
|
.expect({ question: "Settings", options: { include: ["Back"] }, choose: "Back" })
|
|
4165
|
-
.expect({ question: "/pp", options: { include: ["Back"] }, choose: "Back" });
|
|
4283
|
+
.expect({ question: "/pp", options: { include: ["Back to prompt"] }, choose: "Back to prompt" });
|
|
4166
4284
|
|
|
4167
4285
|
const pp = getCommand(pi, "pp");
|
|
4168
4286
|
await pp(undefined, ctx);
|
|
@@ -4178,9 +4296,9 @@ describe("menu contracts", () => {
|
|
|
4178
4296
|
menu
|
|
4179
4297
|
.expect({ question: "/pp", options: { include: ["Settings"] }, choose: "Settings" })
|
|
4180
4298
|
.expect({ question: "Settings", options: { include: ["Agents", "Back"] }, choose: "Agents" })
|
|
4181
|
-
.expect({ question: "Agents", options: { exact: ["Orchestrators", "Subagents", "Back"] }, choose: "Back" })
|
|
4299
|
+
.expect({ question: "Agents", options: { exact: ["Orchestrators", "Subagents", "Max concurrent subagents: 7", "Back"] }, choose: "Back" })
|
|
4182
4300
|
.expect({ question: "Settings", options: { include: ["Back"] }, choose: "Back" })
|
|
4183
|
-
.expect({ question: "/pp", options: { include: ["Back"] }, choose: "Back" });
|
|
4301
|
+
.expect({ question: "/pp", options: { include: ["Back to prompt"] }, choose: "Back to prompt" });
|
|
4184
4302
|
|
|
4185
4303
|
const pp = getCommand(pi, "pp");
|
|
4186
4304
|
await pp(undefined, ctx);
|
|
@@ -4196,9 +4314,9 @@ describe("menu contracts", () => {
|
|
|
4196
4314
|
menu.expect({
|
|
4197
4315
|
question: m.taskMenu("implement", "brainstorm"),
|
|
4198
4316
|
options: {
|
|
4199
|
-
exact: ["Next", "Review", "
|
|
4317
|
+
exact: ["Next", "Review", "Subagents", "Settings", "Back to prompt"],
|
|
4200
4318
|
},
|
|
4201
|
-
choose: "Back",
|
|
4319
|
+
choose: "Back to prompt",
|
|
4202
4320
|
});
|
|
4203
4321
|
|
|
4204
4322
|
const pp = getCommand(pi, "pp");
|
|
@@ -4218,9 +4336,9 @@ describe("menu contracts", () => {
|
|
|
4218
4336
|
menu.expect({
|
|
4219
4337
|
question: m.taskMenu("implement", "plan"),
|
|
4220
4338
|
options: {
|
|
4221
|
-
exact: ["Next", "
|
|
4339
|
+
exact: ["Next", "Subagents", "Settings", "Back to prompt"],
|
|
4222
4340
|
},
|
|
4223
|
-
choose: "Back",
|
|
4341
|
+
choose: "Back to prompt",
|
|
4224
4342
|
});
|
|
4225
4343
|
|
|
4226
4344
|
const pp = getCommand(pi, "pp");
|
|
@@ -4239,9 +4357,9 @@ describe("menu contracts", () => {
|
|
|
4239
4357
|
menu.expect({
|
|
4240
4358
|
question: m.taskMenu("implement", "brainstorm"),
|
|
4241
4359
|
options: {
|
|
4242
|
-
exact: ["Next", "Review", "
|
|
4360
|
+
exact: ["Next", "Review", "Subagents", "Settings", "Back to prompt"],
|
|
4243
4361
|
},
|
|
4244
|
-
choose: "Back",
|
|
4362
|
+
choose: "Back to prompt",
|
|
4245
4363
|
});
|
|
4246
4364
|
|
|
4247
4365
|
const pp = getCommand(pi, "pp");
|
|
@@ -4258,9 +4376,9 @@ describe("menu contracts", () => {
|
|
|
4258
4376
|
menu.expect({
|
|
4259
4377
|
question: m.taskMenu("debug", "debug"),
|
|
4260
4378
|
options: {
|
|
4261
|
-
exact: ["Next", "Review", "
|
|
4379
|
+
exact: ["Next", "Review", "Subagents", "Settings", "Back to prompt"],
|
|
4262
4380
|
},
|
|
4263
|
-
choose: "Back",
|
|
4381
|
+
choose: "Back to prompt",
|
|
4264
4382
|
});
|
|
4265
4383
|
|
|
4266
4384
|
const pp = getCommand(pi, "pp");
|
|
@@ -4277,9 +4395,9 @@ describe("menu contracts", () => {
|
|
|
4277
4395
|
menu.expect({
|
|
4278
4396
|
question: m.taskMenu("review", "review"),
|
|
4279
4397
|
options: {
|
|
4280
|
-
exact: ["Next", "Review", "
|
|
4398
|
+
exact: ["Next", "Review", "Subagents", "Settings", "Back to prompt"],
|
|
4281
4399
|
},
|
|
4282
|
-
choose: "Back",
|
|
4400
|
+
choose: "Back to prompt",
|
|
4283
4401
|
});
|
|
4284
4402
|
|
|
4285
4403
|
const pp = getCommand(pi, "pp");
|
|
@@ -4378,9 +4496,9 @@ describe("menu contracts", () => {
|
|
|
4378
4496
|
menu.expect({
|
|
4379
4497
|
question: m.taskMenu("quick", "quick"),
|
|
4380
4498
|
options: {
|
|
4381
|
-
exact: ["Complete", "Pause", "
|
|
4499
|
+
exact: ["Complete", "Pause", "Subagents", "Settings", "Back to prompt"],
|
|
4382
4500
|
},
|
|
4383
|
-
choose: "Back",
|
|
4501
|
+
choose: "Back to prompt",
|
|
4384
4502
|
});
|
|
4385
4503
|
|
|
4386
4504
|
const pp = getCommand(pi, "pp");
|
|
@@ -4395,8 +4513,8 @@ describe("menu contracts", () => {
|
|
|
4395
4513
|
await orchestrator.startTask(ctx as any, "implement", "contract next");
|
|
4396
4514
|
menu
|
|
4397
4515
|
.expect({ question: m.taskMenu("implement", "brainstorm"), options: { include: ["Next"] }, choose: "Next" })
|
|
4398
|
-
.expect({ question: "Next", options: { exact: ["Continue to plan & implement", "Complete", "Pause", "Back"] }, choose: "Back" })
|
|
4399
|
-
.expect({ question: m.taskMenu("implement", "brainstorm"), options: { include: ["Back"] }, choose: "Back" });
|
|
4516
|
+
.expect({ question: "Next", options: { exact: ["Continue to plan & implement", "Auto review, then continue to plan & implement", "Complete", "Pause", "Back"] }, choose: "Back" })
|
|
4517
|
+
.expect({ question: m.taskMenu("implement", "brainstorm"), options: { include: ["Back to prompt"] }, choose: "Back to prompt" });
|
|
4400
4518
|
|
|
4401
4519
|
const pp = getCommand(pi, "pp");
|
|
4402
4520
|
await pp(undefined, ctx);
|
|
@@ -4408,7 +4526,7 @@ describe("menu contracts", () => {
|
|
|
4408
4526
|
menu
|
|
4409
4527
|
.expect({ question: m.taskMenu("implement", "implement"), options: { include: ["Next"] }, choose: "Next" })
|
|
4410
4528
|
.expect({ question: "Next", options: { exact: ["Complete", "Pause", "Back"] }, choose: "Back" })
|
|
4411
|
-
.expect({ question: m.taskMenu("implement", "implement"), options: { include: ["Back"] }, choose: "Back" });
|
|
4529
|
+
.expect({ question: m.taskMenu("implement", "implement"), options: { include: ["Back to prompt"] }, choose: "Back to prompt" });
|
|
4412
4530
|
|
|
4413
4531
|
await pp(undefined, ctx);
|
|
4414
4532
|
});
|
|
@@ -4422,8 +4540,8 @@ describe("menu contracts", () => {
|
|
|
4422
4540
|
|
|
4423
4541
|
menu
|
|
4424
4542
|
.expect({ question: m.taskMenu("implement", "brainstorm"), options: { include: ["Review"] }, choose: "Review" })
|
|
4425
|
-
.expect({ question: "Review", options: { exact: [m.autoReview, "Review on my own", "Back"] }, choose: "Back" })
|
|
4426
|
-
.expect({ question: m.taskMenu("implement", "brainstorm"), options: { include: ["Back"] }, choose: "Back" });
|
|
4543
|
+
.expect({ question: "Review", options: { exact: [m.autoReview, "Auto review until approved", "Review in Plannotator", "Review on my own", "Back"] }, choose: "Back" })
|
|
4544
|
+
.expect({ question: m.taskMenu("implement", "brainstorm"), options: { include: ["Back to prompt"] }, choose: "Back to prompt" });
|
|
4427
4545
|
|
|
4428
4546
|
const pp = getCommand(pi, "pp");
|
|
4429
4547
|
await pp(undefined, ctx);
|
|
@@ -4441,7 +4559,7 @@ describe("menu contracts", () => {
|
|
|
4441
4559
|
.expect({ question: "Mode", options: { exact: ["Guided", "Autonomous", "Back"] }, choose: "Back" })
|
|
4442
4560
|
.expect({ question: "Implement", options: { include: ["Back"] }, choose: "Back" })
|
|
4443
4561
|
.expect({ question: "Task", options: { include: ["Back"] }, choose: "Back" })
|
|
4444
|
-
.expect({ question: "/pp", options: { include: ["Back"] }, choose: "Back" });
|
|
4562
|
+
.expect({ question: "/pp", options: { include: ["Back to prompt"] }, choose: "Back to prompt" });
|
|
4445
4563
|
|
|
4446
4564
|
const pp = getCommand(pi, "pp");
|
|
4447
4565
|
await pp(undefined, ctx);
|
|
@@ -248,9 +248,10 @@ describe("model-registry", () => {
|
|
|
248
248
|
|
|
249
249
|
it("getModelFamilies returns all family definitions", () => {
|
|
250
250
|
const families = getModelFamilies();
|
|
251
|
-
expect(families).toHaveLength(
|
|
251
|
+
expect(families).toHaveLength(11);
|
|
252
252
|
expect(families.map((f) => f.family).sort()).toEqual([
|
|
253
253
|
"deepseek",
|
|
254
|
+
"fable",
|
|
254
255
|
"gemini-flash",
|
|
255
256
|
"gemini-pro",
|
|
256
257
|
"gpt",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { compareModelVersion } from "./model-version.js";
|
|
2
2
|
|
|
3
3
|
export type Vendor = "anthropic" | "openai" | "google" | "deepseek" | "xai" | "qwen" | "unknown";
|
|
4
|
-
export type Family = "opus" | "sonnet" | "haiku" | "gpt" | "gpt-mini" | "gemini-pro" | "gemini-flash" | "deepseek" | "grok" | "qwen" | "unknown";
|
|
4
|
+
export type Family = "opus" | "fable" | "sonnet" | "haiku" | "gpt" | "gpt-mini" | "gemini-pro" | "gemini-flash" | "deepseek" | "grok" | "qwen" | "unknown";
|
|
5
5
|
export type Tier = "stupid" | "regular" | "smart" | "xsmart" | "unknown";
|
|
6
6
|
|
|
7
7
|
export interface ModelInfo {
|
|
@@ -13,7 +13,7 @@ export interface ModelInfo {
|
|
|
13
13
|
|
|
14
14
|
type ProviderPrefix = "anthropic" | "openai" | "google" | "deepseek" | "x-ai" | "qwen" | "pp-flant-anthropic" | "pp-flant-anthropic-sub" | "pp-flant-openai";
|
|
15
15
|
type KnownVendor = "anthropic" | "openai" | "google" | "deepseek" | "xai" | "qwen";
|
|
16
|
-
type KnownFamily = "opus" | "sonnet" | "haiku" | "gpt" | "gpt-mini" | "gemini-pro" | "gemini-flash" | "deepseek" | "grok" | "qwen";
|
|
16
|
+
type KnownFamily = "opus" | "fable" | "sonnet" | "haiku" | "gpt" | "gpt-mini" | "gemini-pro" | "gemini-flash" | "deepseek" | "grok" | "qwen";
|
|
17
17
|
type KnownTier = "stupid" | "regular" | "smart" | "xsmart";
|
|
18
18
|
|
|
19
19
|
export interface ModelFamilyDefinition {
|
|
@@ -47,6 +47,16 @@ export const MODEL_FAMILIES: ModelFamilyDefinition[] = [
|
|
|
47
47
|
providers: ["anthropic", "pp-flant-anthropic", "pp-flant-anthropic-sub"],
|
|
48
48
|
nativeLatestProviders: ["anthropic"],
|
|
49
49
|
},
|
|
50
|
+
{
|
|
51
|
+
vendor: "anthropic",
|
|
52
|
+
family: "fable",
|
|
53
|
+
tier: "xsmart",
|
|
54
|
+
displayName: "Claude Fable",
|
|
55
|
+
patterns: [/^(anthropic|pp-flant-anthropic)\/claude-fable-[a-z0-9.-]+$/, /^pp-flant-anthropic-sub\/sub\/claude-fable-[a-z0-9.-]+$/],
|
|
56
|
+
aliasTemplate: "claude-fable-latest",
|
|
57
|
+
providers: ["anthropic", "pp-flant-anthropic", "pp-flant-anthropic-sub"],
|
|
58
|
+
nativeLatestProviders: ["anthropic"],
|
|
59
|
+
},
|
|
50
60
|
{
|
|
51
61
|
vendor: "anthropic",
|
|
52
62
|
family: "sonnet",
|
|
@@ -123,6 +123,46 @@ describe("Orchestrator.safeSendUserMessage", () => {
|
|
|
123
123
|
});
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
+
describe("Orchestrator.deliverReviewReady (item 9: no editor leak on /pp close)", () => {
|
|
127
|
+
function setup() {
|
|
128
|
+
const sendUserMessage = vi.fn();
|
|
129
|
+
const orchestrator = new Orchestrator(makePi({ sendUserMessage }));
|
|
130
|
+
orchestrator.active = makeActiveTask(null);
|
|
131
|
+
// isIdle true so sendUserMessageWhenIdle delivers synchronously.
|
|
132
|
+
orchestrator.lastCtx = { isIdle: () => true };
|
|
133
|
+
return { orchestrator, sendUserMessage };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
it("does NOT queue the banner while a menu/ask dialogue is live (nothing to leak on ESC)", () => {
|
|
137
|
+
const { orchestrator, sendUserMessage } = setup();
|
|
138
|
+
orchestrator.interactivePromptOpen = true;
|
|
139
|
+
orchestrator.deliverReviewReady("[PI-PI] Review cycle is ready for apply_feedback.");
|
|
140
|
+
expect(sendUserMessage).not.toHaveBeenCalled();
|
|
141
|
+
expect(orchestrator.pendingReviewReady).toContain("ready for apply_feedback");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("delivers exactly once as a fresh turn when the dialogue closes, then never re-fires", () => {
|
|
145
|
+
const { orchestrator, sendUserMessage } = setup();
|
|
146
|
+
orchestrator.interactivePromptOpen = true;
|
|
147
|
+
orchestrator.deliverReviewReady("[PI-PI] Review cycle is ready for apply_feedback.");
|
|
148
|
+
orchestrator.interactivePromptOpen = false;
|
|
149
|
+
orchestrator.flushPendingReviewReady();
|
|
150
|
+
expect(sendUserMessage).toHaveBeenCalledTimes(1);
|
|
151
|
+
expect(orchestrator.pendingReviewReady).toBeNull();
|
|
152
|
+
// A second flush (e.g. a later idle) must not re-send.
|
|
153
|
+
orchestrator.flushPendingReviewReady();
|
|
154
|
+
expect(sendUserMessage).toHaveBeenCalledTimes(1);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("delivers immediately when no dialogue is open", () => {
|
|
158
|
+
const { orchestrator, sendUserMessage } = setup();
|
|
159
|
+
orchestrator.interactivePromptOpen = false;
|
|
160
|
+
orchestrator.deliverReviewReady("[PI-PI] Review cycle is ready for apply_feedback.");
|
|
161
|
+
expect(sendUserMessage).toHaveBeenCalledTimes(1);
|
|
162
|
+
expect(orchestrator.pendingReviewReady).toBeNull();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
126
166
|
describe("Orchestrator.cancelPendingRetry", () => {
|
|
127
167
|
it("clears the pending timer, disarms the ESC interrupt, and resets the counter", () => {
|
|
128
168
|
vi.useFakeTimers();
|
|
@@ -404,6 +444,33 @@ describe("Orchestrator.checkForConflictingExtensions", () => {
|
|
|
404
444
|
});
|
|
405
445
|
});
|
|
406
446
|
|
|
447
|
+
describe("Orchestrator.applySubagentConcurrency", () => {
|
|
448
|
+
const MANAGER_KEY = Symbol.for("pi-subagents:manager");
|
|
449
|
+
|
|
450
|
+
afterEach(() => {
|
|
451
|
+
delete (globalThis as any)[MANAGER_KEY];
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("applies the configured limit to the manager handle", () => {
|
|
455
|
+
const setMaxConcurrent = vi.fn();
|
|
456
|
+
(globalThis as any)[MANAGER_KEY] = { setMaxConcurrent };
|
|
457
|
+
const orchestrator = new Orchestrator(makePi());
|
|
458
|
+
orchestrator.config = getDefaultConfig() as any;
|
|
459
|
+
orchestrator.config.agents.maxConcurrentSubagents = 7;
|
|
460
|
+
|
|
461
|
+
orchestrator.applySubagentConcurrency();
|
|
462
|
+
|
|
463
|
+
expect(setMaxConcurrent).toHaveBeenCalledWith(7);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it("no-ops when the manager handle is absent", () => {
|
|
467
|
+
const orchestrator = new Orchestrator(makePi());
|
|
468
|
+
orchestrator.config = getDefaultConfig() as any;
|
|
469
|
+
|
|
470
|
+
expect(() => orchestrator.applySubagentConcurrency()).not.toThrow();
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
407
474
|
describe("Orchestrator.abortAllSubagents", () => {
|
|
408
475
|
it("emits stop events for all spawned subagents and clears the set", () => {
|
|
409
476
|
const emit = vi.fn();
|