@ilya-lesikov/pi-pi 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +27 -11
- package/extensions/orchestrator/agents/constraints.test.ts +82 -0
- package/extensions/orchestrator/agents/constraints.ts +66 -2
- 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 +39 -1
- package/extensions/orchestrator/ai-comment-cleanup.test.ts +89 -0
- package/extensions/orchestrator/ai-comment-cleanup.ts +73 -0
- package/extensions/orchestrator/command-handlers.test.ts +47 -0
- package/extensions/orchestrator/command-handlers.ts +44 -28
- package/extensions/orchestrator/config.test.ts +40 -1
- package/extensions/orchestrator/config.ts +18 -2
- package/extensions/orchestrator/context.test.ts +54 -0
- package/extensions/orchestrator/context.ts +81 -2
- package/extensions/orchestrator/custom-footer.test.ts +91 -0
- package/extensions/orchestrator/custom-footer.ts +20 -20
- package/extensions/orchestrator/event-handlers.test.ts +412 -2
- package/extensions/orchestrator/event-handlers.ts +556 -227
- package/extensions/orchestrator/flant-infra.test.ts +25 -0
- package/extensions/orchestrator/flant-infra.ts +91 -0
- package/extensions/orchestrator/index.ts +1 -1
- package/extensions/orchestrator/integration.test.ts +411 -20
- package/extensions/orchestrator/messages.test.ts +30 -0
- package/extensions/orchestrator/messages.ts +6 -0
- package/extensions/orchestrator/model-registry.test.ts +48 -1
- package/extensions/orchestrator/model-registry.ts +43 -1
- package/extensions/orchestrator/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/extensions/orchestrator/orchestrator.test.ts +113 -0
- package/extensions/orchestrator/orchestrator.ts +197 -60
- package/extensions/orchestrator/phases/brainstorm.ts +20 -27
- package/extensions/orchestrator/phases/implementation.test.ts +11 -0
- package/extensions/orchestrator/phases/implementation.ts +4 -6
- package/extensions/orchestrator/phases/machine.test.ts +36 -0
- package/extensions/orchestrator/phases/machine.ts +11 -1
- package/extensions/orchestrator/phases/planning.test.ts +16 -0
- package/extensions/orchestrator/phases/planning.ts +11 -4
- package/extensions/orchestrator/phases/review-task.test.ts +20 -0
- package/extensions/orchestrator/phases/review-task.ts +47 -22
- package/extensions/orchestrator/phases/review.test.ts +34 -0
- package/extensions/orchestrator/phases/review.ts +44 -6
- package/extensions/orchestrator/plannotator.ts +9 -6
- package/extensions/orchestrator/pp-menu.test.ts +207 -1
- package/extensions/orchestrator/pp-menu.ts +514 -402
- 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.test.ts +9 -0
- package/extensions/orchestrator/state.ts +15 -0
- package/extensions/orchestrator/test-helpers.ts +4 -1
- package/extensions/orchestrator/transition-controller.test.ts +100 -0
- package/extensions/orchestrator/transition-controller.ts +61 -3
- package/extensions/orchestrator/usage-tracker.ts +5 -1
- package/extensions/orchestrator/validate-artifacts.test.ts +20 -0
- package/extensions/orchestrator/validate-artifacts.ts +2 -2
- package/package.json +1 -1
- package/AGENTS.md +0 -28
|
@@ -53,6 +53,7 @@ vi.mock("./config.js", async (importOriginal) => {
|
|
|
53
53
|
return { ...original, loadConfig: () => ({
|
|
54
54
|
general: {
|
|
55
55
|
autoCommit: false,
|
|
56
|
+
injectAgentsMd: true,
|
|
56
57
|
loadExtraRepoConfigs: true,
|
|
57
58
|
logLevel: "info",
|
|
58
59
|
},
|
|
@@ -70,6 +71,9 @@ vi.mock("./config.js", async (importOriginal) => {
|
|
|
70
71
|
explore: { model: "test/explore", thinking: "low" },
|
|
71
72
|
librarian: { model: "test/librarian", thinking: "medium" },
|
|
72
73
|
task: { model: "test/task", thinking: "medium" },
|
|
74
|
+
advisor: { model: "test/advisor", thinking: "high" },
|
|
75
|
+
"deep-debugger": { model: "test/deep-debugger", thinking: "high" },
|
|
76
|
+
reviewer: { model: "test/reviewer", thinking: "high" },
|
|
73
77
|
},
|
|
74
78
|
presetGroups: {
|
|
75
79
|
planners: {
|
|
@@ -206,6 +210,7 @@ function makeConfig() {
|
|
|
206
210
|
return {
|
|
207
211
|
general: {
|
|
208
212
|
autoCommit: false,
|
|
213
|
+
injectAgentsMd: true,
|
|
209
214
|
loadExtraRepoConfigs: true,
|
|
210
215
|
logLevel: "info",
|
|
211
216
|
},
|
|
@@ -223,6 +228,9 @@ function makeConfig() {
|
|
|
223
228
|
explore: { model: "test/explore", thinking: "low" },
|
|
224
229
|
librarian: { model: "test/librarian", thinking: "medium" },
|
|
225
230
|
task: { model: "test/task", thinking: "medium" },
|
|
231
|
+
advisor: { model: "test/advisor", thinking: "high" },
|
|
232
|
+
"deep-debugger": { model: "test/deep-debugger", thinking: "high" },
|
|
233
|
+
reviewer: { model: "test/reviewer", thinking: "high" },
|
|
226
234
|
},
|
|
227
235
|
presetGroups: {
|
|
228
236
|
planners: {
|
|
@@ -671,8 +679,12 @@ describe("review cycle lifecycle", () => {
|
|
|
671
679
|
await ppPhaseComplete.execute("call-2", { summary: "plan done" }, undefined, undefined, ctx);
|
|
672
680
|
await new Promise((r) => setTimeout(r, 10));
|
|
673
681
|
|
|
682
|
+
// Pick the auto preset first: with zero enabled reviewers it notifies and
|
|
683
|
+
// (#3d) loops back to the Review submenu — NOT the top-level /pp menu — so the
|
|
684
|
+
// next expectation is the Review submenu directly, without re-selecting "Review".
|
|
674
685
|
expectReviewAuto(menu);
|
|
675
|
-
|
|
686
|
+
menu.expect({ question: "Review", options: { include: ["Review on my own"] }, choose: "Review on my own" });
|
|
687
|
+
menu.expect({ question: "Editor review", options: { include: ["Skip markers"] }, choose: "Skip markers" });
|
|
676
688
|
const result = await ppPhaseComplete.execute("call-3", { summary: "implemented" }, undefined, undefined, ctx);
|
|
677
689
|
|
|
678
690
|
expect(result.content[0].text).toContain("continue");
|
|
@@ -1312,7 +1324,7 @@ describe("edge cases and regressions", () => {
|
|
|
1312
1324
|
|
|
1313
1325
|
await orchestrator.startTask(ctx as any, "implement", "Continue test");
|
|
1314
1326
|
|
|
1315
|
-
expectReviewOnMyOwn(menu);
|
|
1327
|
+
expectReviewOnMyOwn(menu, "Skip markers");
|
|
1316
1328
|
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
1317
1329
|
const result = await ppPhaseComplete.execute("call-1", { summary: "not done yet" }, undefined, undefined, ctx);
|
|
1318
1330
|
|
|
@@ -1355,7 +1367,7 @@ describe("edge cases and regressions", () => {
|
|
|
1355
1367
|
await ppPhaseComplete.execute("call-2", { summary: "plan done" }, undefined, undefined, ctx);
|
|
1356
1368
|
await new Promise((r) => setTimeout(r, 10));
|
|
1357
1369
|
|
|
1358
|
-
expectReviewOnMyOwn(menu);
|
|
1370
|
+
expectReviewOnMyOwn(menu, "Skip markers");
|
|
1359
1371
|
const result = await ppPhaseComplete.execute("call-3", { summary: "partial work" }, undefined, undefined, ctx);
|
|
1360
1372
|
|
|
1361
1373
|
expect(result.content[0].text).toContain("continue");
|
|
@@ -1394,7 +1406,7 @@ describe("edge cases and regressions", () => {
|
|
|
1394
1406
|
"utf-8",
|
|
1395
1407
|
);
|
|
1396
1408
|
|
|
1397
|
-
expectReviewOnMyOwn(menu);
|
|
1409
|
+
expectReviewOnMyOwn(menu, "Skip markers");
|
|
1398
1410
|
const result = await ppPhaseComplete.execute("call-2", { summary: "plan ready" }, undefined, undefined, ctx);
|
|
1399
1411
|
|
|
1400
1412
|
expect(result.content[0].text).toContain("continue");
|
|
@@ -1402,6 +1414,124 @@ describe("edge cases and regressions", () => {
|
|
|
1402
1414
|
expect(orchestrator.active!.state.step).toBe("synthesize");
|
|
1403
1415
|
});
|
|
1404
1416
|
|
|
1417
|
+
it("editor-review Done returns AI_REVIEW marker instructions in implement phase", async () => {
|
|
1418
|
+
const cwd = makeTempDir();
|
|
1419
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
1420
|
+
const ctx = makeCtx();
|
|
1421
|
+
|
|
1422
|
+
await orchestrator.startTask(ctx as any, "implement", "Editor review test");
|
|
1423
|
+
const taskDir = orchestrator.active!.dir;
|
|
1424
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
1425
|
+
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
1426
|
+
expectBrainstormToPlan(menu);
|
|
1427
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
1428
|
+
await ppPhaseComplete.execute("call-1", { summary: "done" }, undefined, undefined, ctx);
|
|
1429
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1430
|
+
|
|
1431
|
+
emitSubagentCreated(pi, "planner-1", "Planner (test)");
|
|
1432
|
+
const plansDir = join(taskDir, "plans");
|
|
1433
|
+
mkdirSync(plansDir, { recursive: true });
|
|
1434
|
+
writeFileSync(
|
|
1435
|
+
join(plansDir, `${Math.floor(Date.now() / 1000)}_test.md`),
|
|
1436
|
+
makeValidPlan(["- [ ] P1. Planner draft item — Done when: planner output exists"]),
|
|
1437
|
+
"utf-8",
|
|
1438
|
+
);
|
|
1439
|
+
emitSubagentCompleted(pi, "planner-1", "Planner (test)");
|
|
1440
|
+
writeFileSync(
|
|
1441
|
+
join(plansDir, `${Math.floor(Date.now() / 1000) + 1}_synthesized.md`),
|
|
1442
|
+
makeValidPlan(["- [ ] P1. Todo item — Done when: item intentionally remains unchecked"]),
|
|
1443
|
+
"utf-8",
|
|
1444
|
+
);
|
|
1445
|
+
expectPlanToImplement(menu);
|
|
1446
|
+
await ppPhaseComplete.execute("call-2", { summary: "plan done" }, undefined, undefined, ctx);
|
|
1447
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1448
|
+
|
|
1449
|
+
expectReviewOnMyOwn(menu, "Done");
|
|
1450
|
+
const result = await ppPhaseComplete.execute("call-3", { summary: "implemented" }, undefined, undefined, ctx);
|
|
1451
|
+
|
|
1452
|
+
expect(result.content[0].text).toContain("AI_REVIEW:");
|
|
1453
|
+
expect(result.content[0].text).toContain("CHANGED files");
|
|
1454
|
+
expect(orchestrator.active!.state.phase).toBe("implement");
|
|
1455
|
+
expect(orchestrator.active!.state.step).toBe("llm_work");
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1458
|
+
it("editor-review Done returns AI_REVIEW state-file instructions in brainstorm phase", async () => {
|
|
1459
|
+
const cwd = makeTempDir();
|
|
1460
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
1461
|
+
const ctx = makeCtx();
|
|
1462
|
+
|
|
1463
|
+
await orchestrator.startTask(ctx as any, "brainstorm", "Brainstorm editor review");
|
|
1464
|
+
|
|
1465
|
+
expectReviewOnMyOwn(menu, "Done");
|
|
1466
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
1467
|
+
const result = await ppPhaseComplete.execute("call-1", { summary: "researched" }, undefined, undefined, ctx);
|
|
1468
|
+
|
|
1469
|
+
expect(result.content[0].text).toContain("AI_REVIEW:");
|
|
1470
|
+
expect(result.content[0].text).toContain("USER_REQUEST.md");
|
|
1471
|
+
expect(result.content[0].text).toContain("RESEARCH.md");
|
|
1472
|
+
expect(result.content[0].text).toContain("artifacts/*.md");
|
|
1473
|
+
expect(result.content[0].text).not.toContain("CHANGED files");
|
|
1474
|
+
expect(orchestrator.active!.state.phase).toBe("brainstorm");
|
|
1475
|
+
expect(orchestrator.active!.state.step).toBe("llm_work");
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
it("editor-review Done returns AI_REVIEW state-file instructions in debug phase", async () => {
|
|
1479
|
+
const cwd = makeTempDir();
|
|
1480
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
1481
|
+
const ctx = makeCtx();
|
|
1482
|
+
|
|
1483
|
+
await orchestrator.startTask(ctx as any, "debug", "Debug editor review");
|
|
1484
|
+
|
|
1485
|
+
expectReviewOnMyOwn(menu, "Done");
|
|
1486
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
1487
|
+
const result = await ppPhaseComplete.execute("call-1", { summary: "diagnosed" }, undefined, undefined, ctx);
|
|
1488
|
+
|
|
1489
|
+
expect(result.content[0].text).toContain("AI_REVIEW:");
|
|
1490
|
+
expect(result.content[0].text).toContain("USER_REQUEST.md");
|
|
1491
|
+
expect(result.content[0].text).not.toContain("CHANGED files");
|
|
1492
|
+
expect(orchestrator.active!.state.phase).toBe("debug");
|
|
1493
|
+
expect(orchestrator.active!.state.step).toBe("llm_work");
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
it("editor-review Done targets synthesized plan in plan phase", async () => {
|
|
1497
|
+
const cwd = makeTempDir();
|
|
1498
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
1499
|
+
const ctx = makeCtx();
|
|
1500
|
+
|
|
1501
|
+
await orchestrator.startTask(ctx as any, "implement", "Plan editor review");
|
|
1502
|
+
const taskDir = orchestrator.active!.dir;
|
|
1503
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
1504
|
+
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
1505
|
+
expectBrainstormToPlan(menu);
|
|
1506
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
1507
|
+
await ppPhaseComplete.execute("call-1", { summary: "done" }, undefined, undefined, ctx);
|
|
1508
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1509
|
+
|
|
1510
|
+
emitSubagentCreated(pi, "planner-1", "Planner (test)");
|
|
1511
|
+
const plansDir = join(taskDir, "plans");
|
|
1512
|
+
mkdirSync(plansDir, { recursive: true });
|
|
1513
|
+
writeFileSync(
|
|
1514
|
+
join(plansDir, `${Math.floor(Date.now() / 1000)}_test.md`),
|
|
1515
|
+
makeValidPlan(["- [ ] P1. Planner draft item — Done when: planner output exists"]),
|
|
1516
|
+
"utf-8",
|
|
1517
|
+
);
|
|
1518
|
+
emitSubagentCompleted(pi, "planner-1", "Planner (test)");
|
|
1519
|
+
writeFileSync(
|
|
1520
|
+
join(plansDir, `${Math.floor(Date.now() / 1000) + 1}_synthesized.md`),
|
|
1521
|
+
makeValidPlan(["- [ ] P1. Todo item — Done when: item intentionally remains unchecked"]),
|
|
1522
|
+
"utf-8",
|
|
1523
|
+
);
|
|
1524
|
+
|
|
1525
|
+
expectReviewOnMyOwn(menu, "Done");
|
|
1526
|
+
const result = await ppPhaseComplete.execute("call-2", { summary: "plan ready" }, undefined, undefined, ctx);
|
|
1527
|
+
|
|
1528
|
+
expect(result.content[0].text).toContain("AI_REVIEW:");
|
|
1529
|
+
expect(result.content[0].text).toContain("_synthesized.md");
|
|
1530
|
+
expect(result.content[0].text).not.toContain("CHANGED files");
|
|
1531
|
+
expect(orchestrator.active!.state.phase).toBe("plan");
|
|
1532
|
+
expect(orchestrator.active!.state.step).toBe("synthesize");
|
|
1533
|
+
});
|
|
1534
|
+
|
|
1405
1535
|
it("generic description task does not auto-trigger agent", async () => {
|
|
1406
1536
|
const cwd = makeTempDir();
|
|
1407
1537
|
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
@@ -1741,8 +1871,11 @@ describe("task modes and quick task", () => {
|
|
|
1741
1871
|
emitSubagentCreated(pi, "reviewer-2", "Code reviewer (test)");
|
|
1742
1872
|
emitSubagentCompleted(pi, "reviewer-2", "Code reviewer (test)");
|
|
1743
1873
|
|
|
1874
|
+
// Terminal handoff (#1): at the cap the autonomous implement no longer auto-
|
|
1875
|
+
// completes — it opens the guided menu; the user drives Next→Complete to finish.
|
|
1876
|
+
expectImplementToDone(menu);
|
|
1744
1877
|
const fourth = await ppPhaseComplete.execute("call-4", { summary: "applied 2" }, undefined, undefined, ctx);
|
|
1745
|
-
expect(fourth.content[0].text).
|
|
1878
|
+
expect(fourth.content[0].text).toMatch(/completed/);
|
|
1746
1879
|
expect(orchestrator.active).toBeNull();
|
|
1747
1880
|
expect(loadTask(taskDir).phase).toBe("done");
|
|
1748
1881
|
});
|
|
@@ -1784,8 +1917,11 @@ describe("task modes and quick task", () => {
|
|
|
1784
1917
|
emitSubagentCreated(pi, "reviewer-1", "Code reviewer (test)");
|
|
1785
1918
|
emitSubagentCompleted(pi, "reviewer-1", "Code reviewer (test)");
|
|
1786
1919
|
|
|
1920
|
+
// Terminal handoff (#1): instead of auto-completing, the guided menu opens
|
|
1921
|
+
// and the user drives Next→Complete to finish.
|
|
1922
|
+
expectImplementToDone(menu);
|
|
1787
1923
|
const second = await ppPhaseComplete.execute("call-2", { summary: "applied" }, undefined, undefined, ctx);
|
|
1788
|
-
expect(second.content[0].text).
|
|
1924
|
+
expect(second.content[0].text).toMatch(/completed/);
|
|
1789
1925
|
expect(orchestrator.active).toBeNull();
|
|
1790
1926
|
const finalState = loadTask(taskDir);
|
|
1791
1927
|
expect(finalState.phase).toBe("done");
|
|
@@ -1865,8 +2001,11 @@ describe("task modes and quick task", () => {
|
|
|
1865
2001
|
);
|
|
1866
2002
|
|
|
1867
2003
|
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
2004
|
+
// Terminal handoff (#1): clean-approved re-entry opens the guided menu rather
|
|
2005
|
+
// than auto-completing; the user drives Next→Complete to finish.
|
|
2006
|
+
expectImplementToDone(menu);
|
|
1868
2007
|
const result = await ppPhaseComplete.execute("call-1", { summary: "checklist repaired" }, undefined, undefined, ctx);
|
|
1869
|
-
expect(result.content[0].text).
|
|
2008
|
+
expect(result.content[0].text).toMatch(/completed/);
|
|
1870
2009
|
expect(orchestrator.active).toBeNull();
|
|
1871
2010
|
expect(loadTask(taskDir).phase).toBe("done");
|
|
1872
2011
|
});
|
|
@@ -2079,7 +2218,7 @@ describe("task modes and quick task", () => {
|
|
|
2079
2218
|
expect(prompt).toContain("<principles>");
|
|
2080
2219
|
expect(prompt).toContain("<tools>");
|
|
2081
2220
|
expect(prompt).toContain("<task>");
|
|
2082
|
-
expect(prompt).toContain("
|
|
2221
|
+
expect(prompt).toContain("call pp_phase_complete");
|
|
2083
2222
|
expect(prompt).not.toContain("HARNESS_BASE_PROMPT");
|
|
2084
2223
|
expect(prompt).toContain(`Working directory: ${cwd}.`);
|
|
2085
2224
|
expect(prompt).toMatch(/Current month: \d{4}-\d{2}\./);
|
|
@@ -2126,6 +2265,66 @@ describe("task modes and quick task", () => {
|
|
|
2126
2265
|
expect(prompt).not.toContain("advance it via the /pp menu");
|
|
2127
2266
|
});
|
|
2128
2267
|
|
|
2268
|
+
it("injects root AGENTS.md into the system prompt when enabled, ignoring nested/extra-repo files", async () => {
|
|
2269
|
+
const cwd = makeTempDir();
|
|
2270
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
2271
|
+
const ctx = makeCtx({ cwd });
|
|
2272
|
+
|
|
2273
|
+
writeFileSync(join(cwd, "AGENTS.md"), "ROOT_AGENTS_CONTENT", "utf-8");
|
|
2274
|
+
mkdirSync(join(cwd, "nested"), { recursive: true });
|
|
2275
|
+
writeFileSync(join(cwd, "nested", "AGENTS.md"), "NESTED_AGENTS_CONTENT", "utf-8");
|
|
2276
|
+
|
|
2277
|
+
await orchestrator.startTask({ ...ctx, cwd } as any, "implement", "implement", undefined, undefined, "guided");
|
|
2278
|
+
orchestrator.active!.state.phase = "plan";
|
|
2279
|
+
orchestrator.active!.state.step = "synthesize";
|
|
2280
|
+
|
|
2281
|
+
const beforeStart = pi._handlers.get("before_agent_start")!;
|
|
2282
|
+
const prompt = (await beforeStart({ systemPrompt: "base" }, ctx))?.systemPrompt ?? "";
|
|
2283
|
+
expect(prompt).toContain(`<agents_md source="${join(cwd, "AGENTS.md")}">`);
|
|
2284
|
+
expect(prompt).toContain("ROOT_AGENTS_CONTENT");
|
|
2285
|
+
expect(prompt).not.toContain("NESTED_AGENTS_CONTENT");
|
|
2286
|
+
});
|
|
2287
|
+
|
|
2288
|
+
it("omits AGENTS.md block when disabled or file absent", async () => {
|
|
2289
|
+
const cwd = makeTempDir();
|
|
2290
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
2291
|
+
const ctx = makeCtx({ cwd });
|
|
2292
|
+
|
|
2293
|
+
await orchestrator.startTask({ ...ctx, cwd } as any, "implement", "implement", undefined, undefined, "guided");
|
|
2294
|
+
orchestrator.active!.state.phase = "plan";
|
|
2295
|
+
orchestrator.active!.state.step = "synthesize";
|
|
2296
|
+
|
|
2297
|
+
const beforeStart = pi._handlers.get("before_agent_start")!;
|
|
2298
|
+
const absent = (await beforeStart({ systemPrompt: "base" }, ctx))?.systemPrompt ?? "";
|
|
2299
|
+
expect(absent).not.toContain("<agents_md");
|
|
2300
|
+
|
|
2301
|
+
writeFileSync(join(cwd, "AGENTS.md"), "ROOT_AGENTS_CONTENT", "utf-8");
|
|
2302
|
+
orchestrator.config.general.injectAgentsMd = false;
|
|
2303
|
+
const disabled = (await beforeStart({ systemPrompt: "base" }, ctx))?.systemPrompt ?? "";
|
|
2304
|
+
expect(disabled).not.toContain("<agents_md");
|
|
2305
|
+
expect(disabled).not.toContain("ROOT_AGENTS_CONTENT");
|
|
2306
|
+
});
|
|
2307
|
+
|
|
2308
|
+
it("auto-refreshes AGENTS.md content on the next turn without restart", async () => {
|
|
2309
|
+
const cwd = makeTempDir();
|
|
2310
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
2311
|
+
const ctx = makeCtx({ cwd });
|
|
2312
|
+
|
|
2313
|
+
writeFileSync(join(cwd, "AGENTS.md"), "FIRST_VERSION", "utf-8");
|
|
2314
|
+
await orchestrator.startTask({ ...ctx, cwd } as any, "implement", "implement", undefined, undefined, "guided");
|
|
2315
|
+
orchestrator.active!.state.phase = "plan";
|
|
2316
|
+
orchestrator.active!.state.step = "synthesize";
|
|
2317
|
+
|
|
2318
|
+
const beforeStart = pi._handlers.get("before_agent_start")!;
|
|
2319
|
+
const first = (await beforeStart({ systemPrompt: "base" }, ctx))?.systemPrompt ?? "";
|
|
2320
|
+
expect(first).toContain("FIRST_VERSION");
|
|
2321
|
+
|
|
2322
|
+
writeFileSync(join(cwd, "AGENTS.md"), "SECOND_VERSION", "utf-8");
|
|
2323
|
+
const second = (await beforeStart({ systemPrompt: "base" }, ctx))?.systemPrompt ?? "";
|
|
2324
|
+
expect(second).toContain("SECOND_VERSION");
|
|
2325
|
+
expect(second).not.toContain("FIRST_VERSION");
|
|
2326
|
+
});
|
|
2327
|
+
|
|
2129
2328
|
it("autonomous plan-phase prompt body contains no /pp guidance", async () => {
|
|
2130
2329
|
const cwd = makeTempDir();
|
|
2131
2330
|
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
@@ -2300,8 +2499,11 @@ describe("task modes and quick task", () => {
|
|
|
2300
2499
|
emitSubagentCreated(pi, "reviewer-auto-1", "Code reviewer (test)");
|
|
2301
2500
|
emitSubagentCompleted(pi, "reviewer-auto-1", "Code reviewer (test)");
|
|
2302
2501
|
|
|
2502
|
+
// Terminal handoff (#1): the clean second pass opens the guided menu; the user
|
|
2503
|
+
// drives Next→Complete to finish rather than auto-completing.
|
|
2504
|
+
expectImplementToDone(menu);
|
|
2303
2505
|
const second = await ppPhaseComplete.execute("call-autonomous-review-2", { summary: "applied" }, undefined, undefined, ctx);
|
|
2304
|
-
expect(second.content[0].text).
|
|
2506
|
+
expect(second.content[0].text).toMatch(/completed/);
|
|
2305
2507
|
expect(orchestrator.active).toBeNull();
|
|
2306
2508
|
});
|
|
2307
2509
|
|
|
@@ -2346,10 +2548,9 @@ describe("task modes and quick task", () => {
|
|
|
2346
2548
|
menu
|
|
2347
2549
|
.expect({ question: "/pp", options: { include: ["Task"] }, choose: "Task" })
|
|
2348
2550
|
.expect({ question: "Task", options: { include: ["Review"] }, choose: "Review" })
|
|
2349
|
-
.expect({ question: "Review", options: {
|
|
2551
|
+
.expect({ question: "Review", options: { exact: ["New", "Resume", "Back"] }, choose: "New" })
|
|
2350
2552
|
.expect({ question: "Mode", options: { include: ["Autonomous"] }, choose: "Autonomous" })
|
|
2351
2553
|
.expect({ question: "Autonomous", options: { include: ["Start"] }, choose: "Start" });
|
|
2352
|
-
ctx.ui.input.mockResolvedValueOnce("Review current branch changes");
|
|
2353
2554
|
const pp = getCommand(pi, "pp");
|
|
2354
2555
|
await pp(undefined, ctx);
|
|
2355
2556
|
|
|
@@ -2419,6 +2620,11 @@ describe("review task lifecycle", () => {
|
|
|
2419
2620
|
await orchestrator.startTask(ctx as any, "review", "Review flow");
|
|
2420
2621
|
const taskDir = orchestrator.active!.dir;
|
|
2421
2622
|
|
|
2623
|
+
// #10: the review phase cannot complete without an ANCHORS-bearing final_pass file.
|
|
2624
|
+
const reviewsDir = join(taskDir, "code-reviews");
|
|
2625
|
+
mkdirSync(reviewsDir, { recursive: true });
|
|
2626
|
+
writeFileSync(join(reviewsDir, "1_final_pass-1.md"), "# Review\nANCHORS: (none)\n", "utf-8");
|
|
2627
|
+
|
|
2422
2628
|
await moveTaskToImplementPhase(pi, orchestrator, ctx, "call-review-to-plan", "call-plan-to-implement");
|
|
2423
2629
|
expect(orchestrator.active!.state.phase).toBe("implement");
|
|
2424
2630
|
|
|
@@ -2510,6 +2716,22 @@ describe("modified file tracking", () => {
|
|
|
2510
2716
|
expect(orchestrator.active!.state.reviewApprovedClean).toBe(false);
|
|
2511
2717
|
});
|
|
2512
2718
|
|
|
2719
|
+
it("source write clears afterImplementRan so post-handoff edits re-run afterImplement (#1)", async () => {
|
|
2720
|
+
const cwd = makeTempDir();
|
|
2721
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
2722
|
+
const ctx = makeCtx();
|
|
2723
|
+
|
|
2724
|
+
await orchestrator.startTask(ctx as any, "implement", "Rerun hooks");
|
|
2725
|
+
await moveTaskToImplementPhase(pi, orchestrator, ctx, "call-rerun-brainstorm", "call-rerun-plan");
|
|
2726
|
+
// Simulate the terminal handoff having already run afterImplement.
|
|
2727
|
+
orchestrator.active!.state.afterImplementRan = true;
|
|
2728
|
+
|
|
2729
|
+
const toolResult = pi._handlers.get("tool_result")!;
|
|
2730
|
+
await toolResult({ toolName: "write", input: { path: "src/d.ts" }, isError: false, content: [] }, {});
|
|
2731
|
+
|
|
2732
|
+
expect(orchestrator.active!.state.afterImplementRan).toBe(false);
|
|
2733
|
+
});
|
|
2734
|
+
|
|
2513
2735
|
it("tool_result ignores writes inside .pp directory", async () => {
|
|
2514
2736
|
const cwd = makeTempDir();
|
|
2515
2737
|
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
@@ -3240,7 +3462,11 @@ describe("tool blocking", () => {
|
|
|
3240
3462
|
});
|
|
3241
3463
|
|
|
3242
3464
|
describe("error retry", () => {
|
|
3243
|
-
|
|
3465
|
+
// Non-SDK-retryable error messages exercise pi-pi's OWN retry path. SDK-
|
|
3466
|
+
// retryable errors (rate limit, overloaded, 5xx, stream-ended, timeout, etc.)
|
|
3467
|
+
// are now deferred to the SDK's own auto-retry (see the defer test below), so
|
|
3468
|
+
// these use a message the SDK does NOT retry.
|
|
3469
|
+
it("turn_end with a non-SDK-retryable error uses pi-pi's idle-gated retry", async () => {
|
|
3244
3470
|
vi.useFakeTimers();
|
|
3245
3471
|
const cwd = makeTempDir();
|
|
3246
3472
|
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
@@ -3249,7 +3475,7 @@ describe("error retry", () => {
|
|
|
3249
3475
|
await orchestrator.startTask(ctx as any, "implement", "retry test");
|
|
3250
3476
|
const turnEnd = pi._handlers.get("turn_end")!;
|
|
3251
3477
|
|
|
3252
|
-
await turnEnd({ message: { stopReason: "error", errorMessage: "
|
|
3478
|
+
await turnEnd({ message: { stopReason: "error", errorMessage: "invalid tool arguments", content: [] } }, ctx);
|
|
3253
3479
|
|
|
3254
3480
|
expect(orchestrator.errorRetryCount).toBe(1);
|
|
3255
3481
|
expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("Retrying in 2s"), "warning");
|
|
@@ -3259,7 +3485,24 @@ describe("error retry", () => {
|
|
|
3259
3485
|
vi.useRealTimers();
|
|
3260
3486
|
});
|
|
3261
3487
|
|
|
3262
|
-
it("turn_end
|
|
3488
|
+
it("turn_end defers SDK-retryable errors to the SDK (no pi-pi retry)", async () => {
|
|
3489
|
+
const cwd = makeTempDir();
|
|
3490
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
3491
|
+
const ctx = makeCtx();
|
|
3492
|
+
|
|
3493
|
+
await orchestrator.startTask(ctx as any, "implement", "sdk defer test");
|
|
3494
|
+
const turnEnd = pi._handlers.get("turn_end")!;
|
|
3495
|
+
|
|
3496
|
+
// "stream ended before message_stop" is SDK-retryable — pi-pi must NOT run
|
|
3497
|
+
// its own retry (which would race the SDK's continue()).
|
|
3498
|
+
await turnEnd({ message: { stopReason: "error", errorMessage: "Anthropic stream ended before message_stop", content: [] } }, ctx);
|
|
3499
|
+
|
|
3500
|
+
expect(orchestrator.errorRetryCount).toBe(0);
|
|
3501
|
+
expect(orchestrator.pendingRetryTimer).toBeNull();
|
|
3502
|
+
expect(ctx.ui.notify).not.toHaveBeenCalledWith(expect.stringContaining("Retrying in"), "warning");
|
|
3503
|
+
});
|
|
3504
|
+
|
|
3505
|
+
it("turn_end stops retrying after max retries (non-SDK-retryable)", async () => {
|
|
3263
3506
|
vi.useFakeTimers();
|
|
3264
3507
|
const cwd = makeTempDir();
|
|
3265
3508
|
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
@@ -3269,27 +3512,54 @@ describe("error retry", () => {
|
|
|
3269
3512
|
const turnEnd = pi._handlers.get("turn_end")!;
|
|
3270
3513
|
|
|
3271
3514
|
for (let i = 0; i < 6; i++) {
|
|
3272
|
-
await turnEnd({ message: { stopReason: "error", errorMessage: "
|
|
3515
|
+
await turnEnd({ message: { stopReason: "error", errorMessage: "invalid tool arguments", content: [] } }, ctx);
|
|
3273
3516
|
}
|
|
3274
3517
|
|
|
3275
|
-
expect(orchestrator.errorRetryCount).toBe(0);
|
|
3276
3518
|
expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringContaining("Stopping auto-retry"), "error");
|
|
3519
|
+
expect(orchestrator.errorNudgeHalted).toBe(true);
|
|
3520
|
+
|
|
3521
|
+
// Once halted, further error turns must NOT arm another retry (this is the
|
|
3522
|
+
// guard against the unbounded-nudge bug).
|
|
3523
|
+
ctx.ui.notify.mockClear();
|
|
3524
|
+
await turnEnd({ message: { stopReason: "error", errorMessage: "invalid tool arguments", content: [] } }, ctx);
|
|
3525
|
+
expect(ctx.ui.notify).not.toHaveBeenCalledWith(expect.stringContaining("Retrying in"), "warning");
|
|
3277
3526
|
vi.useRealTimers();
|
|
3278
3527
|
});
|
|
3279
3528
|
|
|
3280
|
-
it("
|
|
3529
|
+
it("a benign turn does NOT reset the error count (cap must accumulate)", async () => {
|
|
3281
3530
|
const cwd = makeTempDir();
|
|
3282
3531
|
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
3283
3532
|
const ctx = makeCtx();
|
|
3284
3533
|
|
|
3285
|
-
await orchestrator.startTask(ctx as any, "implement", "retry reset test");
|
|
3534
|
+
await orchestrator.startTask(ctx as any, "implement", "retry no-reset test");
|
|
3286
3535
|
const turnEnd = pi._handlers.get("turn_end")!;
|
|
3287
3536
|
|
|
3288
|
-
await turnEnd({ message: { stopReason: "error", errorMessage: "
|
|
3537
|
+
await turnEnd({ message: { stopReason: "error", errorMessage: "invalid tool arguments", content: [] } }, ctx);
|
|
3289
3538
|
expect(orchestrator.errorRetryCount).toBe(1);
|
|
3290
3539
|
|
|
3540
|
+
// A benign (non-error) turn must NOT zero the counter — otherwise a harmless
|
|
3541
|
+
// nudge-induced reply would defeat the cap and allow unbounded error nudges.
|
|
3291
3542
|
await turnEnd({ message: { stopReason: "stop", content: [] } }, ctx);
|
|
3292
|
-
expect(orchestrator.errorRetryCount).toBe(
|
|
3543
|
+
expect(orchestrator.errorRetryCount).toBe(1);
|
|
3544
|
+
});
|
|
3545
|
+
|
|
3546
|
+
it("genuine user re-engagement resets the error count and halt", async () => {
|
|
3547
|
+
const cwd = makeTempDir();
|
|
3548
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
3549
|
+
const ctx = makeCtx();
|
|
3550
|
+
|
|
3551
|
+
await orchestrator.startTask(ctx as any, "implement", "retry reengage test");
|
|
3552
|
+
const turnEnd = pi._handlers.get("turn_end")!;
|
|
3553
|
+
|
|
3554
|
+
await turnEnd({ message: { stopReason: "error", errorMessage: "invalid tool arguments", content: [] } }, ctx);
|
|
3555
|
+
orchestrator.errorNudgeHalted = true;
|
|
3556
|
+
|
|
3557
|
+
const beforeStart = pi._handlers.get("before_agent_start");
|
|
3558
|
+
if (beforeStart) {
|
|
3559
|
+
await beforeStart({ prompt: "user typed something new" }, ctx);
|
|
3560
|
+
expect(orchestrator.errorRetryCount).toBe(0);
|
|
3561
|
+
expect(orchestrator.errorNudgeHalted).toBe(false);
|
|
3562
|
+
}
|
|
3293
3563
|
});
|
|
3294
3564
|
|
|
3295
3565
|
it("empty turn triggers continuation nudge", async () => {
|
|
@@ -3308,6 +3578,76 @@ describe("error retry", () => {
|
|
|
3308
3578
|
expect(pi.sendUserMessage).toHaveBeenCalledWith(expect.stringContaining("Continue the implement phase"), { deliverAs: "followUp" });
|
|
3309
3579
|
});
|
|
3310
3580
|
|
|
3581
|
+
it("drops a continuation nudge whose phase no longer matches at delivery", async () => {
|
|
3582
|
+
const cwd = makeTempDir();
|
|
3583
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
3584
|
+
const ctx = makeCtx();
|
|
3585
|
+
|
|
3586
|
+
await orchestrator.startTask(ctx as any, "implement", "stale phase nudge");
|
|
3587
|
+
orchestrator.active!.state.phase = "plan";
|
|
3588
|
+
orchestrator.active!.state.step = "llm_work";
|
|
3589
|
+
const turnEnd = pi._handlers.get("turn_end")!;
|
|
3590
|
+
await turnEnd({ message: { stopReason: "stop", content: [] }, toolResults: [] }, ctx);
|
|
3591
|
+
const nudge = (pi.sendUserMessage as any).mock.calls.map((c: any[]) => c[0]).find((t: string) => t.includes("Continue the plan phase"));
|
|
3592
|
+
expect(nudge).toBeDefined();
|
|
3593
|
+
expect(orchestrator.pendingNudges.has(nudge)).toBe(true);
|
|
3594
|
+
|
|
3595
|
+
// Phase advances before the queued nudge is delivered.
|
|
3596
|
+
orchestrator.active!.state.phase = "implement";
|
|
3597
|
+
const beforeStart = pi._handlers.get("before_agent_start")!;
|
|
3598
|
+
const abort = vi.fn();
|
|
3599
|
+
await beforeStart({ systemPrompt: "base", prompt: nudge }, makeCtx({ abort }));
|
|
3600
|
+
|
|
3601
|
+
expect(abort).toHaveBeenCalledOnce();
|
|
3602
|
+
expect(orchestrator.pendingNudges.has(nudge)).toBe(false);
|
|
3603
|
+
});
|
|
3604
|
+
|
|
3605
|
+
it("drops a continuation nudge whose task token changed at delivery (same phase)", async () => {
|
|
3606
|
+
const cwd = makeTempDir();
|
|
3607
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
3608
|
+
const ctx = makeCtx();
|
|
3609
|
+
|
|
3610
|
+
await orchestrator.startTask(ctx as any, "implement", "stale token nudge");
|
|
3611
|
+
orchestrator.active!.state.phase = "plan";
|
|
3612
|
+
orchestrator.active!.state.step = "llm_work";
|
|
3613
|
+
const turnEnd = pi._handlers.get("turn_end")!;
|
|
3614
|
+
await turnEnd({ message: { stopReason: "stop", content: [] }, toolResults: [] }, ctx);
|
|
3615
|
+
const nudge = (pi.sendUserMessage as any).mock.calls.map((c: any[]) => c[0]).find((t: string) => t.includes("Continue the plan phase"));
|
|
3616
|
+
expect(nudge).toBeDefined();
|
|
3617
|
+
|
|
3618
|
+
// A new task starts (token bumped), also at the plan phase — the old nudge
|
|
3619
|
+
// must not drive a turn in the new task even though the phase name matches.
|
|
3620
|
+
orchestrator.activeTaskToken++;
|
|
3621
|
+
orchestrator.active!.state.phase = "plan";
|
|
3622
|
+
const beforeStart = pi._handlers.get("before_agent_start")!;
|
|
3623
|
+
const abort = vi.fn();
|
|
3624
|
+
await beforeStart({ systemPrompt: "base", prompt: nudge }, makeCtx({ abort }));
|
|
3625
|
+
|
|
3626
|
+
expect(abort).toHaveBeenCalledOnce();
|
|
3627
|
+
});
|
|
3628
|
+
|
|
3629
|
+
it("delivers a continuation nudge whose phase and token still match", async () => {
|
|
3630
|
+
const cwd = makeTempDir();
|
|
3631
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
3632
|
+
const ctx = makeCtx();
|
|
3633
|
+
|
|
3634
|
+
await orchestrator.startTask(ctx as any, "implement", "fresh nudge");
|
|
3635
|
+
orchestrator.active!.state.phase = "implement";
|
|
3636
|
+
orchestrator.active!.state.step = "llm_work";
|
|
3637
|
+
const turnEnd = pi._handlers.get("turn_end")!;
|
|
3638
|
+
await turnEnd({ message: { stopReason: "stop", content: [] }, toolResults: [] }, ctx);
|
|
3639
|
+
const nudge = (pi.sendUserMessage as any).mock.calls.map((c: any[]) => c[0]).find((t: string) => t.includes("Continue the implement phase"));
|
|
3640
|
+
expect(nudge).toBeDefined();
|
|
3641
|
+
|
|
3642
|
+
const beforeStart = pi._handlers.get("before_agent_start")!;
|
|
3643
|
+
const abort = vi.fn();
|
|
3644
|
+
const result = await beforeStart({ systemPrompt: "base", prompt: nudge }, makeCtx({ abort }));
|
|
3645
|
+
|
|
3646
|
+
expect(abort).not.toHaveBeenCalled();
|
|
3647
|
+
expect(result?.systemPrompt).toContain("<task>");
|
|
3648
|
+
expect(orchestrator.pendingNudges.has(nudge)).toBe(false);
|
|
3649
|
+
});
|
|
3650
|
+
|
|
3311
3651
|
it("suppresses nudges while the controller is transitioning (not running)", async () => {
|
|
3312
3652
|
const cwd = makeTempDir();
|
|
3313
3653
|
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
@@ -3539,6 +3879,54 @@ describe("compaction", () => {
|
|
|
3539
3879
|
expect(result.compaction.summary).toBe("Task done summary");
|
|
3540
3880
|
});
|
|
3541
3881
|
|
|
3882
|
+
it("task-boundary discard uses distinct discard wording and keeps no verbatim transcript", async () => {
|
|
3883
|
+
const cwd = makeTempDir();
|
|
3884
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
3885
|
+
orchestrator.lastCtx = makeCtx({ isIdle: vi.fn().mockReturnValue(false) });
|
|
3886
|
+
|
|
3887
|
+
void orchestrator.transitionController.requestTransition({
|
|
3888
|
+
kind: "done",
|
|
3889
|
+
discard: true,
|
|
3890
|
+
summary: "Task \"x\" (implement) is finished — DISCARD its entire conversation. Do NOT carry forward.",
|
|
3891
|
+
});
|
|
3892
|
+
orchestrator.transitionController.onAgentEnd();
|
|
3893
|
+
const beforeCompact = pi._handlers.get("session_before_compact")!;
|
|
3894
|
+
const result = await beforeCompact(
|
|
3895
|
+
{
|
|
3896
|
+
preparation: { firstKeptEntryId: "old-kept", tokensBefore: 999 },
|
|
3897
|
+
branchEntries: [{ id: "e-a" }, { id: "e-b" }, { id: "newest" }],
|
|
3898
|
+
},
|
|
3899
|
+
{},
|
|
3900
|
+
);
|
|
3901
|
+
|
|
3902
|
+
// Distinct from the phase-transition summary, and instructs discard.
|
|
3903
|
+
expect(result.compaction.summary).not.toBe("Phase transition in progress.");
|
|
3904
|
+
expect(result.compaction.summary).toContain("DISCARD");
|
|
3905
|
+
// Verbatim window collapsed to the newest entry — nothing prior kept.
|
|
3906
|
+
expect(result.compaction.firstKeptEntryId).toBe("newest");
|
|
3907
|
+
});
|
|
3908
|
+
|
|
3909
|
+
it("phase transition keeps the default recent verbatim window (no discard)", async () => {
|
|
3910
|
+
const cwd = makeTempDir();
|
|
3911
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
3912
|
+
orchestrator.lastCtx = makeCtx({ isIdle: vi.fn().mockReturnValue(false) });
|
|
3913
|
+
|
|
3914
|
+
void orchestrator.transitionController.requestTransition({ kind: "phase", summary: "Phase summary text" });
|
|
3915
|
+
orchestrator.transitionController.onAgentEnd();
|
|
3916
|
+
const beforeCompact = pi._handlers.get("session_before_compact")!;
|
|
3917
|
+
const result = await beforeCompact(
|
|
3918
|
+
{
|
|
3919
|
+
preparation: { firstKeptEntryId: "old-kept", tokensBefore: 111 },
|
|
3920
|
+
branchEntries: [{ id: "e-a" }, { id: "newest" }],
|
|
3921
|
+
},
|
|
3922
|
+
{},
|
|
3923
|
+
);
|
|
3924
|
+
|
|
3925
|
+
expect(result.compaction.summary).toBe("Phase summary text");
|
|
3926
|
+
expect(result.compaction.firstKeptEntryId).toBe("old-kept");
|
|
3927
|
+
});
|
|
3928
|
+
|
|
3929
|
+
|
|
3542
3930
|
it("session_before_compact re-injects artifacts after natural compaction", async () => {
|
|
3543
3931
|
const cwd = makeTempDir();
|
|
3544
3932
|
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
@@ -4139,6 +4527,9 @@ describe("full user flows", () => {
|
|
|
4139
4527
|
emitSubagentCreated(pi, "flow-auto-reviewer", "Code reviewer (test)");
|
|
4140
4528
|
emitSubagentCompleted(pi, "flow-auto-reviewer", "Code reviewer (test)");
|
|
4141
4529
|
|
|
4530
|
+
// Terminal handoff (#1): autonomous implement does NOT auto-complete. It opens
|
|
4531
|
+
// the GUIDED implement menu and waits; the user drives Next→Complete to finish.
|
|
4532
|
+
expectImplementToDone(menu);
|
|
4142
4533
|
await ppPhaseComplete.execute("flow-auto-4", { summary: "feedback applied" }, undefined, undefined, ctx);
|
|
4143
4534
|
|
|
4144
4535
|
expect(orchestrator.active).toBeNull();
|
|
@@ -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
|
+
}
|