@ilya-lesikov/pi-pi 0.6.0 → 0.8.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 +1 -1
- package/3p/pi-plannotator/apps/pi-extension/README.md +6 -5
- package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +25 -10
- package/3p/pi-plannotator/apps/pi-extension/plannotator-events.code-review.test.ts +172 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator-events.ts +50 -12
- package/3p/pi-plannotator/apps/pi-extension/server/project.test.ts +45 -0
- package/3p/pi-plannotator/apps/pi-extension/server/project.ts +7 -6
- package/3p/pi-plannotator/apps/pi-extension/server/serverReview.ts +41 -25
- package/3p/pi-subagents/src/agent-manager.ts +34 -1
- package/3p/pi-subagents/src/agent-runner.ts +66 -33
- package/3p/pi-subagents/src/index.ts +3 -38
- package/3p/pi-subagents/src/types.ts +4 -0
- package/extensions/orchestrator/agents/advisor.ts +35 -0
- package/extensions/orchestrator/agents/brainstorm-reviewer.ts +7 -7
- package/extensions/orchestrator/agents/code-reviewer.ts +7 -7
- package/extensions/orchestrator/agents/constraints.test.ts +44 -0
- package/extensions/orchestrator/agents/constraints.ts +3 -0
- package/extensions/orchestrator/agents/deep-debugger.ts +35 -0
- package/extensions/orchestrator/agents/plan-reviewer.ts +7 -7
- package/extensions/orchestrator/agents/planner.ts +9 -8
- package/extensions/orchestrator/agents/prompts.test.ts +120 -0
- package/extensions/orchestrator/agents/reviewer.ts +48 -0
- package/extensions/orchestrator/agents/task.ts +6 -20
- package/extensions/orchestrator/agents/tool-routing.ts +23 -1
- package/extensions/orchestrator/command-handlers.ts +1 -1
- package/extensions/orchestrator/config.ts +8 -4
- package/extensions/orchestrator/context.test.ts +54 -0
- package/extensions/orchestrator/context.ts +65 -2
- package/extensions/orchestrator/custom-footer.ts +5 -2
- package/extensions/orchestrator/doctor.test.ts +3 -1
- package/extensions/orchestrator/doctor.ts +40 -2
- package/extensions/orchestrator/event-handlers.test.ts +97 -1
- package/extensions/orchestrator/event-handlers.ts +222 -48
- package/extensions/orchestrator/flant-infra.test.ts +312 -0
- package/extensions/orchestrator/flant-infra.ts +407 -44
- package/extensions/orchestrator/index.ts +1 -1
- package/extensions/orchestrator/integration.test.ts +312 -18
- package/extensions/orchestrator/messages.test.ts +30 -0
- package/extensions/orchestrator/messages.ts +6 -0
- package/extensions/orchestrator/model-registry.test.ts +124 -13
- package/extensions/orchestrator/model-registry.ts +91 -33
- package/extensions/orchestrator/orchestrator.test.ts +113 -0
- package/extensions/orchestrator/orchestrator.ts +163 -3
- package/extensions/orchestrator/phases/brainstorm.ts +9 -20
- package/extensions/orchestrator/phases/implementation.test.ts +11 -0
- package/extensions/orchestrator/phases/implementation.ts +4 -6
- package/extensions/orchestrator/phases/planning.test.ts +16 -0
- package/extensions/orchestrator/phases/planning.ts +11 -4
- package/extensions/orchestrator/phases/review-task.ts +1 -4
- package/extensions/orchestrator/phases/review.test.ts +62 -0
- package/extensions/orchestrator/phases/review.ts +58 -15
- package/extensions/orchestrator/plannotator.ts +9 -6
- package/extensions/orchestrator/pp-menu.test.ts +74 -1
- package/extensions/orchestrator/pp-menu.ts +366 -94
- package/extensions/orchestrator/pp-state-tools.test.ts +192 -0
- package/extensions/orchestrator/pp-state-tools.ts +249 -0
- package/extensions/orchestrator/rate-limit-fallback-flow.test.ts +128 -0
- package/extensions/orchestrator/rate-limit-fallback.test.ts +98 -0
- package/extensions/orchestrator/rate-limit-fallback.ts +243 -0
- package/extensions/orchestrator/state.ts +41 -20
- package/extensions/orchestrator/test-helpers.ts +18 -3
- package/extensions/orchestrator/usage-tracker.test.ts +131 -3
- package/extensions/orchestrator/usage-tracker.ts +78 -11
- package/extensions/orchestrator/validate-artifacts.test.ts +20 -0
- package/extensions/orchestrator/validate-artifacts.ts +2 -2
- package/package.json +1 -1
|
@@ -63,12 +63,16 @@ vi.mock("./config.js", async (importOriginal) => {
|
|
|
63
63
|
debug: { model: "test/model", thinking: "high" },
|
|
64
64
|
brainstorm: { model: "test/model", thinking: "high" },
|
|
65
65
|
review: { model: "test/model", thinking: "high" },
|
|
66
|
+
quick: { model: "test/model", thinking: "high" },
|
|
66
67
|
},
|
|
67
68
|
subagents: {
|
|
68
69
|
simple: {
|
|
69
70
|
explore: { model: "test/explore", thinking: "low" },
|
|
70
71
|
librarian: { model: "test/librarian", thinking: "medium" },
|
|
71
72
|
task: { model: "test/task", thinking: "medium" },
|
|
73
|
+
advisor: { model: "test/advisor", thinking: "high" },
|
|
74
|
+
"deep-debugger": { model: "test/deep-debugger", thinking: "high" },
|
|
75
|
+
reviewer: { model: "test/reviewer", thinking: "high" },
|
|
72
76
|
},
|
|
73
77
|
presetGroups: {
|
|
74
78
|
planners: {
|
|
@@ -215,12 +219,16 @@ function makeConfig() {
|
|
|
215
219
|
debug: { model: "test/model", thinking: "high" },
|
|
216
220
|
brainstorm: { model: "test/model", thinking: "high" },
|
|
217
221
|
review: { model: "test/model", thinking: "high" },
|
|
222
|
+
quick: { model: "test/model", thinking: "high" },
|
|
218
223
|
},
|
|
219
224
|
subagents: {
|
|
220
225
|
simple: {
|
|
221
226
|
explore: { model: "test/explore", thinking: "low" },
|
|
222
227
|
librarian: { model: "test/librarian", thinking: "medium" },
|
|
223
228
|
task: { model: "test/task", thinking: "medium" },
|
|
229
|
+
advisor: { model: "test/advisor", thinking: "high" },
|
|
230
|
+
"deep-debugger": { model: "test/deep-debugger", thinking: "high" },
|
|
231
|
+
reviewer: { model: "test/reviewer", thinking: "high" },
|
|
224
232
|
},
|
|
225
233
|
presetGroups: {
|
|
226
234
|
planners: {
|
|
@@ -670,7 +678,7 @@ describe("review cycle lifecycle", () => {
|
|
|
670
678
|
await new Promise((r) => setTimeout(r, 10));
|
|
671
679
|
|
|
672
680
|
expectReviewAuto(menu);
|
|
673
|
-
expectReviewOnMyOwn(menu);
|
|
681
|
+
expectReviewOnMyOwn(menu, "Skip markers");
|
|
674
682
|
const result = await ppPhaseComplete.execute("call-3", { summary: "implemented" }, undefined, undefined, ctx);
|
|
675
683
|
|
|
676
684
|
expect(result.content[0].text).toContain("continue");
|
|
@@ -1353,7 +1361,7 @@ describe("edge cases and regressions", () => {
|
|
|
1353
1361
|
await ppPhaseComplete.execute("call-2", { summary: "plan done" }, undefined, undefined, ctx);
|
|
1354
1362
|
await new Promise((r) => setTimeout(r, 10));
|
|
1355
1363
|
|
|
1356
|
-
expectReviewOnMyOwn(menu);
|
|
1364
|
+
expectReviewOnMyOwn(menu, "Skip markers");
|
|
1357
1365
|
const result = await ppPhaseComplete.execute("call-3", { summary: "partial work" }, undefined, undefined, ctx);
|
|
1358
1366
|
|
|
1359
1367
|
expect(result.content[0].text).toContain("continue");
|
|
@@ -1400,6 +1408,47 @@ describe("edge cases and regressions", () => {
|
|
|
1400
1408
|
expect(orchestrator.active!.state.step).toBe("synthesize");
|
|
1401
1409
|
});
|
|
1402
1410
|
|
|
1411
|
+
it("editor-review Done returns AI_REVIEW marker instructions in implement phase", async () => {
|
|
1412
|
+
const cwd = makeTempDir();
|
|
1413
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
1414
|
+
const ctx = makeCtx();
|
|
1415
|
+
|
|
1416
|
+
await orchestrator.startTask(ctx as any, "implement", "Editor review test");
|
|
1417
|
+
const taskDir = orchestrator.active!.dir;
|
|
1418
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
1419
|
+
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
1420
|
+
expectBrainstormToPlan(menu);
|
|
1421
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
1422
|
+
await ppPhaseComplete.execute("call-1", { summary: "done" }, undefined, undefined, ctx);
|
|
1423
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1424
|
+
|
|
1425
|
+
emitSubagentCreated(pi, "planner-1", "Planner (test)");
|
|
1426
|
+
const plansDir = join(taskDir, "plans");
|
|
1427
|
+
mkdirSync(plansDir, { recursive: true });
|
|
1428
|
+
writeFileSync(
|
|
1429
|
+
join(plansDir, `${Math.floor(Date.now() / 1000)}_test.md`),
|
|
1430
|
+
makeValidPlan(["- [ ] P1. Planner draft item — Done when: planner output exists"]),
|
|
1431
|
+
"utf-8",
|
|
1432
|
+
);
|
|
1433
|
+
emitSubagentCompleted(pi, "planner-1", "Planner (test)");
|
|
1434
|
+
writeFileSync(
|
|
1435
|
+
join(plansDir, `${Math.floor(Date.now() / 1000) + 1}_synthesized.md`),
|
|
1436
|
+
makeValidPlan(["- [ ] P1. Todo item — Done when: item intentionally remains unchecked"]),
|
|
1437
|
+
"utf-8",
|
|
1438
|
+
);
|
|
1439
|
+
expectPlanToImplement(menu);
|
|
1440
|
+
await ppPhaseComplete.execute("call-2", { summary: "plan done" }, undefined, undefined, ctx);
|
|
1441
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1442
|
+
|
|
1443
|
+
expectReviewOnMyOwn(menu, "Done");
|
|
1444
|
+
const result = await ppPhaseComplete.execute("call-3", { summary: "implemented" }, undefined, undefined, ctx);
|
|
1445
|
+
|
|
1446
|
+
expect(result.content[0].text).toContain("AI_REVIEW:");
|
|
1447
|
+
expect(result.content[0].text).toContain("CHANGED files");
|
|
1448
|
+
expect(orchestrator.active!.state.phase).toBe("implement");
|
|
1449
|
+
expect(orchestrator.active!.state.step).toBe("llm_work");
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1403
1452
|
it("generic description task does not auto-trigger agent", async () => {
|
|
1404
1453
|
const cwd = makeTempDir();
|
|
1405
1454
|
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
@@ -1565,7 +1614,7 @@ describe("task modes and quick task", () => {
|
|
|
1565
1614
|
expect(orchestrator.active!.state.autonomousConfig).toBeDefined();
|
|
1566
1615
|
});
|
|
1567
1616
|
|
|
1568
|
-
it("autonomous pp_phase_complete auto-advances without opening menu", async () => {
|
|
1617
|
+
it("autonomous pp_phase_complete auto-advances (plan→implement) without opening menu", async () => {
|
|
1569
1618
|
const cwd = makeTempDir();
|
|
1570
1619
|
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
1571
1620
|
const ctx = makeCtx();
|
|
@@ -1581,16 +1630,115 @@ describe("task modes and quick task", () => {
|
|
|
1581
1630
|
};
|
|
1582
1631
|
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
1583
1632
|
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
1633
|
+
// Move to an autonomous-capable phase (plan). brainstorm is always guided
|
|
1634
|
+
// even for an autonomous task, so auto-advance only applies from plan onward.
|
|
1635
|
+
orchestrator.active!.state.phase = "plan";
|
|
1636
|
+
orchestrator.active!.state.step = "llm_work";
|
|
1637
|
+
const plansDir = join(taskDir, "plans");
|
|
1638
|
+
mkdirSync(plansDir, { recursive: true });
|
|
1639
|
+
writeFileSync(
|
|
1640
|
+
join(plansDir, `${Math.floor(Date.now() / 1000)}_synthesized.md`),
|
|
1641
|
+
makeValidPlan([
|
|
1642
|
+
"- [ ] P1. Implement X — Done when: implementation for X is complete",
|
|
1643
|
+
]),
|
|
1644
|
+
"utf-8",
|
|
1645
|
+
);
|
|
1584
1646
|
|
|
1585
1647
|
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
1586
1648
|
const result = await ppPhaseComplete.execute("call-1", { summary: "done" }, undefined, undefined, ctx);
|
|
1587
1649
|
await new Promise((r) => setTimeout(r, 10));
|
|
1588
1650
|
|
|
1589
1651
|
expect(result.content[0].text).toBe("");
|
|
1590
|
-
expect(orchestrator.active!.state.phase).toBe("
|
|
1652
|
+
expect(orchestrator.active!.state.phase).toBe("implement");
|
|
1591
1653
|
expect(menu.transcript.filter((entry) => entry.question.startsWith("/pp"))).toHaveLength(0);
|
|
1592
1654
|
});
|
|
1593
1655
|
|
|
1656
|
+
it("autonomous task in brainstorm stays guided (pp_phase_complete opens the menu, does NOT auto-advance)", async () => {
|
|
1657
|
+
const cwd = makeTempDir();
|
|
1658
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
1659
|
+
const ctx = makeCtx();
|
|
1660
|
+
|
|
1661
|
+
await orchestrator.startTask(ctx as any, "implement", "implement", undefined, undefined, "autonomous");
|
|
1662
|
+
const taskDir = orchestrator.active!.dir;
|
|
1663
|
+
orchestrator.active!.state.autonomousConfig = {
|
|
1664
|
+
phases: {
|
|
1665
|
+
brainstorm: { reviewPreset: "regular", maxReviewPasses: 0 },
|
|
1666
|
+
plan: { plannerPreset: "regular", reviewPreset: "regular", maxReviewPasses: 0 },
|
|
1667
|
+
implement: { reviewPreset: "regular", maxReviewPasses: 0 },
|
|
1668
|
+
},
|
|
1669
|
+
};
|
|
1670
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
1671
|
+
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
1672
|
+
expect(orchestrator.active!.state.phase).toBe("brainstorm");
|
|
1673
|
+
|
|
1674
|
+
// brainstorm is user-driven: pp_phase_complete must fall through to the
|
|
1675
|
+
// guided menu rather than the autonomous auto-advance branch. (Autonomous
|
|
1676
|
+
// tasks skip the planner preset picker on transition, so only the guided
|
|
1677
|
+
// Next→Continue step is expected here.)
|
|
1678
|
+
expectActiveTaskNext(menu, "Continue to plan & implement");
|
|
1679
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
1680
|
+
await ppPhaseComplete.execute("call-1", { summary: "done" }, undefined, undefined, ctx);
|
|
1681
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1682
|
+
|
|
1683
|
+
// The guided menu was shown (auto-advance did NOT bypass it).
|
|
1684
|
+
expect(menu.transcript.filter((entry) => entry.question.startsWith("/pp"))).not.toHaveLength(0);
|
|
1685
|
+
});
|
|
1686
|
+
|
|
1687
|
+
it("ESC on pp_phase_complete stops the turn cleanly (aborts, no reminder text)", async () => {
|
|
1688
|
+
const cwd = makeTempDir();
|
|
1689
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
1690
|
+
const ctx = makeCtx();
|
|
1691
|
+
|
|
1692
|
+
await orchestrator.startTask(ctx as any, "implement", "esc dismiss");
|
|
1693
|
+
// Guided task in brainstorm; transition controller is running so the
|
|
1694
|
+
// non-ESC dismiss path would normally return the reminder text.
|
|
1695
|
+
expect(orchestrator.transitionController.isRunning()).toBe(true);
|
|
1696
|
+
|
|
1697
|
+
// Simulate a deliberate user ESC on the top-level menu.
|
|
1698
|
+
menu.expect({ question: m.anyTaskMenu, cancel: "user" });
|
|
1699
|
+
|
|
1700
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
1701
|
+
const result = await ppPhaseComplete.execute("esc-1", { summary: "done" }, undefined, undefined, ctx);
|
|
1702
|
+
|
|
1703
|
+
expect(ctx.abort).toHaveBeenCalled();
|
|
1704
|
+
expect(result.content[0].text).toBe("");
|
|
1705
|
+
});
|
|
1706
|
+
|
|
1707
|
+
it("Back on pp_phase_complete keeps the artifact-update reminder (does NOT abort)", async () => {
|
|
1708
|
+
const cwd = makeTempDir();
|
|
1709
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
1710
|
+
const ctx = makeCtx();
|
|
1711
|
+
|
|
1712
|
+
await orchestrator.startTask(ctx as any, "implement", "back dismiss");
|
|
1713
|
+
expect(orchestrator.transitionController.isRunning()).toBe(true);
|
|
1714
|
+
|
|
1715
|
+
// Explicit "Back" navigation (not an ESC): the reminder must be preserved.
|
|
1716
|
+
menu.expect({ question: m.anyTaskMenu, options: { include: ["Back"] }, choose: "Back" });
|
|
1717
|
+
|
|
1718
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
1719
|
+
const result = await ppPhaseComplete.execute("back-1", { summary: "done" }, undefined, undefined, ctx);
|
|
1720
|
+
|
|
1721
|
+
expect(ctx.abort).not.toHaveBeenCalled();
|
|
1722
|
+
expect(result.content[0].text).toContain("update USER_REQUEST.md and RESEARCH.md");
|
|
1723
|
+
});
|
|
1724
|
+
|
|
1725
|
+
it("ESC on pp_phase_complete for a quick task also stops the turn cleanly", async () => {
|
|
1726
|
+
const cwd = makeTempDir();
|
|
1727
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
1728
|
+
const ctx = makeCtx();
|
|
1729
|
+
|
|
1730
|
+
await orchestrator.startTask(ctx as any, "quick", "quick esc");
|
|
1731
|
+
expect(orchestrator.active!.type).toBe("quick");
|
|
1732
|
+
|
|
1733
|
+
menu.expect({ question: m.anyTaskMenu, cancel: "user" });
|
|
1734
|
+
|
|
1735
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
1736
|
+
const result = await ppPhaseComplete.execute("quick-esc-1", { summary: "done" }, undefined, undefined, ctx);
|
|
1737
|
+
|
|
1738
|
+
expect(ctx.abort).toHaveBeenCalled();
|
|
1739
|
+
expect(result.content[0].text).toBe("");
|
|
1740
|
+
});
|
|
1741
|
+
|
|
1594
1742
|
it("autonomous review loop re-runs until cap then advances", async () => {
|
|
1595
1743
|
const cwd = makeTempDir();
|
|
1596
1744
|
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
@@ -1694,6 +1842,52 @@ describe("task modes and quick task", () => {
|
|
|
1694
1842
|
expect(finalState.reviewPassByKind?.implement?.auto).toBe(1);
|
|
1695
1843
|
});
|
|
1696
1844
|
|
|
1845
|
+
it("autonomous PLAN review cycle: apply feedback (clean approve) advances to implement", async () => {
|
|
1846
|
+
const cwd = makeTempDir();
|
|
1847
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
1848
|
+
const ctx = makeCtx();
|
|
1849
|
+
|
|
1850
|
+
await orchestrator.startTask(ctx as any, "implement", "plan review advance", undefined, undefined, "autonomous");
|
|
1851
|
+
const taskDir = orchestrator.active!.dir;
|
|
1852
|
+
orchestrator.active!.state.phase = "plan";
|
|
1853
|
+
orchestrator.active!.state.step = "synthesize";
|
|
1854
|
+
orchestrator.active!.state.reviewPass = 0;
|
|
1855
|
+
orchestrator.active!.state.reviewPassByKind = {};
|
|
1856
|
+
orchestrator.active!.state.autonomousConfig = {
|
|
1857
|
+
phases: { plan: { plannerPreset: "regular", reviewPreset: "regular", maxReviewPasses: 3 } },
|
|
1858
|
+
};
|
|
1859
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
1860
|
+
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
1861
|
+
const plansDir = join(taskDir, "plans");
|
|
1862
|
+
mkdirSync(plansDir, { recursive: true });
|
|
1863
|
+
writeFileSync(
|
|
1864
|
+
join(plansDir, "1_synthesized.md"),
|
|
1865
|
+
makeValidPlan(["- [x] P1. Plan ready — Done when: synthesized plan exists"]),
|
|
1866
|
+
"utf-8",
|
|
1867
|
+
);
|
|
1868
|
+
|
|
1869
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
1870
|
+
// First call: plan phase is autonomous, no clean approval yet → starts a review cycle.
|
|
1871
|
+
const first = await ppPhaseComplete.execute("plan-rev-1", { summary: "plan done" }, undefined, undefined, ctx);
|
|
1872
|
+
expect(first.content[0].text).toMatch(/Reviews are running|Started review cycle pass/);
|
|
1873
|
+
|
|
1874
|
+
// Plan reviewers write to plan-reviews/ (not code-reviews/).
|
|
1875
|
+
const planReviewsDir = join(taskDir, "plan-reviews");
|
|
1876
|
+
mkdirSync(planReviewsDir, { recursive: true });
|
|
1877
|
+
const round = orchestrator.active!.state.reviewCycle!.pass;
|
|
1878
|
+
for (const v of ["opus", "gpt", "gemini"]) {
|
|
1879
|
+
writeFileSync(join(planReviewsDir, `1_${v}_round-${round}.md`), "VERDICT: APPROVE\n- CRITICAL: none", "utf-8");
|
|
1880
|
+
}
|
|
1881
|
+
emitSubagentCreated(pi, "plan-reviewer-1", "Plan reviewer (test)");
|
|
1882
|
+
emitSubagentCompleted(pi, "plan-reviewer-1", "Plan reviewer (test)");
|
|
1883
|
+
|
|
1884
|
+
// Second call: finalize the clean-approved pass and advance to implement (single transition).
|
|
1885
|
+
const second = await ppPhaseComplete.execute("plan-rev-2", { summary: "feedback applied" }, undefined, undefined, ctx);
|
|
1886
|
+
expect(second.content[0].text).toBe("");
|
|
1887
|
+
expect(orchestrator.active!.state.phase).toBe("implement");
|
|
1888
|
+
expect(menu.transcript.filter((entry) => entry.question.startsWith("/pp"))).toHaveLength(0);
|
|
1889
|
+
});
|
|
1890
|
+
|
|
1697
1891
|
it("clean-approved review is not re-run on a later checklist-repair re-entry", async () => {
|
|
1698
1892
|
const cwd = makeTempDir();
|
|
1699
1893
|
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
@@ -1932,7 +2126,7 @@ describe("task modes and quick task", () => {
|
|
|
1932
2126
|
expect(prompt).toContain("<principles>");
|
|
1933
2127
|
expect(prompt).toContain("<tools>");
|
|
1934
2128
|
expect(prompt).toContain("<task>");
|
|
1935
|
-
expect(prompt).toContain("
|
|
2129
|
+
expect(prompt).toContain("call pp_phase_complete");
|
|
1936
2130
|
expect(prompt).not.toContain("HARNESS_BASE_PROMPT");
|
|
1937
2131
|
expect(prompt).toContain(`Working directory: ${cwd}.`);
|
|
1938
2132
|
expect(prompt).toMatch(/Current month: \d{4}-\d{2}\./);
|
|
@@ -2041,10 +2235,11 @@ describe("task modes and quick task", () => {
|
|
|
2041
2235
|
|
|
2042
2236
|
const title = "implement";
|
|
2043
2237
|
const pp = getCommand(pi, "pp");
|
|
2238
|
+
const titleMatch = (t: string) => t.includes(title);
|
|
2044
2239
|
menu
|
|
2045
2240
|
.expect({ question: "/pp", options: { include: ["Task"] }, choose: "Task" })
|
|
2046
2241
|
.expect({ question: "Task", options: { include: ["Resume"] }, choose: "Resume" })
|
|
2047
|
-
.expect({ question: "Resume", options: { include: [
|
|
2242
|
+
.expect({ question: "Resume", options: { include: [titleMatch] }, choose: titleMatch });
|
|
2048
2243
|
await pp(undefined, ctx);
|
|
2049
2244
|
|
|
2050
2245
|
expect(orchestrator.active).not.toBeNull();
|
|
@@ -2069,10 +2264,11 @@ describe("task modes and quick task", () => {
|
|
|
2069
2264
|
await orchestrator.cleanupActive();
|
|
2070
2265
|
|
|
2071
2266
|
const pp = getCommand(pi, "pp");
|
|
2267
|
+
const configTitleMatch = (t: string) => t.includes("resume autonomous config");
|
|
2072
2268
|
menu
|
|
2073
2269
|
.expect({ question: "/pp", options: { include: ["Task"] }, choose: "Task" })
|
|
2074
2270
|
.expect({ question: "Task", options: { include: ["Resume"] }, choose: "Resume" })
|
|
2075
|
-
.expect({ question: "Resume", options: { include: [
|
|
2271
|
+
.expect({ question: "Resume", options: { include: [configTitleMatch] }, choose: configTitleMatch });
|
|
2076
2272
|
await pp(undefined, ctx);
|
|
2077
2273
|
|
|
2078
2274
|
expect(orchestrator.active).not.toBeNull();
|
|
@@ -2102,6 +2298,10 @@ describe("task modes and quick task", () => {
|
|
|
2102
2298
|
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
2103
2299
|
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
2104
2300
|
|
|
2301
|
+
// brainstorm is user-driven even for an autonomous task, so the transition
|
|
2302
|
+
// to plan goes through the guided menu (which auto-skips the planner preset
|
|
2303
|
+
// picker because the task is autonomous).
|
|
2304
|
+
expectActiveTaskNext(menu, "Continue to plan & implement");
|
|
2105
2305
|
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
2106
2306
|
await ppPhaseComplete.execute("call-autonomous-skip-planner", { summary: "done" }, undefined, undefined, ctx);
|
|
2107
2307
|
await new Promise((r) => setTimeout(r, 10));
|
|
@@ -2596,7 +2796,7 @@ describe("resume and recovery", () => {
|
|
|
2596
2796
|
menu
|
|
2597
2797
|
.expect({ question: "/pp", options: { include: ["Task"] }, choose: "Task" })
|
|
2598
2798
|
.expect({ question: "Task", options: { include: ["Resume"] }, choose: "Resume" })
|
|
2599
|
-
.expect({ question: "Resume", options: { include: ["resume phase state"] }, choose: "resume phase state" });
|
|
2799
|
+
.expect({ question: "Resume", options: { include: [(t: string) => t.includes("resume phase state")] }, choose: (t: string) => t.includes("resume phase state") });
|
|
2600
2800
|
await pp(undefined, ctx);
|
|
2601
2801
|
|
|
2602
2802
|
expect(orchestrator.active).not.toBeNull();
|
|
@@ -2625,7 +2825,7 @@ describe("resume and recovery", () => {
|
|
|
2625
2825
|
menu
|
|
2626
2826
|
.expect({ question: "/pp", options: { include: ["Task"] }, choose: "Task" })
|
|
2627
2827
|
.expect({ question: "Task", options: { include: ["Resume"] }, choose: "Resume" })
|
|
2628
|
-
.expect({ question: "Resume", options: { include: ["resume stale repos"] }, choose: "resume stale repos" });
|
|
2828
|
+
.expect({ question: "Resume", options: { include: [(t: string) => t.includes("resume stale repos")] }, choose: (t: string) => t.includes("resume stale repos") });
|
|
2629
2829
|
await pp(undefined, ctx);
|
|
2630
2830
|
|
|
2631
2831
|
expect(orchestrator.active!.state.repos).toEqual([{ path: cwd, isRoot: true }]);
|
|
@@ -2852,6 +3052,49 @@ describe("brainstorm and plan review cycles", () => {
|
|
|
2852
3052
|
expect(readyMessages[readyMessages.length - 1]).toContain("artifacts/");
|
|
2853
3053
|
});
|
|
2854
3054
|
|
|
3055
|
+
const runPlanReviewReady = async (mode?: "autonomous") => {
|
|
3056
|
+
const cwd = makeTempDir();
|
|
3057
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
3058
|
+
const ctx = makeCtx({ cwd });
|
|
3059
|
+
|
|
3060
|
+
await orchestrator.startTask(ctx as any, "implement", "plan review message", undefined, undefined, mode);
|
|
3061
|
+
const taskDir = orchestrator.active!.dir;
|
|
3062
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
3063
|
+
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
3064
|
+
mkdirSync(join(taskDir, "plan-reviews"), { recursive: true });
|
|
3065
|
+
writeFileSync(
|
|
3066
|
+
join(taskDir, "plan-reviews", `${Math.floor(Date.now() / 1000)}_test_round-1.md`),
|
|
3067
|
+
"Plan review feedback",
|
|
3068
|
+
"utf-8",
|
|
3069
|
+
);
|
|
3070
|
+
orchestrator.active!.state.phase = "plan";
|
|
3071
|
+
orchestrator.active!.state.step = "synthesize";
|
|
3072
|
+
saveTask(taskDir, orchestrator.active!.state);
|
|
3073
|
+
|
|
3074
|
+
await enterReviewCycle(orchestrator, ctx, "regular");
|
|
3075
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
3076
|
+
|
|
3077
|
+
return (pi.sendUserMessage as any).mock.calls
|
|
3078
|
+
.map((c: any[]) => c[0] as string)
|
|
3079
|
+
.filter((text: string) => text.includes("ready for apply_feedback"));
|
|
3080
|
+
};
|
|
3081
|
+
|
|
3082
|
+
it("autonomous plan review-ready message mandates re-calling pp_phase_complete", async () => {
|
|
3083
|
+
const readyMessages = await runPlanReviewReady("autonomous");
|
|
3084
|
+
expect(readyMessages.length).toBeGreaterThan(0);
|
|
3085
|
+
const last = readyMessages[readyMessages.length - 1];
|
|
3086
|
+
expect(last).toContain("pp_phase_complete");
|
|
3087
|
+
expect(last).toContain("Do NOT stop or wait for the user");
|
|
3088
|
+
});
|
|
3089
|
+
|
|
3090
|
+
it("guided plan review-ready message does NOT tell the agent to re-call pp_phase_complete or auto-advance", async () => {
|
|
3091
|
+
const readyMessages = await runPlanReviewReady();
|
|
3092
|
+
expect(readyMessages.length).toBeGreaterThan(0);
|
|
3093
|
+
const last = readyMessages[readyMessages.length - 1];
|
|
3094
|
+
expect(last).not.toContain("pp_phase_complete");
|
|
3095
|
+
expect(last).not.toContain("Do NOT stop or wait for the user");
|
|
3096
|
+
});
|
|
3097
|
+
|
|
2855
3098
|
it("plan review cycle reaches apply_feedback and finalizeReviewCycle returns to user_gate", async () => {
|
|
2856
3099
|
const cwd = makeTempDir();
|
|
2857
3100
|
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
@@ -3044,7 +3287,11 @@ describe("tool blocking", () => {
|
|
|
3044
3287
|
});
|
|
3045
3288
|
|
|
3046
3289
|
describe("error retry", () => {
|
|
3047
|
-
|
|
3290
|
+
// Non-SDK-retryable error messages exercise pi-pi's OWN retry path. SDK-
|
|
3291
|
+
// retryable errors (rate limit, overloaded, 5xx, stream-ended, timeout, etc.)
|
|
3292
|
+
// are now deferred to the SDK's own auto-retry (see the defer test below), so
|
|
3293
|
+
// these use a message the SDK does NOT retry.
|
|
3294
|
+
it("turn_end with a non-SDK-retryable error uses pi-pi's idle-gated retry", async () => {
|
|
3048
3295
|
vi.useFakeTimers();
|
|
3049
3296
|
const cwd = makeTempDir();
|
|
3050
3297
|
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
@@ -3053,7 +3300,7 @@ describe("error retry", () => {
|
|
|
3053
3300
|
await orchestrator.startTask(ctx as any, "implement", "retry test");
|
|
3054
3301
|
const turnEnd = pi._handlers.get("turn_end")!;
|
|
3055
3302
|
|
|
3056
|
-
await turnEnd({ message: { stopReason: "error", errorMessage: "
|
|
3303
|
+
await turnEnd({ message: { stopReason: "error", errorMessage: "invalid tool arguments", content: [] } }, ctx);
|
|
3057
3304
|
|
|
3058
3305
|
expect(orchestrator.errorRetryCount).toBe(1);
|
|
3059
3306
|
expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("Retrying in 2s"), "warning");
|
|
@@ -3063,7 +3310,24 @@ describe("error retry", () => {
|
|
|
3063
3310
|
vi.useRealTimers();
|
|
3064
3311
|
});
|
|
3065
3312
|
|
|
3066
|
-
it("turn_end
|
|
3313
|
+
it("turn_end defers SDK-retryable errors to the SDK (no pi-pi retry)", async () => {
|
|
3314
|
+
const cwd = makeTempDir();
|
|
3315
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
3316
|
+
const ctx = makeCtx();
|
|
3317
|
+
|
|
3318
|
+
await orchestrator.startTask(ctx as any, "implement", "sdk defer test");
|
|
3319
|
+
const turnEnd = pi._handlers.get("turn_end")!;
|
|
3320
|
+
|
|
3321
|
+
// "stream ended before message_stop" is SDK-retryable — pi-pi must NOT run
|
|
3322
|
+
// its own retry (which would race the SDK's continue()).
|
|
3323
|
+
await turnEnd({ message: { stopReason: "error", errorMessage: "Anthropic stream ended before message_stop", content: [] } }, ctx);
|
|
3324
|
+
|
|
3325
|
+
expect(orchestrator.errorRetryCount).toBe(0);
|
|
3326
|
+
expect(orchestrator.pendingRetryTimer).toBeNull();
|
|
3327
|
+
expect(ctx.ui.notify).not.toHaveBeenCalledWith(expect.stringContaining("Retrying in"), "warning");
|
|
3328
|
+
});
|
|
3329
|
+
|
|
3330
|
+
it("turn_end stops retrying after max retries (non-SDK-retryable)", async () => {
|
|
3067
3331
|
vi.useFakeTimers();
|
|
3068
3332
|
const cwd = makeTempDir();
|
|
3069
3333
|
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
@@ -3073,27 +3337,54 @@ describe("error retry", () => {
|
|
|
3073
3337
|
const turnEnd = pi._handlers.get("turn_end")!;
|
|
3074
3338
|
|
|
3075
3339
|
for (let i = 0; i < 6; i++) {
|
|
3076
|
-
await turnEnd({ message: { stopReason: "error", errorMessage: "
|
|
3340
|
+
await turnEnd({ message: { stopReason: "error", errorMessage: "invalid tool arguments", content: [] } }, ctx);
|
|
3077
3341
|
}
|
|
3078
3342
|
|
|
3079
|
-
expect(orchestrator.errorRetryCount).toBe(0);
|
|
3080
3343
|
expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("Stopping auto-retry"), "error");
|
|
3344
|
+
expect(orchestrator.errorNudgeHalted).toBe(true);
|
|
3345
|
+
|
|
3346
|
+
// Once halted, further error turns must NOT arm another retry (this is the
|
|
3347
|
+
// guard against the unbounded-nudge bug).
|
|
3348
|
+
ctx.ui.notify.mockClear();
|
|
3349
|
+
await turnEnd({ message: { stopReason: "error", errorMessage: "invalid tool arguments", content: [] } }, ctx);
|
|
3350
|
+
expect(ctx.ui.notify).not.toHaveBeenCalledWith(expect.stringContaining("Retrying in"), "warning");
|
|
3081
3351
|
vi.useRealTimers();
|
|
3082
3352
|
});
|
|
3083
3353
|
|
|
3084
|
-
it("
|
|
3354
|
+
it("a benign turn does NOT reset the error count (cap must accumulate)", async () => {
|
|
3085
3355
|
const cwd = makeTempDir();
|
|
3086
3356
|
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
3087
3357
|
const ctx = makeCtx();
|
|
3088
3358
|
|
|
3089
|
-
await orchestrator.startTask(ctx as any, "implement", "retry reset test");
|
|
3359
|
+
await orchestrator.startTask(ctx as any, "implement", "retry no-reset test");
|
|
3090
3360
|
const turnEnd = pi._handlers.get("turn_end")!;
|
|
3091
3361
|
|
|
3092
|
-
await turnEnd({ message: { stopReason: "error", errorMessage: "
|
|
3362
|
+
await turnEnd({ message: { stopReason: "error", errorMessage: "invalid tool arguments", content: [] } }, ctx);
|
|
3093
3363
|
expect(orchestrator.errorRetryCount).toBe(1);
|
|
3094
3364
|
|
|
3365
|
+
// A benign (non-error) turn must NOT zero the counter — otherwise a harmless
|
|
3366
|
+
// nudge-induced reply would defeat the cap and allow unbounded error nudges.
|
|
3095
3367
|
await turnEnd({ message: { stopReason: "stop", content: [] } }, ctx);
|
|
3096
|
-
expect(orchestrator.errorRetryCount).toBe(
|
|
3368
|
+
expect(orchestrator.errorRetryCount).toBe(1);
|
|
3369
|
+
});
|
|
3370
|
+
|
|
3371
|
+
it("genuine user re-engagement resets the error count and halt", async () => {
|
|
3372
|
+
const cwd = makeTempDir();
|
|
3373
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
3374
|
+
const ctx = makeCtx();
|
|
3375
|
+
|
|
3376
|
+
await orchestrator.startTask(ctx as any, "implement", "retry reengage test");
|
|
3377
|
+
const turnEnd = pi._handlers.get("turn_end")!;
|
|
3378
|
+
|
|
3379
|
+
await turnEnd({ message: { stopReason: "error", errorMessage: "invalid tool arguments", content: [] } }, ctx);
|
|
3380
|
+
orchestrator.errorNudgeHalted = true;
|
|
3381
|
+
|
|
3382
|
+
const beforeStart = pi._handlers.get("before_agent_start");
|
|
3383
|
+
if (beforeStart) {
|
|
3384
|
+
await beforeStart({ prompt: "user typed something new" }, ctx);
|
|
3385
|
+
expect(orchestrator.errorRetryCount).toBe(0);
|
|
3386
|
+
expect(orchestrator.errorNudgeHalted).toBe(false);
|
|
3387
|
+
}
|
|
3097
3388
|
});
|
|
3098
3389
|
|
|
3099
3390
|
it("empty turn triggers continuation nudge", async () => {
|
|
@@ -3917,6 +4208,9 @@ describe("full user flows", () => {
|
|
|
3917
4208
|
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
3918
4209
|
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
3919
4210
|
|
|
4211
|
+
// brainstorm is user-driven: the brainstorm→plan transition uses the guided
|
|
4212
|
+
// menu. plan→implement and implement→review below are autonomous auto-advance.
|
|
4213
|
+
expectActiveTaskNext(menu, "Continue to plan & implement");
|
|
3920
4214
|
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
3921
4215
|
await ppPhaseComplete.execute("flow-auto-1", { summary: "brainstorm done" }, undefined, undefined, ctx);
|
|
3922
4216
|
await new Promise((r) => setTimeout(r, 10));
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { advanceBanner } from "./messages.js";
|
|
3
|
+
|
|
4
|
+
describe("advanceBanner", () => {
|
|
5
|
+
it("starts with [PI-PI] so the controller-injection guard still matches", () => {
|
|
6
|
+
const out = advanceBanner("[PI-PI] User wants to continue. Run /pp when ready to advance.");
|
|
7
|
+
expect(out.startsWith("[PI-PI]")).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("renders separators around the content for visual distinctness", () => {
|
|
11
|
+
const out = advanceBanner("[PI-PI] hello");
|
|
12
|
+
const lines = out.split("\n");
|
|
13
|
+
expect(lines[0]).toBe("[PI-PI]");
|
|
14
|
+
expect(lines[1]).toMatch(/^─+$/);
|
|
15
|
+
expect(lines[2]).toBe("hello");
|
|
16
|
+
expect(lines[3]).toMatch(/^─+$/);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("does not double up the [PI-PI] prefix when the body already has one", () => {
|
|
20
|
+
const out = advanceBanner("[PI-PI] hello");
|
|
21
|
+
expect(out.match(/\[PI-PI\]/g)?.length).toBe(1);
|
|
22
|
+
expect(out).not.toContain("[PI-PI] [PI-PI]");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("adds the prefix when the body lacks one", () => {
|
|
26
|
+
const out = advanceBanner("plain body");
|
|
27
|
+
expect(out.startsWith("[PI-PI]")).toBe(true);
|
|
28
|
+
expect(out).toContain("plain body");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
const BANNER_SEPARATOR = "─".repeat(48);
|
|
2
|
+
|
|
3
|
+
export function advanceBanner(body: string): string {
|
|
4
|
+
const content = body.startsWith("[PI-PI]") ? body.slice("[PI-PI]".length).trimStart() : body;
|
|
5
|
+
return `[PI-PI]\n${BANNER_SEPARATOR}\n${content}\n${BANNER_SEPARATOR}`;
|
|
6
|
+
}
|