@ilya-lesikov/pi-pi 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/3p/pi-ask-user/index.ts +71 -124
- package/3p/pi-ask-user/single-select-layout.ts +3 -14
- package/3p/pi-subagents/package.json +13 -7
- package/3p/pi-subagents/src/agent-manager.ts +243 -79
- package/3p/pi-subagents/src/agent-runner.ts +501 -365
- package/3p/pi-subagents/src/agent-types.ts +46 -57
- package/3p/pi-subagents/src/cross-extension-rpc.ts +52 -46
- package/3p/pi-subagents/src/custom-agents.ts +30 -6
- package/3p/pi-subagents/src/default-agents.ts +25 -43
- package/3p/pi-subagents/src/enabled-models.ts +180 -0
- package/3p/pi-subagents/src/index.ts +599 -839
- package/3p/pi-subagents/src/model-resolver.ts +26 -7
- package/3p/pi-subagents/src/output-file.ts +21 -2
- package/3p/pi-subagents/src/prompts.ts +17 -3
- package/3p/pi-subagents/src/schedule-store.ts +153 -0
- package/3p/pi-subagents/src/schedule.ts +365 -0
- package/3p/pi-subagents/src/settings.ts +261 -0
- package/3p/pi-subagents/src/skill-loader.ts +77 -54
- package/3p/pi-subagents/src/status-note.ts +25 -0
- package/3p/pi-subagents/src/types.ts +97 -6
- package/3p/pi-subagents/src/ui/agent-widget.ts +94 -21
- package/3p/pi-subagents/src/ui/conversation-viewer.ts +147 -35
- package/3p/pi-subagents/src/ui/schedule-menu.ts +104 -0
- package/3p/pi-subagents/src/ui/viewer-keys.ts +39 -0
- package/3p/pi-subagents/src/usage.ts +60 -0
- package/3p/pi-subagents/src/worktree.ts +49 -20
- package/extensions/orchestrator/agents/advisor.ts +13 -8
- package/extensions/orchestrator/agents/brainstorm-reviewer.ts +8 -3
- package/extensions/orchestrator/agents/code-reviewer.ts +29 -8
- package/extensions/orchestrator/agents/constraints.test.ts +65 -1
- package/extensions/orchestrator/agents/constraints.ts +73 -2
- package/extensions/orchestrator/agents/deep-debugger.ts +13 -8
- package/extensions/orchestrator/agents/explore.ts +12 -6
- package/extensions/orchestrator/agents/librarian.ts +13 -5
- package/extensions/orchestrator/agents/plan-reviewer.ts +8 -3
- package/extensions/orchestrator/agents/planner.ts +8 -3
- package/extensions/orchestrator/agents/pools.test.ts +56 -0
- package/extensions/orchestrator/agents/prompts.test.ts +52 -10
- package/extensions/orchestrator/agents/registry.test.ts +245 -0
- package/extensions/orchestrator/agents/registry.ts +145 -10
- package/extensions/orchestrator/agents/reviewer.ts +14 -8
- package/extensions/orchestrator/agents/task.ts +12 -6
- package/extensions/orchestrator/agents/tool-routing.ts +213 -69
- package/extensions/orchestrator/agents/wait-for-completion.test.ts +171 -0
- package/extensions/orchestrator/ai-comment-cleanup.test.ts +89 -0
- package/extensions/orchestrator/ai-comment-cleanup.ts +73 -0
- package/extensions/orchestrator/ast-search.test.ts +124 -0
- package/extensions/orchestrator/billing-spoof.test.ts +67 -0
- package/extensions/orchestrator/billing-spoof.ts +95 -0
- package/extensions/orchestrator/cbm.more.test.ts +358 -0
- package/extensions/orchestrator/command-handlers.test.ts +47 -0
- package/extensions/orchestrator/command-handlers.ts +49 -27
- package/extensions/orchestrator/commands.test.ts +15 -2
- package/extensions/orchestrator/commands.ts +1 -1
- package/extensions/orchestrator/config.test.ts +129 -2
- package/extensions/orchestrator/config.ts +115 -19
- package/extensions/orchestrator/context.test.ts +46 -0
- package/extensions/orchestrator/context.ts +34 -5
- package/extensions/orchestrator/custom-footer.test.ts +105 -0
- package/extensions/orchestrator/custom-footer.ts +22 -20
- package/extensions/orchestrator/doctor.more.test.ts +315 -0
- package/extensions/orchestrator/doctor.ts +6 -2
- package/extensions/orchestrator/event-handlers.more.test.ts +561 -0
- package/extensions/orchestrator/event-handlers.test.ts +411 -10
- package/extensions/orchestrator/event-handlers.ts +738 -349
- package/extensions/orchestrator/exa.more.test.ts +118 -0
- package/extensions/orchestrator/flant-infra.more.test.ts +326 -0
- package/extensions/orchestrator/flant-infra.test.ts +127 -0
- package/extensions/orchestrator/flant-infra.ts +113 -39
- package/extensions/orchestrator/index.test.ts +76 -0
- package/extensions/orchestrator/index.ts +2 -0
- package/extensions/orchestrator/integration.test.ts +488 -74
- package/extensions/orchestrator/model-registry.test.ts +2 -1
- package/extensions/orchestrator/model-registry.ts +12 -2
- package/extensions/orchestrator/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/extensions/orchestrator/orchestrator.test.ts +67 -0
- package/extensions/orchestrator/orchestrator.ts +165 -85
- package/extensions/orchestrator/phases/brainstorm.test.ts +30 -1
- package/extensions/orchestrator/phases/brainstorm.ts +53 -12
- package/extensions/orchestrator/phases/implementation.ts +2 -0
- package/extensions/orchestrator/phases/machine.test.ts +53 -1
- package/extensions/orchestrator/phases/machine.ts +15 -2
- package/extensions/orchestrator/phases/planning.test.ts +47 -1
- package/extensions/orchestrator/phases/planning.ts +18 -3
- package/extensions/orchestrator/phases/review-task.test.ts +20 -0
- package/extensions/orchestrator/phases/review-task.ts +47 -14
- package/extensions/orchestrator/phases/review.test.ts +36 -0
- package/extensions/orchestrator/phases/review.ts +62 -10
- package/extensions/orchestrator/phases/spawn-blocking.test.ts +1 -0
- package/extensions/orchestrator/phases/verdict.ts +6 -5
- package/extensions/orchestrator/plannotator-open-failure.test.ts +67 -0
- package/extensions/orchestrator/plannotator.test.ts +38 -1
- package/extensions/orchestrator/plannotator.ts +50 -3
- package/extensions/orchestrator/pp-menu.leaves.test.ts +590 -0
- package/extensions/orchestrator/pp-menu.more.test.ts +524 -0
- package/extensions/orchestrator/pp-menu.test.ts +314 -1
- package/extensions/orchestrator/pp-menu.ts +937 -450
- package/extensions/orchestrator/pp-state-tools.test.ts +80 -0
- package/extensions/orchestrator/pp-state-tools.ts +65 -0
- package/extensions/orchestrator/rate-limit-fallback.more.test.ts +241 -0
- package/extensions/orchestrator/rate-limit-fallback.test.ts +20 -1
- package/extensions/orchestrator/rate-limit-fallback.ts +12 -0
- package/extensions/orchestrator/review-files.test.ts +26 -0
- package/extensions/orchestrator/review-files.ts +3 -0
- package/extensions/orchestrator/state.test.ts +82 -1
- package/extensions/orchestrator/state.ts +110 -23
- package/extensions/orchestrator/suppress-pierre-theme-spam.test.ts +56 -0
- package/extensions/orchestrator/suppress-pierre-theme-spam.ts +45 -0
- package/extensions/orchestrator/transition-controller.test.ts +100 -0
- package/extensions/orchestrator/transition-controller.ts +61 -3
- package/extensions/orchestrator/validate-artifacts.ts +1 -1
- package/package.json +6 -2
- package/scripts/test-3p.sh +52 -0
- package/AGENTS.md +0 -28
|
@@ -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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
vi.mock("./agents/registry.js", async (importOriginal) => {
|
|
44
|
+
const actual = await importOriginal<typeof import("./agents/registry.js")>();
|
|
45
|
+
return {
|
|
46
|
+
...actual,
|
|
47
|
+
registerAgentDefinitions: vi.fn(),
|
|
48
|
+
unregisterAgentDefinitions: vi.fn(),
|
|
49
|
+
setExtensionOnlyMode: vi.fn(),
|
|
50
|
+
getAgentConfigSnapshot: vi.fn(() => undefined),
|
|
51
|
+
spawnViaRpc: vi.fn(async (_pi: any, _type: string) => ({ id: `mock-${Math.random().toString(36).slice(2)}` })),
|
|
52
|
+
waitForCompletion: vi.fn(async () => undefined),
|
|
53
|
+
};
|
|
54
|
+
});
|
|
50
55
|
|
|
51
56
|
vi.mock("./config.js", async (importOriginal) => {
|
|
52
57
|
const original = await importOriginal<typeof import("./config.js")>();
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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).
|
|
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).
|
|
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).
|
|
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("<
|
|
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("<
|
|
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).
|
|
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: {
|
|
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
|
-
//
|
|
3291
|
-
//
|
|
3292
|
-
//
|
|
3293
|
-
//
|
|
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"), "
|
|
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("
|
|
3592
|
+
expect(pi.sendUserMessage).toHaveBeenCalledWith(expect.stringContaining("Transient API error"), { deliverAs: "followUp" });
|
|
3310
3593
|
vi.useRealTimers();
|
|
3311
3594
|
});
|
|
3312
3595
|
|
|
3313
|
-
it("turn_end
|
|
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
|
|
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
|
|
3322
|
-
//
|
|
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(
|
|
3326
|
-
expect(
|
|
3327
|
-
|
|
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
|
-
|
|
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
|
|
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: ["
|
|
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: ["
|
|
3810
|
-
exclude: ["LSP"],
|
|
4217
|
+
include: ["Usage", "Doctor", "Back"],
|
|
4218
|
+
exclude: ["LSP", "Subagents"],
|
|
3811
4219
|
},
|
|
3812
4220
|
choose: "Back",
|
|
3813
4221
|
})
|
|
3814
|
-
.expect({ question: "
|
|
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: ["
|
|
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: "
|
|
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", "
|
|
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", "
|
|
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", "
|
|
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", "
|
|
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", "
|
|
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", "
|
|
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();
|