@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.
Files changed (114) hide show
  1. package/3p/pi-ask-user/index.ts +71 -124
  2. package/3p/pi-ask-user/single-select-layout.ts +3 -14
  3. package/3p/pi-subagents/package.json +13 -7
  4. package/3p/pi-subagents/src/agent-manager.ts +243 -79
  5. package/3p/pi-subagents/src/agent-runner.ts +501 -365
  6. package/3p/pi-subagents/src/agent-types.ts +46 -57
  7. package/3p/pi-subagents/src/cross-extension-rpc.ts +52 -46
  8. package/3p/pi-subagents/src/custom-agents.ts +30 -6
  9. package/3p/pi-subagents/src/default-agents.ts +25 -43
  10. package/3p/pi-subagents/src/enabled-models.ts +180 -0
  11. package/3p/pi-subagents/src/index.ts +599 -839
  12. package/3p/pi-subagents/src/model-resolver.ts +26 -7
  13. package/3p/pi-subagents/src/output-file.ts +21 -2
  14. package/3p/pi-subagents/src/prompts.ts +17 -3
  15. package/3p/pi-subagents/src/schedule-store.ts +153 -0
  16. package/3p/pi-subagents/src/schedule.ts +365 -0
  17. package/3p/pi-subagents/src/settings.ts +261 -0
  18. package/3p/pi-subagents/src/skill-loader.ts +77 -54
  19. package/3p/pi-subagents/src/status-note.ts +25 -0
  20. package/3p/pi-subagents/src/types.ts +97 -6
  21. package/3p/pi-subagents/src/ui/agent-widget.ts +94 -21
  22. package/3p/pi-subagents/src/ui/conversation-viewer.ts +147 -35
  23. package/3p/pi-subagents/src/ui/schedule-menu.ts +104 -0
  24. package/3p/pi-subagents/src/ui/viewer-keys.ts +39 -0
  25. package/3p/pi-subagents/src/usage.ts +60 -0
  26. package/3p/pi-subagents/src/worktree.ts +49 -20
  27. package/extensions/orchestrator/agents/advisor.ts +13 -8
  28. package/extensions/orchestrator/agents/brainstorm-reviewer.ts +8 -3
  29. package/extensions/orchestrator/agents/code-reviewer.ts +29 -8
  30. package/extensions/orchestrator/agents/constraints.test.ts +65 -1
  31. package/extensions/orchestrator/agents/constraints.ts +73 -2
  32. package/extensions/orchestrator/agents/deep-debugger.ts +13 -8
  33. package/extensions/orchestrator/agents/explore.ts +12 -6
  34. package/extensions/orchestrator/agents/librarian.ts +13 -5
  35. package/extensions/orchestrator/agents/plan-reviewer.ts +8 -3
  36. package/extensions/orchestrator/agents/planner.ts +8 -3
  37. package/extensions/orchestrator/agents/pools.test.ts +56 -0
  38. package/extensions/orchestrator/agents/prompts.test.ts +52 -10
  39. package/extensions/orchestrator/agents/registry.test.ts +245 -0
  40. package/extensions/orchestrator/agents/registry.ts +145 -10
  41. package/extensions/orchestrator/agents/reviewer.ts +14 -8
  42. package/extensions/orchestrator/agents/task.ts +12 -6
  43. package/extensions/orchestrator/agents/tool-routing.ts +213 -69
  44. package/extensions/orchestrator/agents/wait-for-completion.test.ts +171 -0
  45. package/extensions/orchestrator/ai-comment-cleanup.test.ts +89 -0
  46. package/extensions/orchestrator/ai-comment-cleanup.ts +73 -0
  47. package/extensions/orchestrator/ast-search.test.ts +124 -0
  48. package/extensions/orchestrator/billing-spoof.test.ts +67 -0
  49. package/extensions/orchestrator/billing-spoof.ts +95 -0
  50. package/extensions/orchestrator/cbm.more.test.ts +358 -0
  51. package/extensions/orchestrator/command-handlers.test.ts +47 -0
  52. package/extensions/orchestrator/command-handlers.ts +49 -27
  53. package/extensions/orchestrator/commands.test.ts +15 -2
  54. package/extensions/orchestrator/commands.ts +1 -1
  55. package/extensions/orchestrator/config.test.ts +129 -2
  56. package/extensions/orchestrator/config.ts +115 -19
  57. package/extensions/orchestrator/context.test.ts +46 -0
  58. package/extensions/orchestrator/context.ts +34 -5
  59. package/extensions/orchestrator/custom-footer.test.ts +105 -0
  60. package/extensions/orchestrator/custom-footer.ts +22 -20
  61. package/extensions/orchestrator/doctor.more.test.ts +315 -0
  62. package/extensions/orchestrator/doctor.ts +6 -2
  63. package/extensions/orchestrator/event-handlers.more.test.ts +561 -0
  64. package/extensions/orchestrator/event-handlers.test.ts +411 -10
  65. package/extensions/orchestrator/event-handlers.ts +738 -349
  66. package/extensions/orchestrator/exa.more.test.ts +118 -0
  67. package/extensions/orchestrator/flant-infra.more.test.ts +326 -0
  68. package/extensions/orchestrator/flant-infra.test.ts +127 -0
  69. package/extensions/orchestrator/flant-infra.ts +113 -39
  70. package/extensions/orchestrator/index.test.ts +76 -0
  71. package/extensions/orchestrator/index.ts +2 -0
  72. package/extensions/orchestrator/integration.test.ts +488 -74
  73. package/extensions/orchestrator/model-registry.test.ts +2 -1
  74. package/extensions/orchestrator/model-registry.ts +12 -2
  75. package/extensions/orchestrator/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  76. package/extensions/orchestrator/orchestrator.test.ts +67 -0
  77. package/extensions/orchestrator/orchestrator.ts +165 -85
  78. package/extensions/orchestrator/phases/brainstorm.test.ts +30 -1
  79. package/extensions/orchestrator/phases/brainstorm.ts +53 -12
  80. package/extensions/orchestrator/phases/implementation.ts +2 -0
  81. package/extensions/orchestrator/phases/machine.test.ts +53 -1
  82. package/extensions/orchestrator/phases/machine.ts +15 -2
  83. package/extensions/orchestrator/phases/planning.test.ts +47 -1
  84. package/extensions/orchestrator/phases/planning.ts +18 -3
  85. package/extensions/orchestrator/phases/review-task.test.ts +20 -0
  86. package/extensions/orchestrator/phases/review-task.ts +47 -14
  87. package/extensions/orchestrator/phases/review.test.ts +36 -0
  88. package/extensions/orchestrator/phases/review.ts +62 -10
  89. package/extensions/orchestrator/phases/spawn-blocking.test.ts +1 -0
  90. package/extensions/orchestrator/phases/verdict.ts +6 -5
  91. package/extensions/orchestrator/plannotator-open-failure.test.ts +67 -0
  92. package/extensions/orchestrator/plannotator.test.ts +38 -1
  93. package/extensions/orchestrator/plannotator.ts +50 -3
  94. package/extensions/orchestrator/pp-menu.leaves.test.ts +590 -0
  95. package/extensions/orchestrator/pp-menu.more.test.ts +524 -0
  96. package/extensions/orchestrator/pp-menu.test.ts +314 -1
  97. package/extensions/orchestrator/pp-menu.ts +937 -450
  98. package/extensions/orchestrator/pp-state-tools.test.ts +80 -0
  99. package/extensions/orchestrator/pp-state-tools.ts +65 -0
  100. package/extensions/orchestrator/rate-limit-fallback.more.test.ts +241 -0
  101. package/extensions/orchestrator/rate-limit-fallback.test.ts +20 -1
  102. package/extensions/orchestrator/rate-limit-fallback.ts +12 -0
  103. package/extensions/orchestrator/review-files.test.ts +26 -0
  104. package/extensions/orchestrator/review-files.ts +3 -0
  105. package/extensions/orchestrator/state.test.ts +82 -1
  106. package/extensions/orchestrator/state.ts +110 -23
  107. package/extensions/orchestrator/suppress-pierre-theme-spam.test.ts +56 -0
  108. package/extensions/orchestrator/suppress-pierre-theme-spam.ts +45 -0
  109. package/extensions/orchestrator/transition-controller.test.ts +100 -0
  110. package/extensions/orchestrator/transition-controller.ts +61 -3
  111. package/extensions/orchestrator/validate-artifacts.ts +1 -1
  112. package/package.json +6 -2
  113. package/scripts/test-3p.sh +52 -0
  114. package/AGENTS.md +0 -28
