@ilya-lesikov/pi-pi 0.8.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/extensions/orchestrator/agents/code-reviewer.ts +21 -5
- package/extensions/orchestrator/agents/constraints.test.ts +39 -1
- package/extensions/orchestrator/agents/constraints.ts +63 -2
- package/extensions/orchestrator/agents/tool-routing.ts +30 -14
- 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 +43 -27
- package/extensions/orchestrator/config.test.ts +40 -1
- package/extensions/orchestrator/config.ts +13 -0
- package/extensions/orchestrator/context.ts +16 -0
- package/extensions/orchestrator/custom-footer.test.ts +91 -0
- package/extensions/orchestrator/custom-footer.ts +20 -20
- package/extensions/orchestrator/event-handlers.test.ts +315 -1
- package/extensions/orchestrator/event-handlers.ts +397 -201
- package/extensions/orchestrator/integration.test.ts +305 -9
- package/extensions/orchestrator/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/extensions/orchestrator/orchestrator.ts +46 -58
- package/extensions/orchestrator/phases/brainstorm.ts +11 -7
- package/extensions/orchestrator/phases/machine.test.ts +36 -0
- package/extensions/orchestrator/phases/machine.ts +11 -1
- package/extensions/orchestrator/phases/review-task.test.ts +20 -0
- package/extensions/orchestrator/phases/review-task.ts +44 -16
- package/extensions/orchestrator/phases/review.test.ts +26 -0
- package/extensions/orchestrator/phases/review.ts +40 -3
- package/extensions/orchestrator/pp-menu.test.ts +207 -1
- package/extensions/orchestrator/pp-menu.ts +376 -378
- package/extensions/orchestrator/state.test.ts +9 -0
- package/extensions/orchestrator/state.ts +15 -0
- package/extensions/orchestrator/transition-controller.test.ts +100 -0
- package/extensions/orchestrator/transition-controller.ts +61 -3
- 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
|
},
|
|
@@ -209,6 +210,7 @@ function makeConfig() {
|
|
|
209
210
|
return {
|
|
210
211
|
general: {
|
|
211
212
|
autoCommit: false,
|
|
213
|
+
injectAgentsMd: true,
|
|
212
214
|
loadExtraRepoConfigs: true,
|
|
213
215
|
logLevel: "info",
|
|
214
216
|
},
|
|
@@ -677,8 +679,12 @@ describe("review cycle lifecycle", () => {
|
|
|
677
679
|
await ppPhaseComplete.execute("call-2", { summary: "plan done" }, undefined, undefined, ctx);
|
|
678
680
|
await new Promise((r) => setTimeout(r, 10));
|
|
679
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".
|
|
680
685
|
expectReviewAuto(menu);
|
|
681
|
-
|
|
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" });
|
|
682
688
|
const result = await ppPhaseComplete.execute("call-3", { summary: "implemented" }, undefined, undefined, ctx);
|
|
683
689
|
|
|
684
690
|
expect(result.content[0].text).toContain("continue");
|
|
@@ -1318,7 +1324,7 @@ describe("edge cases and regressions", () => {
|
|
|
1318
1324
|
|
|
1319
1325
|
await orchestrator.startTask(ctx as any, "implement", "Continue test");
|
|
1320
1326
|
|
|
1321
|
-
expectReviewOnMyOwn(menu);
|
|
1327
|
+
expectReviewOnMyOwn(menu, "Skip markers");
|
|
1322
1328
|
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
1323
1329
|
const result = await ppPhaseComplete.execute("call-1", { summary: "not done yet" }, undefined, undefined, ctx);
|
|
1324
1330
|
|
|
@@ -1400,7 +1406,7 @@ describe("edge cases and regressions", () => {
|
|
|
1400
1406
|
"utf-8",
|
|
1401
1407
|
);
|
|
1402
1408
|
|
|
1403
|
-
expectReviewOnMyOwn(menu);
|
|
1409
|
+
expectReviewOnMyOwn(menu, "Skip markers");
|
|
1404
1410
|
const result = await ppPhaseComplete.execute("call-2", { summary: "plan ready" }, undefined, undefined, ctx);
|
|
1405
1411
|
|
|
1406
1412
|
expect(result.content[0].text).toContain("continue");
|
|
@@ -1449,6 +1455,83 @@ describe("edge cases and regressions", () => {
|
|
|
1449
1455
|
expect(orchestrator.active!.state.step).toBe("llm_work");
|
|
1450
1456
|
});
|
|
1451
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
|
+
|
|
1452
1535
|
it("generic description task does not auto-trigger agent", async () => {
|
|
1453
1536
|
const cwd = makeTempDir();
|
|
1454
1537
|
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
@@ -1788,8 +1871,11 @@ describe("task modes and quick task", () => {
|
|
|
1788
1871
|
emitSubagentCreated(pi, "reviewer-2", "Code reviewer (test)");
|
|
1789
1872
|
emitSubagentCompleted(pi, "reviewer-2", "Code reviewer (test)");
|
|
1790
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);
|
|
1791
1877
|
const fourth = await ppPhaseComplete.execute("call-4", { summary: "applied 2" }, undefined, undefined, ctx);
|
|
1792
|
-
expect(fourth.content[0].text).
|
|
1878
|
+
expect(fourth.content[0].text).toMatch(/completed/);
|
|
1793
1879
|
expect(orchestrator.active).toBeNull();
|
|
1794
1880
|
expect(loadTask(taskDir).phase).toBe("done");
|
|
1795
1881
|
});
|
|
@@ -1831,8 +1917,11 @@ describe("task modes and quick task", () => {
|
|
|
1831
1917
|
emitSubagentCreated(pi, "reviewer-1", "Code reviewer (test)");
|
|
1832
1918
|
emitSubagentCompleted(pi, "reviewer-1", "Code reviewer (test)");
|
|
1833
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);
|
|
1834
1923
|
const second = await ppPhaseComplete.execute("call-2", { summary: "applied" }, undefined, undefined, ctx);
|
|
1835
|
-
expect(second.content[0].text).
|
|
1924
|
+
expect(second.content[0].text).toMatch(/completed/);
|
|
1836
1925
|
expect(orchestrator.active).toBeNull();
|
|
1837
1926
|
const finalState = loadTask(taskDir);
|
|
1838
1927
|
expect(finalState.phase).toBe("done");
|
|
@@ -1912,8 +2001,11 @@ describe("task modes and quick task", () => {
|
|
|
1912
2001
|
);
|
|
1913
2002
|
|
|
1914
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);
|
|
1915
2007
|
const result = await ppPhaseComplete.execute("call-1", { summary: "checklist repaired" }, undefined, undefined, ctx);
|
|
1916
|
-
expect(result.content[0].text).
|
|
2008
|
+
expect(result.content[0].text).toMatch(/completed/);
|
|
1917
2009
|
expect(orchestrator.active).toBeNull();
|
|
1918
2010
|
expect(loadTask(taskDir).phase).toBe("done");
|
|
1919
2011
|
});
|
|
@@ -2173,6 +2265,66 @@ describe("task modes and quick task", () => {
|
|
|
2173
2265
|
expect(prompt).not.toContain("advance it via the /pp menu");
|
|
2174
2266
|
});
|
|
2175
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
|
+
|
|
2176
2328
|
it("autonomous plan-phase prompt body contains no /pp guidance", async () => {
|
|
2177
2329
|
const cwd = makeTempDir();
|
|
2178
2330
|
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
@@ -2347,8 +2499,11 @@ describe("task modes and quick task", () => {
|
|
|
2347
2499
|
emitSubagentCreated(pi, "reviewer-auto-1", "Code reviewer (test)");
|
|
2348
2500
|
emitSubagentCompleted(pi, "reviewer-auto-1", "Code reviewer (test)");
|
|
2349
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);
|
|
2350
2505
|
const second = await ppPhaseComplete.execute("call-autonomous-review-2", { summary: "applied" }, undefined, undefined, ctx);
|
|
2351
|
-
expect(second.content[0].text).
|
|
2506
|
+
expect(second.content[0].text).toMatch(/completed/);
|
|
2352
2507
|
expect(orchestrator.active).toBeNull();
|
|
2353
2508
|
});
|
|
2354
2509
|
|
|
@@ -2393,10 +2548,9 @@ describe("task modes and quick task", () => {
|
|
|
2393
2548
|
menu
|
|
2394
2549
|
.expect({ question: "/pp", options: { include: ["Task"] }, choose: "Task" })
|
|
2395
2550
|
.expect({ question: "Task", options: { include: ["Review"] }, choose: "Review" })
|
|
2396
|
-
.expect({ question: "Review", options: {
|
|
2551
|
+
.expect({ question: "Review", options: { exact: ["New", "Resume", "Back"] }, choose: "New" })
|
|
2397
2552
|
.expect({ question: "Mode", options: { include: ["Autonomous"] }, choose: "Autonomous" })
|
|
2398
2553
|
.expect({ question: "Autonomous", options: { include: ["Start"] }, choose: "Start" });
|
|
2399
|
-
ctx.ui.input.mockResolvedValueOnce("Review current branch changes");
|
|
2400
2554
|
const pp = getCommand(pi, "pp");
|
|
2401
2555
|
await pp(undefined, ctx);
|
|
2402
2556
|
|
|
@@ -2466,6 +2620,11 @@ describe("review task lifecycle", () => {
|
|
|
2466
2620
|
await orchestrator.startTask(ctx as any, "review", "Review flow");
|
|
2467
2621
|
const taskDir = orchestrator.active!.dir;
|
|
2468
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
|
+
|
|
2469
2628
|
await moveTaskToImplementPhase(pi, orchestrator, ctx, "call-review-to-plan", "call-plan-to-implement");
|
|
2470
2629
|
expect(orchestrator.active!.state.phase).toBe("implement");
|
|
2471
2630
|
|
|
@@ -2557,6 +2716,22 @@ describe("modified file tracking", () => {
|
|
|
2557
2716
|
expect(orchestrator.active!.state.reviewApprovedClean).toBe(false);
|
|
2558
2717
|
});
|
|
2559
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
|
+
|
|
2560
2735
|
it("tool_result ignores writes inside .pp directory", async () => {
|
|
2561
2736
|
const cwd = makeTempDir();
|
|
2562
2737
|
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
@@ -3403,6 +3578,76 @@ describe("error retry", () => {
|
|
|
3403
3578
|
expect(pi.sendUserMessage).toHaveBeenCalledWith(expect.stringContaining("Continue the implement phase"), { deliverAs: "followUp" });
|
|
3404
3579
|
});
|
|
3405
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
|
+
|
|
3406
3651
|
it("suppresses nudges while the controller is transitioning (not running)", async () => {
|
|
3407
3652
|
const cwd = makeTempDir();
|
|
3408
3653
|
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
@@ -3634,6 +3879,54 @@ describe("compaction", () => {
|
|
|
3634
3879
|
expect(result.compaction.summary).toBe("Task done summary");
|
|
3635
3880
|
});
|
|
3636
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
|
+
|
|
3637
3930
|
it("session_before_compact re-injects artifacts after natural compaction", async () => {
|
|
3638
3931
|
const cwd = makeTempDir();
|
|
3639
3932
|
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
@@ -4234,6 +4527,9 @@ describe("full user flows", () => {
|
|
|
4234
4527
|
emitSubagentCreated(pi, "flow-auto-reviewer", "Code reviewer (test)");
|
|
4235
4528
|
emitSubagentCompleted(pi, "flow-auto-reviewer", "Code reviewer (test)");
|
|
4236
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);
|
|
4237
4533
|
await ppPhaseComplete.execute("flow-auto-4", { summary: "feedback applied" }, undefined, undefined, ctx);
|
|
4238
4534
|
|
|
4239
4535
|
expect(orchestrator.active).toBeNull();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":"4.1.4","results":[[":pp-menu.test.ts",{"duration":5.372961000000032,"failed":false}],[":integration.test.ts",{"duration":943.253143,"failed":false}],[":phases/review.test.ts",{"duration":2.5133239999999546,"failed":false}],[":pr-comments.test.ts",{"duration":4.420278999999994,"failed":false}],[":transition-controller.test.ts",{"duration":5.222491000000005,"failed":false}],[":event-handlers.test.ts",{"duration":10.928670000000011,"failed":false}],[":ai-comment-cleanup.test.ts",{"duration":3.3210659999999947,"failed":false}],[":flant-infra.test.ts",{"duration":40.368476,"failed":false}],[":command-handlers.test.ts",{"duration":13.727003000000025,"failed":false}],[":usage-tracker.test.ts",{"duration":7.265218000000004,"failed":false}],[":doctor.test.ts",{"duration":10.992058,"failed":false}],[":context.test.ts",{"duration":8.419984999999997,"failed":false}],[":orchestrator.test.ts",{"duration":12.065533000000016,"failed":false}],[":config.test.ts",{"duration":8.647639000000026,"failed":false}],[":model-registry.test.ts",{"duration":9.934792000000002,"failed":false}],[":state.test.ts",{"duration":13.755476000000002,"failed":false}],[":commands.test.ts",{"duration":270.744763,"failed":false}],[":phases/machine.test.ts",{"duration":8.854923999999983,"failed":false}],[":pp-state-tools.test.ts",{"duration":7.878408999999991,"failed":false}],[":log.test.ts",{"duration":884.438896,"failed":false}],[":repo-utils.test.ts",{"duration":3.7061610000000016,"failed":false}],[":phases/verdict.test.ts",{"duration":4.656940000000006,"failed":false}],[":rate-limit-fallback-flow.test.ts",{"duration":6.88740700000001,"failed":false}],[":agents/prompts.test.ts",{"duration":4.860277999999994,"failed":false}],[":tracer.test.ts",{"duration":3.9392250000000004,"failed":false}],[":rate-limit-fallback.test.ts",{"duration":3.124099000000001,"failed":false}],[":validate-artifacts.test.ts",{"duration":3.718504000000003,"failed":false}],[":plannotator.test.ts",{"duration":6.168539999999993,"failed":false}],[":agents/constraints.test.ts",{"duration":2.458521999999995,"failed":false}],[":custom-footer.test.ts",{"duration":3.8825789999999927,"failed":false}],[":phases/spawn-blocking.test.ts",{"duration":54.621959000000004,"failed":false}],[":exa.test.ts",{"duration":3.8611999999999966,"failed":false}],[":spawn-cleanup.test.ts",{"duration":2.2092779999999976,"failed":false}],[":subagent-session-marker.test.ts",{"duration":1.6192010000000039,"failed":false}],[":model-version.test.ts",{"duration":5.690410999999997,"failed":false}],[":messages.test.ts",{"duration":1.798244000000004,"failed":false}],[":cbm.test.ts",{"duration":2.306488999999999,"failed":false}],[":phases/brainstorm.test.ts",{"duration":1.6732620000000225,"failed":false}],[":phases/planning.test.ts",{"duration":1.6508400000000165,"failed":false}],[":phases/implementation.test.ts",{"duration":2.0228309999999965,"failed":false}]]}
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
type TaskState,
|
|
14
14
|
type Phase,
|
|
15
15
|
} from "./state.js";
|
|
16
|
-
import { phasePipeline } from "./phases/machine.js";
|
|
17
16
|
import { getContextDirs, loadAllContextFiles, getPhaseArtifacts, getLatestSynthesizedPlan } from "./context.js";
|
|
18
17
|
import { brainstormSystemPrompt } from "./phases/brainstorm.js";
|
|
19
18
|
import { planningSystemPrompt, spawnPlanners } from "./phases/planning.js";
|
|
@@ -31,6 +30,7 @@ import { resolveModel, getModelInfo, findLatestFamilyMatch, setSubscriptionFallb
|
|
|
31
30
|
import { buildRepoContext } from "./agents/repo-context.js";
|
|
32
31
|
import { getLogger, addTaskDestination, removeTaskDestination, setLogLevel } from "./log.js";
|
|
33
32
|
import { handleSpawnResult } from "./spawn-cleanup.js";
|
|
33
|
+
import { getTracer } from "./tracer.js";
|
|
34
34
|
import { TransitionController, type TransitionHost } from "./transition-controller.js";
|
|
35
35
|
|
|
36
36
|
function isEnabled(value: { enabled?: boolean } | undefined): boolean {
|
|
@@ -73,6 +73,16 @@ export class Orchestrator {
|
|
|
73
73
|
step?: string;
|
|
74
74
|
}>();
|
|
75
75
|
staleAgentTimer: ReturnType<typeof setInterval> | null = null;
|
|
76
|
+
// Main-turn stall watchdog (BUG-2). A main turn that starts but never emits a
|
|
77
|
+
// terminal turn_end/error can wedge the session ("Working…" forever). Unlike
|
|
78
|
+
// staleAgentTimer (subagents only), this watches the MAIN session: any main-
|
|
79
|
+
// session stream/tool/turn activity refreshes mainTurnLastActivity; when a turn
|
|
80
|
+
// is in flight with no activity beyond config.performance.internals.mainTurnStale,
|
|
81
|
+
// the watchdog recovers via the idle-gated single-send path.
|
|
82
|
+
mainTurnTimer: ReturnType<typeof setInterval> | null = null;
|
|
83
|
+
mainTurnLastActivity = 0;
|
|
84
|
+
mainTurnInFlight = false;
|
|
85
|
+
mainTurnRecovering = false;
|
|
76
86
|
// Single consecutive-nudge guard (replaces the old multi-tier throttle). Reset
|
|
77
87
|
// to 0 on any productive turn; once it reaches the cap the nudges halt with one
|
|
78
88
|
// user notification.
|
|
@@ -96,6 +106,14 @@ export class Orchestrator {
|
|
|
96
106
|
// without this ESC would not cancel it.
|
|
97
107
|
pendingRetryEscUnsub: (() => void) | null = null;
|
|
98
108
|
activeTaskToken = 0;
|
|
109
|
+
// Side-channel for stale-nudge re-validation. A continuation nudge is delivered
|
|
110
|
+
// as a followUp whose prompt STRING carries no phase/task token, and the SDK
|
|
111
|
+
// queue only surfaces the string in before_agent_start. So at nudge-generation
|
|
112
|
+
// time we record {phase, taskToken} keyed by the exact nudge string; at delivery
|
|
113
|
+
// we re-check both against the live phase/token and drop the nudge on mismatch
|
|
114
|
+
// (a nudge generated for an old phase/task must not drive a turn in the new one).
|
|
115
|
+
// Last-wins per distinct string; a stale entry only ever fails-closed to drop.
|
|
116
|
+
pendingNudges = new Map<string, { phase: Phase; taskToken: number }>();
|
|
99
117
|
// Subscription rate-limit fallback (Issue 5). subFallbackActive mirrors the
|
|
100
118
|
// model-registry override flag; subFallbackDialogPending guards against
|
|
101
119
|
// opening more than one switch dialogue at a time (across main + subagents);
|
|
@@ -103,6 +121,11 @@ export class Orchestrator {
|
|
|
103
121
|
// switch-back probe); subSwitchBackTimer is the fixed-interval probe timer.
|
|
104
122
|
subFallbackActive = false;
|
|
105
123
|
subFallbackDialogPending = false;
|
|
124
|
+
// True while a user-facing dialogue (ask_user / the /pp menu / any interactive
|
|
125
|
+
// selectOption) is open. The main-turn watchdog skips while set so a turn
|
|
126
|
+
// legitimately parked on a human is not aborted. Set on dialogue open, cleared
|
|
127
|
+
// in finally on every exit (resolve, ESC/cancel, error).
|
|
128
|
+
interactivePromptOpen = false;
|
|
106
129
|
// Set SYNCHRONOUSLY the moment a sub-429 is detected (before any async dialog),
|
|
107
130
|
// and cleared once the decision resolves. The autonomous planner/reviewer
|
|
108
131
|
// auto-retry consults this to avoid re-spawning a failed variant on the still-
|
|
@@ -129,10 +152,16 @@ export class Orchestrator {
|
|
|
129
152
|
checkPlannerCompletion: () => void = () => {};
|
|
130
153
|
readonly transitionController: TransitionController;
|
|
131
154
|
|
|
155
|
+
// The single live instance, so module-level dialogue wrappers (selectOption in
|
|
156
|
+
// event-handlers/pp-menu) can toggle interactivePromptOpen without threading a
|
|
157
|
+
// reference through ~70 call sites.
|
|
158
|
+
static current: Orchestrator | null = null;
|
|
159
|
+
|
|
132
160
|
constructor(readonly pi: ExtensionAPI) {
|
|
133
161
|
// The controller calls pi (the main session) directly for sends, and uses the
|
|
134
162
|
// host only for live-ctx-dependent bits (compact/isIdle/currentStep).
|
|
135
163
|
this.transitionController = new TransitionController(this.makeTransitionHost(), this.pi);
|
|
164
|
+
Orchestrator.current = this;
|
|
136
165
|
}
|
|
137
166
|
|
|
138
167
|
// Live-session host the TransitionController uses for compaction/idle/step.
|
|
@@ -329,67 +358,17 @@ export class Orchestrator {
|
|
|
329
358
|
return true;
|
|
330
359
|
}
|
|
331
360
|
|
|
361
|
+
// The footer's phase/mode display (line 1) reads orchestrator state directly; this only
|
|
362
|
+
// sets a hidden "pp-phase" status whose value changes per transition so the host repaints
|
|
363
|
+
// the footer. Nothing renders this string (footer line 3 was removed), so it stays terse.
|
|
332
364
|
updateStatus(ctx: ExtensionContext): void {
|
|
333
365
|
if (!this.active || this.active.state.phase === "done") {
|
|
334
366
|
ctx.ui.setStatus("pp-phase", undefined);
|
|
335
367
|
return;
|
|
336
368
|
}
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
const step = this.active.state.step;
|
|
341
|
-
const reviewCycle = this.active.state.reviewCycle;
|
|
342
|
-
const effectiveMode = this.active.state.effectiveMode ?? this.active.state.mode;
|
|
343
|
-
const modeLabel = effectiveMode === "autonomous" ? " [autonomous]" : "";
|
|
344
|
-
|
|
345
|
-
if (type === "debug" || type === "brainstorm" || type === "quick") {
|
|
346
|
-
const elapsed = this.phaseStartTime > 0 ? this.formatElapsed(this.phaseStartTime) : "";
|
|
347
|
-
const suffix = elapsed ? ` (${elapsed})` : "";
|
|
348
|
-
ctx.ui.setStatus("pp-phase", `pp: ${type}${modeLabel}${suffix}`);
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const pipeline = phasePipeline(type).filter((p) => p !== "done");
|
|
353
|
-
const currentIdx = pipeline.indexOf(phase as (typeof pipeline)[number]);
|
|
354
|
-
|
|
355
|
-
const parts: string[] = [];
|
|
356
|
-
for (let i = 0; i < pipeline.length; i++) {
|
|
357
|
-
const p = pipeline[i];
|
|
358
|
-
if (i < currentIdx) {
|
|
359
|
-
parts.push(`✔ ${p}`);
|
|
360
|
-
} else if (p === phase) {
|
|
361
|
-
let detail = "";
|
|
362
|
-
if (step === "await_planners") detail = "planners";
|
|
363
|
-
else if (step === "await_reviewers") detail = "reviewers";
|
|
364
|
-
else if (step === "synthesize") detail = "synthesize";
|
|
365
|
-
else if (step === "apply_feedback") detail = "feedback";
|
|
366
|
-
else if (step === "user_gate") detail = "review";
|
|
367
|
-
|
|
368
|
-
if (reviewCycle) {
|
|
369
|
-
const kind = reviewCycle.kind === "plannotator" ? "plannotator" : "review";
|
|
370
|
-
detail = `${kind} #${reviewCycle.pass}`;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const elapsed = this.phaseStartTime > 0 ? this.formatElapsed(this.phaseStartTime) : "";
|
|
374
|
-
const sub = [detail, elapsed].filter(Boolean).join(", ");
|
|
375
|
-
parts.push(sub ? `${p} (${sub})` : p);
|
|
376
|
-
} else {
|
|
377
|
-
parts.push(p);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
ctx.ui.setStatus("pp-phase", `pp: ${parts.join(" → ")}${modeLabel}`);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
private formatElapsed(startTime: number): string {
|
|
385
|
-
const sec = Math.floor((Date.now() - startTime) / 1000);
|
|
386
|
-
if (sec < 60) return `${sec}s`;
|
|
387
|
-
const min = Math.floor(sec / 60);
|
|
388
|
-
const remSec = sec % 60;
|
|
389
|
-
if (min < 60) return remSec > 0 ? `${min}m ${remSec}s` : `${min}m`;
|
|
390
|
-
const hr = Math.floor(min / 60);
|
|
391
|
-
const remMin = min % 60;
|
|
392
|
-
return remMin > 0 ? `${hr}h ${remMin}m` : `${hr}h`;
|
|
369
|
+
const s = this.active.state;
|
|
370
|
+
const cycle = s.reviewCycle ? `:${s.reviewCycle.kind}#${s.reviewCycle.pass}` : "";
|
|
371
|
+
ctx.ui.setStatus("pp-phase", `${this.active.type}:${s.phase}:${s.step}:${getEffectivePhaseMode(s)}${cycle}`);
|
|
393
372
|
}
|
|
394
373
|
|
|
395
374
|
getPlanStartState(taskDir: string, plannerPresetName?: string): { step: string; shouldSpawnPlanners: boolean } {
|
|
@@ -480,7 +459,8 @@ export class Orchestrator {
|
|
|
480
459
|
this.lastCtx = ctx;
|
|
481
460
|
await this.transitionController.requestTransition({
|
|
482
461
|
kind: "done",
|
|
483
|
-
|
|
462
|
+
discard: true,
|
|
463
|
+
summary: `A new, unrelated ${type} task is starting. The previous task is finished — DISCARD its entire conversation. Do NOT carry forward, reference, or act on any prior task's messages, phase, plan, or aborted turns; treat the new task as a clean slate.`,
|
|
484
464
|
});
|
|
485
465
|
}
|
|
486
466
|
|
|
@@ -653,6 +633,7 @@ export class Orchestrator {
|
|
|
653
633
|
this.commitReminderSent = false;
|
|
654
634
|
this.consecutiveNudges = 0;
|
|
655
635
|
this.nudgeHalted = false;
|
|
636
|
+
this.pendingNudges.clear();
|
|
656
637
|
this.phaseStartTime = 0;
|
|
657
638
|
this.userGatePending = false;
|
|
658
639
|
this.failedPlannerVariants = [];
|
|
@@ -668,6 +649,12 @@ export class Orchestrator {
|
|
|
668
649
|
clearInterval(this.staleAgentTimer);
|
|
669
650
|
this.staleAgentTimer = null;
|
|
670
651
|
}
|
|
652
|
+
if (this.mainTurnTimer) {
|
|
653
|
+
clearInterval(this.mainTurnTimer);
|
|
654
|
+
this.mainTurnTimer = null;
|
|
655
|
+
}
|
|
656
|
+
this.mainTurnInFlight = false;
|
|
657
|
+
this.mainTurnRecovering = false;
|
|
671
658
|
this.clearSubscriptionFallback();
|
|
672
659
|
}
|
|
673
660
|
|
|
@@ -681,6 +668,7 @@ export class Orchestrator {
|
|
|
681
668
|
}
|
|
682
669
|
this.subFallbackActive = false;
|
|
683
670
|
this.subFallbackDialogPending = false;
|
|
671
|
+
this.interactivePromptOpen = false;
|
|
684
672
|
this.subFallbackPendingDecision = false;
|
|
685
673
|
this.subFallbackModelId = null;
|
|
686
674
|
setSubscriptionFallbackActive(false);
|