@@ -40,23 +40,30 @@ 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
- registerAgentDefinitions: vi.fn(),
45
- unregisterAgentDefinitions: vi.fn(),
46
- setExtensionOnlyMode: vi.fn(),
47
- spawnViaRpc: vi.fn(async (_pi: any, _type: string) => ({ id: `mock-${Math.random().toString(36).slice(2)}` })),
48
- waitForCompletion: vi.fn(async () => undefined),
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")>();
53
58
  return { ...original, loadConfig: () => ({
54
59
  general: {
55
60
  autoCommit: false,
61
+ injectAgentsMd: true,
56
62
  loadExtraRepoConfigs: true,
57
63
  logLevel: "info",
58
64
  },
59
65
  agents: {
66
+ maxConcurrentSubagents: 7,
60
67
  orchestrators: {
61
68
  implement: { model: "test/model", thinking: "high" },
62
69
  plan: { model: "test/model", thinking: "high" },
@@ -70,9 +77,14 @@ vi.mock("./config.js", async (importOriginal) => {
70
77
  explore: { model: "test/explore", thinking: "low" },
71
78
  librarian: { model: "test/librarian", thinking: "medium" },
72
79
  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" },
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" }],
76
88
  },
77
89
  presetGroups: {
78
90
  planners: {
@@ -209,10 +221,12 @@ function makeConfig() {
209
221
  return {
210
222
  general: {
211
223
  autoCommit: false,
224
+ injectAgentsMd: true,
212
225
  loadExtraRepoConfigs: true,
213
226
  logLevel: "info",
214
227
  },
215
228
  agents: {
229
+ maxConcurrentSubagents: 7,
216
230
  orchestrators: {
217
231
  implement: { model: "test/model", thinking: "high" },
218
232
  plan: { model: "test/model", thinking: "high" },
@@ -226,9 +240,14 @@ function makeConfig() {
226
240
  explore: { model: "test/explore", thinking: "low" },
227
241
  librarian: { model: "test/librarian", thinking: "medium" },
228
242
  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" },
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" }],
232
251
  },
233
252
  presetGroups: {
234
253
  planners: {
@@ -677,8 +696,12 @@ describe("review cycle lifecycle", () => {
677
696
  await ppPhaseComplete.execute("call-2", { summary: "plan done" }, undefined, undefined, ctx);
678
697
  await new Promise((r) => setTimeout(r, 10));
679
698
 
699
+ // Pick the auto preset first: with zero enabled reviewers it notifies and
700
+ // (#3d) loops back to the Review submenu — NOT the top-level /pp menu — so the
701
+ // next expectation is the Review submenu directly, without re-selecting "Review".
680
702
  expectReviewAuto(menu);
681
- expectReviewOnMyOwn(menu, "Skip markers");
703
+ menu.expect({ question: "Review", options: { include: ["Review on my own"] }, choose: "Review on my own" });
704
+ menu.expect({ question: "Editor review", options: { include: ["Skip markers"] }, choose: "Skip markers" });
682
705
  const result = await ppPhaseComplete.execute("call-3", { summary: "implemented" }, undefined, undefined, ctx);
683
706
 
684
707
  expect(result.content[0].text).toContain("continue");
@@ -1311,6 +1334,49 @@ describe("edge cases and regressions", () => {
1311
1334
  expect(finalState.reviewCycle).toBeNull();
1312
1335
  });
1313
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
+
1314
1380
  it("continue brainstorming sets step back to llm_work", async () => {
1315
1381
  const cwd = makeTempDir();
1316
1382
  const { pi, orchestrator } = await setupOrchestrator(cwd);
@@ -1318,7 +1384,7 @@ describe("edge cases and regressions", () => {
1318
1384
 
1319
1385
  await orchestrator.startTask(ctx as any, "implement", "Continue test");
1320
1386
 
1321
- expectReviewOnMyOwn(menu);
1387
+ expectReviewOnMyOwn(menu, "Skip markers");
1322
1388
  const ppPhaseComplete = getTool(pi, "pp_phase_complete");
1323
1389
  const result = await ppPhaseComplete.execute("call-1", { summary: "not done yet" }, undefined, undefined, ctx);
1324
1390
 
@@ -1400,7 +1466,7 @@ describe("edge cases and regressions", () => {
1400
1466
  "utf-8",
1401
1467
  );
1402
1468
 
1403
- expectReviewOnMyOwn(menu);
1469
+ expectReviewOnMyOwn(menu, "Skip markers");
1404
1470
  const result = await ppPhaseComplete.execute("call-2", { summary: "plan ready" }, undefined, undefined, ctx);
1405
1471
 
1406
1472
  expect(result.content[0].text).toContain("continue");
@@ -1449,6 +1515,83 @@ describe("edge cases and regressions", () => {
1449
1515
  expect(orchestrator.active!.state.step).toBe("llm_work");
1450
1516
  });
1451
1517
 
1518
+ it("editor-review Done returns AI_REVIEW state-file instructions in brainstorm phase", async () => {
1519
+ const cwd = makeTempDir();
1520
+ const { pi, orchestrator } = await setupOrchestrator(cwd);
1521
+ const ctx = makeCtx();
1522
+
1523
+ await orchestrator.startTask(ctx as any, "brainstorm", "Brainstorm editor review");
1524
+
1525
+ expectReviewOnMyOwn(menu, "Done");
1526
+ const ppPhaseComplete = getTool(pi, "pp_phase_complete");
1527
+ const result = await ppPhaseComplete.execute("call-1", { summary: "researched" }, undefined, undefined, ctx);
1528
+
1529
+ expect(result.content[0].text).toContain("AI_REVIEW:");
1530
+ expect(result.content[0].text).toContain("USER_REQUEST.md");
1531
+ expect(result.content[0].text).toContain("RESEARCH.md");
1532
+ expect(result.content[0].text).toContain("artifacts/*.md");
1533
+ expect(result.content[0].text).not.toContain("CHANGED files");
1534
+ expect(orchestrator.active!.state.phase).toBe("brainstorm");
1535
+ expect(orchestrator.active!.state.step).toBe("llm_work");
1536
+ });
1537
+
1538
+ it("editor-review Done returns AI_REVIEW state-file instructions in debug phase", async () => {
1539
+ const cwd = makeTempDir();
1540
+ const { pi, orchestrator } = await setupOrchestrator(cwd);
1541
+ const ctx = makeCtx();
1542
+
1543
+ await orchestrator.startTask(ctx as any, "debug", "Debug editor review");
1544
+
1545
+ expectReviewOnMyOwn(menu, "Done");
1546
+ const ppPhaseComplete = getTool(pi, "pp_phase_complete");
1547
+ const result = await ppPhaseComplete.execute("call-1", { summary: "diagnosed" }, undefined, undefined, ctx);
1548
+
1549
+ expect(result.content[0].text).toContain("AI_REVIEW:");
1550
+ expect(result.content[0].text).toContain("USER_REQUEST.md");
1551
+ expect(result.content[0].text).not.toContain("CHANGED files");
1552
+ expect(orchestrator.active!.state.phase).toBe("debug");
1553
+ expect(orchestrator.active!.state.step).toBe("llm_work");
1554
+ });
1555
+
1556
+ it("editor-review Done targets synthesized plan in plan phase", async () => {
1557
+ const cwd = makeTempDir();
1558
+ const { pi, orchestrator } = await setupOrchestrator(cwd);
1559
+ const ctx = makeCtx();
1560
+
1561
+ await orchestrator.startTask(ctx as any, "implement", "Plan editor review");
1562
+ const taskDir = orchestrator.active!.dir;
1563
+ writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
1564
+ writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
1565
+ expectBrainstormToPlan(menu);
1566
+ const ppPhaseComplete = getTool(pi, "pp_phase_complete");
1567
+ await ppPhaseComplete.execute("call-1", { summary: "done" }, undefined, undefined, ctx);
1568
+ await new Promise((r) => setTimeout(r, 10));
1569
+
1570
+ emitSubagentCreated(pi, "planner-1", "Planner (test)");
1571
+ const plansDir = join(taskDir, "plans");
1572
+ mkdirSync(plansDir, { recursive: true });
1573
+ writeFileSync(
1574
+ join(plansDir, `${Math.floor(Date.now() / 1000)}_test.md`),
1575
+ makeValidPlan(["- [ ] P1. Planner draft item — Done when: planner output exists"]),
1576
+ "utf-8",
1577
+ );
1578
+ emitSubagentCompleted(pi, "planner-1", "Planner (test)");
1579
+ writeFileSync(
1580
+ join(plansDir, `${Math.floor(Date.now() / 1000) + 1}_synthesized.md`),
1581
+ makeValidPlan(["- [ ] P1. Todo item — Done when: item intentionally remains unchecked"]),
1582
+ "utf-8",
1583
+ );
1584
+
1585
+ expectReviewOnMyOwn(menu, "Done");
1586
+ const result = await ppPhaseComplete.execute("call-2", { summary: "plan ready" }, undefined, undefined, ctx);
1587
+
1588
+ expect(result.content[0].text).toContain("AI_REVIEW:");
1589
+ expect(result.content[0].text).toContain("_synthesized.md");
1590
+ expect(result.content[0].text).not.toContain("CHANGED files");
1591
+ expect(orchestrator.active!.state.phase).toBe("plan");
1592
+ expect(orchestrator.active!.state.step).toBe("synthesize");
1593
+ });
1594
+
1452
1595
  it("generic description task does not auto-trigger agent", async () => {
1453
1596
  const cwd = makeTempDir();
1454
1597
  const { pi, orchestrator } = await setupOrchestrator(cwd);
@@ -1595,7 +1738,7 @@ describe("task modes and quick task", () => {
1595
1738
  const ctx = makeCtx();
1596
1739
 
1597
1740
  await orchestrator.startTask(ctx as any, "quick", "quick");
1598
- 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" });
1599
1742
  const pp = getCommand(pi, "pp");
1600
1743
  await pp(undefined, ctx);
1601
1744
  });
@@ -1713,7 +1856,7 @@ describe("task modes and quick task", () => {
1713
1856
  expect(orchestrator.transitionController.isRunning()).toBe(true);
1714
1857
 
1715
1858
  // Explicit "Back" navigation (not an ESC): the reminder must be preserved.
1716
- 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" });
1717
1860
 
1718
1861
  const ppPhaseComplete = getTool(pi, "pp_phase_complete");
1719
1862
  const result = await ppPhaseComplete.execute("back-1", { summary: "done" }, undefined, undefined, ctx);
@@ -1788,8 +1931,11 @@ describe("task modes and quick task", () => {
1788
1931
  emitSubagentCreated(pi, "reviewer-2", "Code reviewer (test)");
1789
1932
  emitSubagentCompleted(pi, "reviewer-2", "Code reviewer (test)");
1790
1933
 
1934
+ // Terminal handoff (#1): at the cap the autonomous implement no longer auto-
1935
+ // completes — it opens the guided menu; the user drives Next→Complete to finish.
1936
+ expectImplementToDone(menu);
1791
1937
  const fourth = await ppPhaseComplete.execute("call-4", { summary: "applied 2" }, undefined, undefined, ctx);
1792
- expect(fourth.content[0].text).toBe("");
1938
+ expect(fourth.content[0].text).toMatch(/completed/);
1793
1939
  expect(orchestrator.active).toBeNull();
1794
1940
  expect(loadTask(taskDir).phase).toBe("done");
1795
1941
  });
@@ -1831,8 +1977,11 @@ describe("task modes and quick task", () => {
1831
1977
  emitSubagentCreated(pi, "reviewer-1", "Code reviewer (test)");
1832
1978
  emitSubagentCompleted(pi, "reviewer-1", "Code reviewer (test)");
1833
1979
 
1980
+ // Terminal handoff (#1): instead of auto-completing, the guided menu opens
1981
+ // and the user drives Next→Complete to finish.
1982
+ expectImplementToDone(menu);
1834
1983
  const second = await ppPhaseComplete.execute("call-2", { summary: "applied" }, undefined, undefined, ctx);
1835
- expect(second.content[0].text).toBe("");
1984
+ expect(second.content[0].text).toMatch(/completed/);
1836
1985
  expect(orchestrator.active).toBeNull();
1837
1986
  const finalState = loadTask(taskDir);
1838
1987
  expect(finalState.phase).toBe("done");
@@ -1912,8 +2061,11 @@ describe("task modes and quick task", () => {
1912
2061
  );
1913
2062
 
1914
2063
  const ppPhaseComplete = getTool(pi, "pp_phase_complete");
2064
+ // Terminal handoff (#1): clean-approved re-entry opens the guided menu rather
2065
+ // than auto-completing; the user drives Next→Complete to finish.
2066
+ expectImplementToDone(menu);
1915
2067
  const result = await ppPhaseComplete.execute("call-1", { summary: "checklist repaired" }, undefined, undefined, ctx);
1916
- expect(result.content[0].text).toBe("");
2068
+ expect(result.content[0].text).toMatch(/completed/);
1917
2069
  expect(orchestrator.active).toBeNull();
1918
2070
  expect(loadTask(taskDir).phase).toBe("done");
1919
2071
  });
@@ -2038,7 +2190,7 @@ describe("task modes and quick task", () => {
2038
2190
  .expect({ question: "Implement", options: { include: ["Resume", "Back"] }, choose: "Resume" })
2039
2191
  .expect({ question: "Implement", options: { include: ["Back"] }, choose: "Back" })
2040
2192
  .expect({ question: "Task", options: { include: ["Back"] }, choose: "Back" })
2041
- .expect({ question: "/pp", options: { include: ["Back"] }, choose: "Back" });
2193
+ .expect({ question: "/pp", options: { include: ["Back to prompt"] }, choose: "Back to prompt" });
2042
2194
  const pp = getCommand(pi, "pp");
2043
2195
  await pp(undefined, ctx);
2044
2196
 
@@ -2102,7 +2254,8 @@ describe("task modes and quick task", () => {
2102
2254
  const beforeStart = pi._handlers.get("before_agent_start")!;
2103
2255
  const result = await beforeStart({ systemPrompt: "HARNESS_BASE_PROMPT" }, ctx);
2104
2256
  const prompt = result?.systemPrompt ?? "";
2105
- expect(prompt.startsWith("<constraints>")).toBe(true);
2257
+ expect(prompt.startsWith("<identity>")).toBe(true);
2258
+ expect(prompt).toContain("<constraints>");
2106
2259
  expect(prompt).not.toContain("HARNESS_BASE_PROMPT");
2107
2260
  expect(prompt).toContain("The moment its work is complete, call pp_phase_complete");
2108
2261
  // No interactive '/pp menu' advance guidance in autonomous mode.
@@ -2121,7 +2274,8 @@ describe("task modes and quick task", () => {
2121
2274
 
2122
2275
  const beforeStart = pi._handlers.get("before_agent_start")!;
2123
2276
  const prompt = (await beforeStart({ systemPrompt: "HARNESS_BASE_PROMPT" }, ctx))?.systemPrompt ?? "";
2124
- expect(prompt.startsWith("<constraints>")).toBe(true);
2277
+ expect(prompt.startsWith("<identity>")).toBe(true);
2278
+ expect(prompt).toContain("<constraints>");
2125
2279
  expect(prompt).toContain("ACTIVE PHASE: plan (READ-ONLY)");
2126
2280
  expect(prompt).toContain("<principles>");
2127
2281
  expect(prompt).toContain("<tools>");
@@ -2173,6 +2327,66 @@ describe("task modes and quick task", () => {
2173
2327
  expect(prompt).not.toContain("advance it via the /pp menu");
2174
2328
  });
2175
2329
 
2330
+ it("injects root AGENTS.md into the system prompt when enabled, ignoring nested/extra-repo files", async () => {
2331
+ const cwd = makeTempDir();
2332
+ const { pi, orchestrator } = await setupOrchestrator(cwd);
2333
+ const ctx = makeCtx({ cwd });
2334
+
2335
+ writeFileSync(join(cwd, "AGENTS.md"), "ROOT_AGENTS_CONTENT", "utf-8");
2336
+ mkdirSync(join(cwd, "nested"), { recursive: true });
2337
+ writeFileSync(join(cwd, "nested", "AGENTS.md"), "NESTED_AGENTS_CONTENT", "utf-8");
2338
+
2339
+ await orchestrator.startTask({ ...ctx, cwd } as any, "implement", "implement", undefined, undefined, "guided");
2340
+ orchestrator.active!.state.phase = "plan";
2341
+ orchestrator.active!.state.step = "synthesize";
2342
+
2343
+ const beforeStart = pi._handlers.get("before_agent_start")!;
2344
+ const prompt = (await beforeStart({ systemPrompt: "base" }, ctx))?.systemPrompt ?? "";
2345
+ expect(prompt).toContain(`<agents_md source="${join(cwd, "AGENTS.md")}">`);
2346
+ expect(prompt).toContain("ROOT_AGENTS_CONTENT");
2347
+ expect(prompt).not.toContain("NESTED_AGENTS_CONTENT");
2348
+ });
2349
+
2350
+ it("omits AGENTS.md block when disabled or file absent", async () => {
2351
+ const cwd = makeTempDir();
2352
+ const { pi, orchestrator } = await setupOrchestrator(cwd);
2353
+ const ctx = makeCtx({ cwd });
2354
+
2355
+ await orchestrator.startTask({ ...ctx, cwd } as any, "implement", "implement", undefined, undefined, "guided");
2356
+ orchestrator.active!.state.phase = "plan";
2357
+ orchestrator.active!.state.step = "synthesize";
2358
+
2359
+ const beforeStart = pi._handlers.get("before_agent_start")!;
2360
+ const absent = (await beforeStart({ systemPrompt: "base" }, ctx))?.systemPrompt ?? "";
2361
+ expect(absent).not.toContain("<agents_md");
2362
+
2363
+ writeFileSync(join(cwd, "AGENTS.md"), "ROOT_AGENTS_CONTENT", "utf-8");
2364
+ orchestrator.config.general.injectAgentsMd = false;
2365
+ const disabled = (await beforeStart({ systemPrompt: "base" }, ctx))?.systemPrompt ?? "";
2366
+ expect(disabled).not.toContain("<agents_md");
2367
+ expect(disabled).not.toContain("ROOT_AGENTS_CONTENT");
2368
+ });
2369
+
2370
+ it("auto-refreshes AGENTS.md content on the next turn without restart", async () => {
2371
+ const cwd = makeTempDir();
2372
+ const { pi, orchestrator } = await setupOrchestrator(cwd);
2373
+ const ctx = makeCtx({ cwd });
2374
+
2375
+ writeFileSync(join(cwd, "AGENTS.md"), "FIRST_VERSION", "utf-8");
2376
+ await orchestrator.startTask({ ...ctx, cwd } as any, "implement", "implement", undefined, undefined, "guided");
2377
+ orchestrator.active!.state.phase = "plan";
2378
+ orchestrator.active!.state.step = "synthesize";
2379
+
2380
+ const beforeStart = pi._handlers.get("before_agent_start")!;
2381
+ const first = (await beforeStart({ systemPrompt: "base" }, ctx))?.systemPrompt ?? "";
2382
+ expect(first).toContain("FIRST_VERSION");
2383
+
2384
+ writeFileSync(join(cwd, "AGENTS.md"), "SECOND_VERSION", "utf-8");
2385
+ const second = (await beforeStart({ systemPrompt: "base" }, ctx))?.systemPrompt ?? "";
2386
+ expect(second).toContain("SECOND_VERSION");
2387
+ expect(second).not.toContain("FIRST_VERSION");
2388
+ });
2389
+
2176
2390
  it("autonomous plan-phase prompt body contains no /pp guidance", async () => {
2177
2391
  const cwd = makeTempDir();
2178
2392
  const { pi, orchestrator } = await setupOrchestrator(cwd);
@@ -2199,6 +2413,52 @@ describe("task modes and quick task", () => {
2199
2413
  expect(state.reviewerFailureAutoRetried).toBe(false);
2200
2414
  });
2201
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
+
2202
2462
  it("blocks ask_user in autonomous mode after first phase", async () => {
2203
2463
  const cwd = makeTempDir();
2204
2464
  const { pi, orchestrator } = await setupOrchestrator(cwd);
@@ -2347,8 +2607,11 @@ describe("task modes and quick task", () => {
2347
2607
  emitSubagentCreated(pi, "reviewer-auto-1", "Code reviewer (test)");
2348
2608
  emitSubagentCompleted(pi, "reviewer-auto-1", "Code reviewer (test)");
2349
2609
 
2610
+ // Terminal handoff (#1): the clean second pass opens the guided menu; the user
2611
+ // drives Next→Complete to finish rather than auto-completing.
2612
+ expectImplementToDone(menu);
2350
2613
  const second = await ppPhaseComplete.execute("call-autonomous-review-2", { summary: "applied" }, undefined, undefined, ctx);
2351
- expect(second.content[0].text).toBe("");
2614
+ expect(second.content[0].text).toMatch(/completed/);
2352
2615
  expect(orchestrator.active).toBeNull();
2353
2616
  });
2354
2617
 
@@ -2393,10 +2656,9 @@ describe("task modes and quick task", () => {
2393
2656
  menu
2394
2657
  .expect({ question: "/pp", options: { include: ["Task"] }, choose: "Task" })
2395
2658
  .expect({ question: "Task", options: { include: ["Review"] }, choose: "Review" })
2396
- .expect({ question: "Review", options: { include: ["Describe"] }, choose: "Describe" })
2659
+ .expect({ question: "Review", options: { exact: ["New", "Resume", "Back"] }, choose: "New" })
2397
2660
  .expect({ question: "Mode", options: { include: ["Autonomous"] }, choose: "Autonomous" })
2398
2661
  .expect({ question: "Autonomous", options: { include: ["Start"] }, choose: "Start" });
2399
- ctx.ui.input.mockResolvedValueOnce("Review current branch changes");
2400
2662
  const pp = getCommand(pi, "pp");
2401
2663
  await pp(undefined, ctx);
2402
2664
 
@@ -2466,6 +2728,11 @@ describe("review task lifecycle", () => {
2466
2728
  await orchestrator.startTask(ctx as any, "review", "Review flow");
2467
2729
  const taskDir = orchestrator.active!.dir;
2468
2730
 
2731
+ // #10: the review phase cannot complete without an ANCHORS-bearing final_pass file.
2732
+ const reviewsDir = join(taskDir, "code-reviews");
2733
+ mkdirSync(reviewsDir, { recursive: true });
2734
+ writeFileSync(join(reviewsDir, "1_final_pass-1.md"), "# Review\nANCHORS: (none)\n", "utf-8");
2735
+
2469
2736
  await moveTaskToImplementPhase(pi, orchestrator, ctx, "call-review-to-plan", "call-plan-to-implement");
2470
2737
  expect(orchestrator.active!.state.phase).toBe("implement");
2471
2738
 
@@ -2557,6 +2824,22 @@ describe("modified file tracking", () => {
2557
2824
  expect(orchestrator.active!.state.reviewApprovedClean).toBe(false);
2558
2825
  });
2559
2826
 
2827
+ it("source write clears afterImplementRan so post-handoff edits re-run afterImplement (#1)", async () => {
2828
+ const cwd = makeTempDir();
2829
+ const { pi, orchestrator } = await setupOrchestrator(cwd);
2830
+ const ctx = makeCtx();
2831
+
2832
+ await orchestrator.startTask(ctx as any, "implement", "Rerun hooks");
2833
+ await moveTaskToImplementPhase(pi, orchestrator, ctx, "call-rerun-brainstorm", "call-rerun-plan");
2834
+ // Simulate the terminal handoff having already run afterImplement.
2835
+ orchestrator.active!.state.afterImplementRan = true;
2836
+
2837
+ const toolResult = pi._handlers.get("tool_result")!;
2838
+ await toolResult({ toolName: "write", input: { path: "src/d.ts" }, isError: false, content: [] }, {});
2839
+
2840
+ expect(orchestrator.active!.state.afterImplementRan).toBe(false);
2841
+ });
2842
+
2560
2843
  it("tool_result ignores writes inside .pp directory", async () => {
2561
2844
  const cwd = makeTempDir();
2562
2845
  const { pi, orchestrator } = await setupOrchestrator(cwd);
@@ -3287,10 +3570,10 @@ describe("tool blocking", () => {
3287
3570
  });
3288
3571
 
3289
3572
  describe("error retry", () => {
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.
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.
3294
3577
  it("turn_end with a non-SDK-retryable error uses pi-pi's idle-gated retry", async () => {
3295
3578
  vi.useFakeTimers();
3296
3579
  const cwd = makeTempDir();
@@ -3303,28 +3586,33 @@ describe("error retry", () => {
3303
3586
  await turnEnd({ message: { stopReason: "error", errorMessage: "invalid tool arguments", content: [] } }, ctx);
3304
3587
 
3305
3588
  expect(orchestrator.errorRetryCount).toBe(1);
3306
- expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("Retrying in 2s"), "warning");
3589
+ expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("Retrying in 2s"), "info");
3307
3590
 
3308
3591
  await vi.advanceTimersByTimeAsync(2000);
3309
- expect(pi.sendUserMessage).toHaveBeenCalledWith(expect.stringContaining("Previous request failed"), { deliverAs: "followUp" });
3592
+ expect(pi.sendUserMessage).toHaveBeenCalledWith(expect.stringContaining("Transient API error"), { deliverAs: "followUp" });
3310
3593
  vi.useRealTimers();
3311
3594
  });
3312
3595
 
3313
- it("turn_end defers SDK-retryable errors to the SDK (no pi-pi retry)", async () => {
3596
+ it("turn_end retries SDK-retryable errors via pi-pi's idle-gated retry", async () => {
3597
+ vi.useFakeTimers();
3314
3598
  const cwd = makeTempDir();
3315
3599
  const { pi, orchestrator } = await setupOrchestrator(cwd);
3316
3600
  const ctx = makeCtx();
3317
3601
 
3318
- await orchestrator.startTask(ctx as any, "implement", "sdk defer test");
3602
+ await orchestrator.startTask(ctx as any, "implement", "sdk retryable test");
3319
3603
  const turnEnd = pi._handlers.get("turn_end")!;
3320
3604
 
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()).
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.
3323
3608
  await turnEnd({ message: { stopReason: "error", errorMessage: "Anthropic stream ended before message_stop", content: [] } }, ctx);
3324
3609
 
3325
- expect(orchestrator.errorRetryCount).toBe(0);
3326
- expect(orchestrator.pendingRetryTimer).toBeNull();
3327
- expect(ctx.ui.notify).not.toHaveBeenCalledWith(expect.stringContaining("Retrying in"), "warning");
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();
3328
3616
  });
3329
3617
 
3330
3618
  it("turn_end stops retrying after max retries (non-SDK-retryable)", async () => {
@@ -3336,7 +3624,8 @@ describe("error retry", () => {
3336
3624
  await orchestrator.startTask(ctx as any, "implement", "retry max test");
3337
3625
  const turnEnd = pi._handlers.get("turn_end")!;
3338
3626
 
3339
- for (let i = 0; i < 6; i++) {
3627
+ // 8 retries then a 9th error turn that trips the halt.
3628
+ for (let i = 0; i < 9; i++) {
3340
3629
  await turnEnd({ message: { stopReason: "error", errorMessage: "invalid tool arguments", content: [] } }, ctx);
3341
3630
  }
3342
3631
 
@@ -3403,6 +3692,76 @@ describe("error retry", () => {
3403
3692
  expect(pi.sendUserMessage).toHaveBeenCalledWith(expect.stringContaining("Continue the implement phase"), { deliverAs: "followUp" });
3404
3693
  });
3405
3694
 
3695
+ it("drops a continuation nudge whose phase no longer matches at delivery", async () => {
3696
+ const cwd = makeTempDir();
3697
+ const { pi, orchestrator } = await setupOrchestrator(cwd);
3698
+ const ctx = makeCtx();
3699
+
3700
+ await orchestrator.startTask(ctx as any, "implement", "stale phase nudge");
3701
+ orchestrator.active!.state.phase = "plan";
3702
+ orchestrator.active!.state.step = "llm_work";
3703
+ const turnEnd = pi._handlers.get("turn_end")!;
3704
+ await turnEnd({ message: { stopReason: "stop", content: [] }, toolResults: [] }, ctx);
3705
+ const nudge = (pi.sendUserMessage as any).mock.calls.map((c: any[]) => c[0]).find((t: string) => t.includes("Continue the plan phase"));
3706
+ expect(nudge).toBeDefined();
3707
+ expect(orchestrator.pendingNudges.has(nudge)).toBe(true);
3708
+
3709
+ // Phase advances before the queued nudge is delivered.
3710
+ orchestrator.active!.state.phase = "implement";
3711
+ const beforeStart = pi._handlers.get("before_agent_start")!;
3712
+ const abort = vi.fn();
3713
+ await beforeStart({ systemPrompt: "base", prompt: nudge }, makeCtx({ abort }));
3714
+
3715
+ expect(abort).toHaveBeenCalledOnce();
3716
+ expect(orchestrator.pendingNudges.has(nudge)).toBe(false);
3717
+ });
3718
+
3719
+ it("drops a continuation nudge whose task token changed at delivery (same phase)", async () => {
3720
+ const cwd = makeTempDir();
3721
+ const { pi, orchestrator } = await setupOrchestrator(cwd);
3722
+ const ctx = makeCtx();
3723
+
3724
+ await orchestrator.startTask(ctx as any, "implement", "stale token nudge");
3725
+ orchestrator.active!.state.phase = "plan";
3726
+ orchestrator.active!.state.step = "llm_work";
3727
+ const turnEnd = pi._handlers.get("turn_end")!;
3728
+ await turnEnd({ message: { stopReason: "stop", content: [] }, toolResults: [] }, ctx);
3729
+ const nudge = (pi.sendUserMessage as any).mock.calls.map((c: any[]) => c[0]).find((t: string) => t.includes("Continue the plan phase"));
3730
+ expect(nudge).toBeDefined();
3731
+
3732
+ // A new task starts (token bumped), also at the plan phase — the old nudge
3733
+ // must not drive a turn in the new task even though the phase name matches.
3734
+ orchestrator.activeTaskToken++;
3735
+ orchestrator.active!.state.phase = "plan";
3736
+ const beforeStart = pi._handlers.get("before_agent_start")!;
3737
+ const abort = vi.fn();
3738
+ await beforeStart({ systemPrompt: "base", prompt: nudge }, makeCtx({ abort }));
3739
+
3740
+ expect(abort).toHaveBeenCalledOnce();
3741
+ });
3742
+
3743
+ it("delivers a continuation nudge whose phase and token still match", async () => {
3744
+ const cwd = makeTempDir();
3745
+ const { pi, orchestrator } = await setupOrchestrator(cwd);
3746
+ const ctx = makeCtx();
3747
+
3748
+ await orchestrator.startTask(ctx as any, "implement", "fresh nudge");
3749
+ orchestrator.active!.state.phase = "implement";
3750
+ orchestrator.active!.state.step = "llm_work";
3751
+ const turnEnd = pi._handlers.get("turn_end")!;
3752
+ await turnEnd({ message: { stopReason: "stop", content: [] }, toolResults: [] }, ctx);
3753
+ const nudge = (pi.sendUserMessage as any).mock.calls.map((c: any[]) => c[0]).find((t: string) => t.includes("Continue the implement phase"));
3754
+ expect(nudge).toBeDefined();
3755
+
3756
+ const beforeStart = pi._handlers.get("before_agent_start")!;
3757
+ const abort = vi.fn();
3758
+ const result = await beforeStart({ systemPrompt: "base", prompt: nudge }, makeCtx({ abort }));
3759
+
3760
+ expect(abort).not.toHaveBeenCalled();
3761
+ expect(result?.systemPrompt).toContain("<task>");
3762
+ expect(orchestrator.pendingNudges.has(nudge)).toBe(false);
3763
+ });
3764
+
3406
3765
  it("suppresses nudges while the controller is transitioning (not running)", async () => {
3407
3766
  const cwd = makeTempDir();
3408
3767
  const { pi, orchestrator } = await setupOrchestrator(cwd);
@@ -3634,6 +3993,54 @@ describe("compaction", () => {
3634
3993
  expect(result.compaction.summary).toBe("Task done summary");
3635
3994
  });
3636
3995
 
3996
+ it("task-boundary discard uses distinct discard wording and keeps no verbatim transcript", async () => {
3997
+ const cwd = makeTempDir();
3998
+ const { pi, orchestrator } = await setupOrchestrator(cwd);
3999
+ orchestrator.lastCtx = makeCtx({ isIdle: vi.fn().mockReturnValue(false) });
4000
+
4001
+ void orchestrator.transitionController.requestTransition({
4002
+ kind: "done",
4003
+ discard: true,
4004
+ summary: "Task \"x\" (implement) is finished — DISCARD its entire conversation. Do NOT carry forward.",
4005
+ });
4006
+ orchestrator.transitionController.onAgentEnd();
4007
+ const beforeCompact = pi._handlers.get("session_before_compact")!;
4008
+ const result = await beforeCompact(
4009
+ {
4010
+ preparation: { firstKeptEntryId: "old-kept", tokensBefore: 999 },
4011
+ branchEntries: [{ id: "e-a" }, { id: "e-b" }, { id: "newest" }],
4012
+ },
4013
+ {},
4014
+ );
4015
+
4016
+ // Distinct from the phase-transition summary, and instructs discard.
4017
+ expect(result.compaction.summary).not.toBe("Phase transition in progress.");
4018
+ expect(result.compaction.summary).toContain("DISCARD");
4019
+ // Verbatim window collapsed to the newest entry — nothing prior kept.
4020
+ expect(result.compaction.firstKeptEntryId).toBe("newest");
4021
+ });
4022
+
4023
+ it("phase transition keeps the default recent verbatim window (no discard)", async () => {
4024
+ const cwd = makeTempDir();
4025
+ const { pi, orchestrator } = await setupOrchestrator(cwd);
4026
+ orchestrator.lastCtx = makeCtx({ isIdle: vi.fn().mockReturnValue(false) });
4027
+
4028
+ void orchestrator.transitionController.requestTransition({ kind: "phase", summary: "Phase summary text" });
4029
+ orchestrator.transitionController.onAgentEnd();
4030
+ const beforeCompact = pi._handlers.get("session_before_compact")!;
4031
+ const result = await beforeCompact(
4032
+ {
4033
+ preparation: { firstKeptEntryId: "old-kept", tokensBefore: 111 },
4034
+ branchEntries: [{ id: "e-a" }, { id: "newest" }],
4035
+ },
4036
+ {},
4037
+ );
4038
+
4039
+ expect(result.compaction.summary).toBe("Phase summary text");
4040
+ expect(result.compaction.firstKeptEntryId).toBe("old-kept");
4041
+ });
4042
+
4043
+
3637
4044
  it("session_before_compact re-injects artifacts after natural compaction", async () => {
3638
4045
  const cwd = makeTempDir();
3639
4046
  const { pi, orchestrator } = await setupOrchestrator(cwd);
@@ -3745,7 +4152,7 @@ describe("session lifecycle", () => {
3745
4152
  expect(toolNames).toContain("pp_register_repo");
3746
4153
  });
3747
4154
 
3748
- it("session_start detects paused tasks and notifies", async () => {
4155
+ it("session_start does not notify about paused tasks (#1: all hints removed)", async () => {
3749
4156
  const cwd = makeTempDir();
3750
4157
  createTask(cwd, "implement", "Paused task");
3751
4158
  const pi = makePi();
@@ -3757,7 +4164,7 @@ describe("session lifecycle", () => {
3757
4164
  const sessionStart = pi._handlers.get("session_start")!;
3758
4165
  await sessionStart({}, ctx);
3759
4166
 
3760
- expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("Paused task"), "info");
4167
+ expect(ctx.ui.notify).not.toHaveBeenCalledWith(expect.stringContaining("Paused task"), "info");
3761
4168
  });
3762
4169
 
3763
4170
  it("session_start loads config", async () => {
@@ -3789,29 +4196,31 @@ describe("menu contracts", () => {
3789
4196
 
3790
4197
  menu
3791
4198
  .expect({ question: "/pp", options: { include: ["Settings"] }, choose: "Settings" })
3792
- .expect({ question: "Settings", options: { exact: ["General", "Agents", "Commands", "Performance", "LSP", "Back"] }, choose: "Back" })
3793
- .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" });
3794
4201
 
3795
4202
  const pp = getCommand(pi, "pp");
3796
4203
  await pp(undefined, ctx);
3797
4204
  });
3798
4205
 
3799
- it("info menu shows Doctor and hides LSP", async () => {
4206
+ it("info menu (under Settings) shows Doctor and hides LSP", async () => {
3800
4207
  const cwd = makeTempDir();
3801
4208
  const { pi } = await setupOrchestrator(cwd);
3802
4209
  const ctx = makeCtx();
3803
4210
 
3804
4211
  menu
3805
- .expect({ question: "/pp", options: { include: ["Info"] }, choose: "Info" })
4212
+ .expect({ question: "/pp", options: { include: ["Settings"] }, choose: "Settings" })
4213
+ .expect({ question: "Settings", options: { include: ["Info"] }, choose: "Info" })
3806
4214
  .expect({
3807
4215
  question: "Info",
3808
4216
  options: {
3809
- include: ["Subagents", "Usage", "Doctor", "Back"],
3810
- exclude: ["LSP"],
4217
+ include: ["Usage", "Doctor", "Back"],
4218
+ exclude: ["LSP", "Subagents"],
3811
4219
  },
3812
4220
  choose: "Back",
3813
4221
  })
3814
- .expect({ question: "/pp", options: { include: ["Back"] }, choose: "Back" });
4222
+ .expect({ question: "Settings", options: { include: ["Back"] }, choose: "Back" })
4223
+ .expect({ question: "/pp", options: { include: ["Back to prompt"] }, choose: "Back to prompt" });
3815
4224
 
3816
4225
  const pp = getCommand(pi, "pp");
3817
4226
  await pp(undefined, ctx);
@@ -3824,10 +4233,12 @@ describe("menu contracts", () => {
3824
4233
  const runDoctorSpy = vi.mocked(doctorModule.runDoctor);
3825
4234
 
3826
4235
  menu
3827
- .expect({ question: "/pp", options: { include: ["Info"] }, choose: "Info" })
4236
+ .expect({ question: "/pp", options: { include: ["Settings"] }, choose: "Settings" })
4237
+ .expect({ question: "Settings", options: { include: ["Info"] }, choose: "Info" })
3828
4238
  .expect({ question: "Info", options: { include: ["Doctor", "Back"] }, choose: "Doctor" })
3829
4239
  .expect({ question: "Info", options: { include: ["Back"] }, choose: "Back" })
3830
- .expect({ question: "/pp", options: { include: ["Back"] }, choose: "Back" });
4240
+ .expect({ question: "Settings", options: { include: ["Back"] }, choose: "Back" })
4241
+ .expect({ question: "/pp", options: { include: ["Back to prompt"] }, choose: "Back to prompt" });
3831
4242
 
3832
4243
  const pp = getCommand(pi, "pp");
3833
4244
  await pp(undefined, ctx);
@@ -3849,7 +4260,7 @@ describe("menu contracts", () => {
3849
4260
  .expect({ question: "LSP", options: { exact: ["Restart all servers", "Back"] }, choose: "Restart all servers" })
3850
4261
  .expect({ question: "LSP", options: { include: ["Back"] }, choose: "Back" })
3851
4262
  .expect({ question: "Settings", options: { include: ["Back"] }, choose: "Back" })
3852
- .expect({ question: "/pp", options: { include: ["Back"] }, choose: "Back" });
4263
+ .expect({ question: "/pp", options: { include: ["Back to prompt"] }, choose: "Back to prompt" });
3853
4264
 
3854
4265
  const pp = getCommand(pi, "pp");
3855
4266
  await pp(undefined, ctx);
@@ -3869,7 +4280,7 @@ describe("menu contracts", () => {
3869
4280
  .expect({ question: "LSP", options: { exact: ["Restart all servers", "Back"] }, choose: "Restart all servers" })
3870
4281
  .expect({ question: "LSP", options: { include: ["Back"] }, choose: "Back" })
3871
4282
  .expect({ question: "Settings", options: { include: ["Back"] }, choose: "Back" })
3872
- .expect({ question: "/pp", options: { include: ["Back"] }, choose: "Back" });
4283
+ .expect({ question: "/pp", options: { include: ["Back to prompt"] }, choose: "Back to prompt" });
3873
4284
 
3874
4285
  const pp = getCommand(pi, "pp");
3875
4286
  await pp(undefined, ctx);
@@ -3885,9 +4296,9 @@ describe("menu contracts", () => {
3885
4296
  menu
3886
4297
  .expect({ question: "/pp", options: { include: ["Settings"] }, choose: "Settings" })
3887
4298
  .expect({ question: "Settings", options: { include: ["Agents", "Back"] }, choose: "Agents" })
3888
- .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" })
3889
4300
  .expect({ question: "Settings", options: { include: ["Back"] }, choose: "Back" })
3890
- .expect({ question: "/pp", options: { include: ["Back"] }, choose: "Back" });
4301
+ .expect({ question: "/pp", options: { include: ["Back to prompt"] }, choose: "Back to prompt" });
3891
4302
 
3892
4303
  const pp = getCommand(pi, "pp");
3893
4304
  await pp(undefined, ctx);
@@ -3903,9 +4314,9 @@ describe("menu contracts", () => {
3903
4314
  menu.expect({
3904
4315
  question: m.taskMenu("implement", "brainstorm"),
3905
4316
  options: {
3906
- exact: ["Next", "Review", "Info", "Settings", "Back"],
4317
+ exact: ["Next", "Review", "Subagents", "Settings", "Back to prompt"],
3907
4318
  },
3908
- choose: "Back",
4319
+ choose: "Back to prompt",
3909
4320
  });
3910
4321
 
3911
4322
  const pp = getCommand(pi, "pp");
@@ -3925,9 +4336,9 @@ describe("menu contracts", () => {
3925
4336
  menu.expect({
3926
4337
  question: m.taskMenu("implement", "plan"),
3927
4338
  options: {
3928
- exact: ["Next", "Info", "Settings", "Back"],
4339
+ exact: ["Next", "Subagents", "Settings", "Back to prompt"],
3929
4340
  },
3930
- choose: "Back",
4341
+ choose: "Back to prompt",
3931
4342
  });
3932
4343
 
3933
4344
  const pp = getCommand(pi, "pp");
@@ -3946,9 +4357,9 @@ describe("menu contracts", () => {
3946
4357
  menu.expect({
3947
4358
  question: m.taskMenu("implement", "brainstorm"),
3948
4359
  options: {
3949
- exact: ["Next", "Review", "Info", "Settings", "Back"],
4360
+ exact: ["Next", "Review", "Subagents", "Settings", "Back to prompt"],
3950
4361
  },
3951
- choose: "Back",
4362
+ choose: "Back to prompt",
3952
4363
  });
3953
4364
 
3954
4365
  const pp = getCommand(pi, "pp");
@@ -3965,9 +4376,9 @@ describe("menu contracts", () => {
3965
4376
  menu.expect({
3966
4377
  question: m.taskMenu("debug", "debug"),
3967
4378
  options: {
3968
- exact: ["Next", "Review", "Info", "Settings", "Back"],
4379
+ exact: ["Next", "Review", "Subagents", "Settings", "Back to prompt"],
3969
4380
  },
3970
- choose: "Back",
4381
+ choose: "Back to prompt",
3971
4382
  });
3972
4383
 
3973
4384
  const pp = getCommand(pi, "pp");
@@ -3984,9 +4395,9 @@ describe("menu contracts", () => {
3984
4395
  menu.expect({
3985
4396
  question: m.taskMenu("review", "review"),
3986
4397
  options: {
3987
- exact: ["Next", "Review", "Info", "Settings", "Back"],
4398
+ exact: ["Next", "Review", "Subagents", "Settings", "Back to prompt"],
3988
4399
  },
3989
- choose: "Back",
4400
+ choose: "Back to prompt",
3990
4401
  });
3991
4402
 
3992
4403
  const pp = getCommand(pi, "pp");
@@ -4085,9 +4496,9 @@ describe("menu contracts", () => {
4085
4496
  menu.expect({
4086
4497
  question: m.taskMenu("quick", "quick"),
4087
4498
  options: {
4088
- exact: ["Complete", "Pause", "Info", "Settings", "Back"],
4499
+ exact: ["Complete", "Pause", "Subagents", "Settings", "Back to prompt"],
4089
4500
  },
4090
- choose: "Back",
4501
+ choose: "Back to prompt",
4091
4502
  });
4092
4503
 
4093
4504
  const pp = getCommand(pi, "pp");
@@ -4102,8 +4513,8 @@ describe("menu contracts", () => {
4102
4513
  await orchestrator.startTask(ctx as any, "implement", "contract next");
4103
4514
  menu
4104
4515
  .expect({ question: m.taskMenu("implement", "brainstorm"), options: { include: ["Next"] }, choose: "Next" })
4105
- .expect({ question: "Next", options: { exact: ["Continue to plan & implement", "Complete", "Pause", "Back"] }, choose: "Back" })
4106
- .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" });
4107
4518
 
4108
4519
  const pp = getCommand(pi, "pp");
4109
4520
  await pp(undefined, ctx);
@@ -4115,7 +4526,7 @@ describe("menu contracts", () => {
4115
4526
  menu
4116
4527
  .expect({ question: m.taskMenu("implement", "implement"), options: { include: ["Next"] }, choose: "Next" })
4117
4528
  .expect({ question: "Next", options: { exact: ["Complete", "Pause", "Back"] }, choose: "Back" })
4118
- .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" });
4119
4530
 
4120
4531
  await pp(undefined, ctx);
4121
4532
  });
@@ -4129,8 +4540,8 @@ describe("menu contracts", () => {
4129
4540
 
4130
4541
  menu
4131
4542
  .expect({ question: m.taskMenu("implement", "brainstorm"), options: { include: ["Review"] }, choose: "Review" })
4132
- .expect({ question: "Review", options: { exact: [m.autoReview, "Review on my own", "Back"] }, choose: "Back" })
4133
- .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" });
4134
4545
 
4135
4546
  const pp = getCommand(pi, "pp");
4136
4547
  await pp(undefined, ctx);
@@ -4148,7 +4559,7 @@ describe("menu contracts", () => {
4148
4559
  .expect({ question: "Mode", options: { exact: ["Guided", "Autonomous", "Back"] }, choose: "Back" })
4149
4560
  .expect({ question: "Implement", options: { include: ["Back"] }, choose: "Back" })
4150
4561
  .expect({ question: "Task", options: { include: ["Back"] }, choose: "Back" })
4151
- .expect({ question: "/pp", options: { include: ["Back"] }, choose: "Back" });
4562
+ .expect({ question: "/pp", options: { include: ["Back to prompt"] }, choose: "Back to prompt" });
4152
4563
 
4153
4564
  const pp = getCommand(pi, "pp");
4154
4565
  await pp(undefined, ctx);
@@ -4234,6 +4645,9 @@ describe("full user flows", () => {
4234
4645
  emitSubagentCreated(pi, "flow-auto-reviewer", "Code reviewer (test)");
4235
4646
  emitSubagentCompleted(pi, "flow-auto-reviewer", "Code reviewer (test)");
4236
4647
 
4648
+ // Terminal handoff (#1): autonomous implement does NOT auto-complete. It opens
4649
+ // the GUIDED implement menu and waits; the user drives Next→Complete to finish.
4650
+ expectImplementToDone(menu);
4237
4651
  await ppPhaseComplete.execute("flow-auto-4", { summary: "feedback applied" }, undefined, undefined, ctx);
4238
4652
 
4239
4653
  expect(orchestrator.active).toBeNull();