@ilya-lesikov/pi-pi 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/3p/pi-ask-user/index.ts +1 -1
- package/extensions/orchestrator/config.ts +3 -2
- package/extensions/orchestrator/custom-footer.ts +5 -2
- package/extensions/orchestrator/doctor.test.ts +3 -1
- package/extensions/orchestrator/doctor.ts +40 -2
- package/extensions/orchestrator/event-handlers.ts +47 -6
- package/extensions/orchestrator/flant-infra.test.ts +287 -0
- package/extensions/orchestrator/flant-infra.ts +316 -44
- package/extensions/orchestrator/integration.test.ts +205 -6
- package/extensions/orchestrator/model-registry.test.ts +76 -12
- package/extensions/orchestrator/model-registry.ts +48 -32
- package/extensions/orchestrator/orchestrator.ts +13 -2
- package/extensions/orchestrator/phases/review.test.ts +54 -0
- package/extensions/orchestrator/phases/review.ts +54 -12
- package/extensions/orchestrator/pp-menu.test.ts +74 -1
- package/extensions/orchestrator/pp-menu.ts +199 -41
- package/extensions/orchestrator/state.ts +41 -20
- package/extensions/orchestrator/test-helpers.ts +14 -2
- package/extensions/orchestrator/usage-tracker.test.ts +131 -3
- package/extensions/orchestrator/usage-tracker.ts +74 -11
- package/package.json +1 -1
|
@@ -63,6 +63,7 @@ vi.mock("./config.js", async (importOriginal) => {
|
|
|
63
63
|
debug: { model: "test/model", thinking: "high" },
|
|
64
64
|
brainstorm: { model: "test/model", thinking: "high" },
|
|
65
65
|
review: { model: "test/model", thinking: "high" },
|
|
66
|
+
quick: { model: "test/model", thinking: "high" },
|
|
66
67
|
},
|
|
67
68
|
subagents: {
|
|
68
69
|
simple: {
|
|
@@ -215,6 +216,7 @@ function makeConfig() {
|
|
|
215
216
|
debug: { model: "test/model", thinking: "high" },
|
|
216
217
|
brainstorm: { model: "test/model", thinking: "high" },
|
|
217
218
|
review: { model: "test/model", thinking: "high" },
|
|
219
|
+
quick: { model: "test/model", thinking: "high" },
|
|
218
220
|
},
|
|
219
221
|
subagents: {
|
|
220
222
|
simple: {
|
|
@@ -1565,7 +1567,7 @@ describe("task modes and quick task", () => {
|
|
|
1565
1567
|
expect(orchestrator.active!.state.autonomousConfig).toBeDefined();
|
|
1566
1568
|
});
|
|
1567
1569
|
|
|
1568
|
-
it("autonomous pp_phase_complete auto-advances without opening menu", async () => {
|
|
1570
|
+
it("autonomous pp_phase_complete auto-advances (plan→implement) without opening menu", async () => {
|
|
1569
1571
|
const cwd = makeTempDir();
|
|
1570
1572
|
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
1571
1573
|
const ctx = makeCtx();
|
|
@@ -1581,16 +1583,115 @@ describe("task modes and quick task", () => {
|
|
|
1581
1583
|
};
|
|
1582
1584
|
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
1583
1585
|
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
1586
|
+
// Move to an autonomous-capable phase (plan). brainstorm is always guided
|
|
1587
|
+
// even for an autonomous task, so auto-advance only applies from plan onward.
|
|
1588
|
+
orchestrator.active!.state.phase = "plan";
|
|
1589
|
+
orchestrator.active!.state.step = "llm_work";
|
|
1590
|
+
const plansDir = join(taskDir, "plans");
|
|
1591
|
+
mkdirSync(plansDir, { recursive: true });
|
|
1592
|
+
writeFileSync(
|
|
1593
|
+
join(plansDir, `${Math.floor(Date.now() / 1000)}_synthesized.md`),
|
|
1594
|
+
makeValidPlan([
|
|
1595
|
+
"- [ ] P1. Implement X — Done when: implementation for X is complete",
|
|
1596
|
+
]),
|
|
1597
|
+
"utf-8",
|
|
1598
|
+
);
|
|
1584
1599
|
|
|
1585
1600
|
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
1586
1601
|
const result = await ppPhaseComplete.execute("call-1", { summary: "done" }, undefined, undefined, ctx);
|
|
1587
1602
|
await new Promise((r) => setTimeout(r, 10));
|
|
1588
1603
|
|
|
1589
1604
|
expect(result.content[0].text).toBe("");
|
|
1590
|
-
expect(orchestrator.active!.state.phase).toBe("
|
|
1605
|
+
expect(orchestrator.active!.state.phase).toBe("implement");
|
|
1591
1606
|
expect(menu.transcript.filter((entry) => entry.question.startsWith("/pp"))).toHaveLength(0);
|
|
1592
1607
|
});
|
|
1593
1608
|
|
|
1609
|
+
it("autonomous task in brainstorm stays guided (pp_phase_complete opens the menu, does NOT auto-advance)", async () => {
|
|
1610
|
+
const cwd = makeTempDir();
|
|
1611
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
1612
|
+
const ctx = makeCtx();
|
|
1613
|
+
|
|
1614
|
+
await orchestrator.startTask(ctx as any, "implement", "implement", undefined, undefined, "autonomous");
|
|
1615
|
+
const taskDir = orchestrator.active!.dir;
|
|
1616
|
+
orchestrator.active!.state.autonomousConfig = {
|
|
1617
|
+
phases: {
|
|
1618
|
+
brainstorm: { reviewPreset: "regular", maxReviewPasses: 0 },
|
|
1619
|
+
plan: { plannerPreset: "regular", reviewPreset: "regular", maxReviewPasses: 0 },
|
|
1620
|
+
implement: { reviewPreset: "regular", maxReviewPasses: 0 },
|
|
1621
|
+
},
|
|
1622
|
+
};
|
|
1623
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
1624
|
+
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
1625
|
+
expect(orchestrator.active!.state.phase).toBe("brainstorm");
|
|
1626
|
+
|
|
1627
|
+
// brainstorm is user-driven: pp_phase_complete must fall through to the
|
|
1628
|
+
// guided menu rather than the autonomous auto-advance branch. (Autonomous
|
|
1629
|
+
// tasks skip the planner preset picker on transition, so only the guided
|
|
1630
|
+
// Next→Continue step is expected here.)
|
|
1631
|
+
expectActiveTaskNext(menu, "Continue to plan & implement");
|
|
1632
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
1633
|
+
await ppPhaseComplete.execute("call-1", { summary: "done" }, undefined, undefined, ctx);
|
|
1634
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1635
|
+
|
|
1636
|
+
// The guided menu was shown (auto-advance did NOT bypass it).
|
|
1637
|
+
expect(menu.transcript.filter((entry) => entry.question.startsWith("/pp"))).not.toHaveLength(0);
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
it("ESC on pp_phase_complete stops the turn cleanly (aborts, no reminder text)", async () => {
|
|
1641
|
+
const cwd = makeTempDir();
|
|
1642
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
1643
|
+
const ctx = makeCtx();
|
|
1644
|
+
|
|
1645
|
+
await orchestrator.startTask(ctx as any, "implement", "esc dismiss");
|
|
1646
|
+
// Guided task in brainstorm; transition controller is running so the
|
|
1647
|
+
// non-ESC dismiss path would normally return the reminder text.
|
|
1648
|
+
expect(orchestrator.transitionController.isRunning()).toBe(true);
|
|
1649
|
+
|
|
1650
|
+
// Simulate a deliberate user ESC on the top-level menu.
|
|
1651
|
+
menu.expect({ question: m.anyTaskMenu, cancel: "user" });
|
|
1652
|
+
|
|
1653
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
1654
|
+
const result = await ppPhaseComplete.execute("esc-1", { summary: "done" }, undefined, undefined, ctx);
|
|
1655
|
+
|
|
1656
|
+
expect(ctx.abort).toHaveBeenCalled();
|
|
1657
|
+
expect(result.content[0].text).toBe("");
|
|
1658
|
+
});
|
|
1659
|
+
|
|
1660
|
+
it("Back on pp_phase_complete keeps the artifact-update reminder (does NOT abort)", async () => {
|
|
1661
|
+
const cwd = makeTempDir();
|
|
1662
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
1663
|
+
const ctx = makeCtx();
|
|
1664
|
+
|
|
1665
|
+
await orchestrator.startTask(ctx as any, "implement", "back dismiss");
|
|
1666
|
+
expect(orchestrator.transitionController.isRunning()).toBe(true);
|
|
1667
|
+
|
|
1668
|
+
// Explicit "Back" navigation (not an ESC): the reminder must be preserved.
|
|
1669
|
+
menu.expect({ question: m.anyTaskMenu, options: { include: ["Back"] }, choose: "Back" });
|
|
1670
|
+
|
|
1671
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
1672
|
+
const result = await ppPhaseComplete.execute("back-1", { summary: "done" }, undefined, undefined, ctx);
|
|
1673
|
+
|
|
1674
|
+
expect(ctx.abort).not.toHaveBeenCalled();
|
|
1675
|
+
expect(result.content[0].text).toContain("update USER_REQUEST.md and RESEARCH.md");
|
|
1676
|
+
});
|
|
1677
|
+
|
|
1678
|
+
it("ESC on pp_phase_complete for a quick task also stops the turn cleanly", async () => {
|
|
1679
|
+
const cwd = makeTempDir();
|
|
1680
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
1681
|
+
const ctx = makeCtx();
|
|
1682
|
+
|
|
1683
|
+
await orchestrator.startTask(ctx as any, "quick", "quick esc");
|
|
1684
|
+
expect(orchestrator.active!.type).toBe("quick");
|
|
1685
|
+
|
|
1686
|
+
menu.expect({ question: m.anyTaskMenu, cancel: "user" });
|
|
1687
|
+
|
|
1688
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
1689
|
+
const result = await ppPhaseComplete.execute("quick-esc-1", { summary: "done" }, undefined, undefined, ctx);
|
|
1690
|
+
|
|
1691
|
+
expect(ctx.abort).toHaveBeenCalled();
|
|
1692
|
+
expect(result.content[0].text).toBe("");
|
|
1693
|
+
});
|
|
1694
|
+
|
|
1594
1695
|
it("autonomous review loop re-runs until cap then advances", async () => {
|
|
1595
1696
|
const cwd = makeTempDir();
|
|
1596
1697
|
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
@@ -1694,6 +1795,52 @@ describe("task modes and quick task", () => {
|
|
|
1694
1795
|
expect(finalState.reviewPassByKind?.implement?.auto).toBe(1);
|
|
1695
1796
|
});
|
|
1696
1797
|
|
|
1798
|
+
it("autonomous PLAN review cycle: apply feedback (clean approve) advances to implement", async () => {
|
|
1799
|
+
const cwd = makeTempDir();
|
|
1800
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
1801
|
+
const ctx = makeCtx();
|
|
1802
|
+
|
|
1803
|
+
await orchestrator.startTask(ctx as any, "implement", "plan review advance", undefined, undefined, "autonomous");
|
|
1804
|
+
const taskDir = orchestrator.active!.dir;
|
|
1805
|
+
orchestrator.active!.state.phase = "plan";
|
|
1806
|
+
orchestrator.active!.state.step = "synthesize";
|
|
1807
|
+
orchestrator.active!.state.reviewPass = 0;
|
|
1808
|
+
orchestrator.active!.state.reviewPassByKind = {};
|
|
1809
|
+
orchestrator.active!.state.autonomousConfig = {
|
|
1810
|
+
phases: { plan: { plannerPreset: "regular", reviewPreset: "regular", maxReviewPasses: 3 } },
|
|
1811
|
+
};
|
|
1812
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
1813
|
+
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
1814
|
+
const plansDir = join(taskDir, "plans");
|
|
1815
|
+
mkdirSync(plansDir, { recursive: true });
|
|
1816
|
+
writeFileSync(
|
|
1817
|
+
join(plansDir, "1_synthesized.md"),
|
|
1818
|
+
makeValidPlan(["- [x] P1. Plan ready — Done when: synthesized plan exists"]),
|
|
1819
|
+
"utf-8",
|
|
1820
|
+
);
|
|
1821
|
+
|
|
1822
|
+
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
1823
|
+
// First call: plan phase is autonomous, no clean approval yet → starts a review cycle.
|
|
1824
|
+
const first = await ppPhaseComplete.execute("plan-rev-1", { summary: "plan done" }, undefined, undefined, ctx);
|
|
1825
|
+
expect(first.content[0].text).toMatch(/Reviews are running|Started review cycle pass/);
|
|
1826
|
+
|
|
1827
|
+
// Plan reviewers write to plan-reviews/ (not code-reviews/).
|
|
1828
|
+
const planReviewsDir = join(taskDir, "plan-reviews");
|
|
1829
|
+
mkdirSync(planReviewsDir, { recursive: true });
|
|
1830
|
+
const round = orchestrator.active!.state.reviewCycle!.pass;
|
|
1831
|
+
for (const v of ["opus", "gpt", "gemini"]) {
|
|
1832
|
+
writeFileSync(join(planReviewsDir, `1_${v}_round-${round}.md`), "VERDICT: APPROVE\n- CRITICAL: none", "utf-8");
|
|
1833
|
+
}
|
|
1834
|
+
emitSubagentCreated(pi, "plan-reviewer-1", "Plan reviewer (test)");
|
|
1835
|
+
emitSubagentCompleted(pi, "plan-reviewer-1", "Plan reviewer (test)");
|
|
1836
|
+
|
|
1837
|
+
// Second call: finalize the clean-approved pass and advance to implement (single transition).
|
|
1838
|
+
const second = await ppPhaseComplete.execute("plan-rev-2", { summary: "feedback applied" }, undefined, undefined, ctx);
|
|
1839
|
+
expect(second.content[0].text).toBe("");
|
|
1840
|
+
expect(orchestrator.active!.state.phase).toBe("implement");
|
|
1841
|
+
expect(menu.transcript.filter((entry) => entry.question.startsWith("/pp"))).toHaveLength(0);
|
|
1842
|
+
});
|
|
1843
|
+
|
|
1697
1844
|
it("clean-approved review is not re-run on a later checklist-repair re-entry", async () => {
|
|
1698
1845
|
const cwd = makeTempDir();
|
|
1699
1846
|
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
@@ -2041,10 +2188,11 @@ describe("task modes and quick task", () => {
|
|
|
2041
2188
|
|
|
2042
2189
|
const title = "implement";
|
|
2043
2190
|
const pp = getCommand(pi, "pp");
|
|
2191
|
+
const titleMatch = (t: string) => t.includes(title);
|
|
2044
2192
|
menu
|
|
2045
2193
|
.expect({ question: "/pp", options: { include: ["Task"] }, choose: "Task" })
|
|
2046
2194
|
.expect({ question: "Task", options: { include: ["Resume"] }, choose: "Resume" })
|
|
2047
|
-
.expect({ question: "Resume", options: { include: [
|
|
2195
|
+
.expect({ question: "Resume", options: { include: [titleMatch] }, choose: titleMatch });
|
|
2048
2196
|
await pp(undefined, ctx);
|
|
2049
2197
|
|
|
2050
2198
|
expect(orchestrator.active).not.toBeNull();
|
|
@@ -2069,10 +2217,11 @@ describe("task modes and quick task", () => {
|
|
|
2069
2217
|
await orchestrator.cleanupActive();
|
|
2070
2218
|
|
|
2071
2219
|
const pp = getCommand(pi, "pp");
|
|
2220
|
+
const configTitleMatch = (t: string) => t.includes("resume autonomous config");
|
|
2072
2221
|
menu
|
|
2073
2222
|
.expect({ question: "/pp", options: { include: ["Task"] }, choose: "Task" })
|
|
2074
2223
|
.expect({ question: "Task", options: { include: ["Resume"] }, choose: "Resume" })
|
|
2075
|
-
.expect({ question: "Resume", options: { include: [
|
|
2224
|
+
.expect({ question: "Resume", options: { include: [configTitleMatch] }, choose: configTitleMatch });
|
|
2076
2225
|
await pp(undefined, ctx);
|
|
2077
2226
|
|
|
2078
2227
|
expect(orchestrator.active).not.toBeNull();
|
|
@@ -2102,6 +2251,10 @@ describe("task modes and quick task", () => {
|
|
|
2102
2251
|
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
2103
2252
|
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
2104
2253
|
|
|
2254
|
+
// brainstorm is user-driven even for an autonomous task, so the transition
|
|
2255
|
+
// to plan goes through the guided menu (which auto-skips the planner preset
|
|
2256
|
+
// picker because the task is autonomous).
|
|
2257
|
+
expectActiveTaskNext(menu, "Continue to plan & implement");
|
|
2105
2258
|
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
2106
2259
|
await ppPhaseComplete.execute("call-autonomous-skip-planner", { summary: "done" }, undefined, undefined, ctx);
|
|
2107
2260
|
await new Promise((r) => setTimeout(r, 10));
|
|
@@ -2596,7 +2749,7 @@ describe("resume and recovery", () => {
|
|
|
2596
2749
|
menu
|
|
2597
2750
|
.expect({ question: "/pp", options: { include: ["Task"] }, choose: "Task" })
|
|
2598
2751
|
.expect({ question: "Task", options: { include: ["Resume"] }, choose: "Resume" })
|
|
2599
|
-
.expect({ question: "Resume", options: { include: ["resume phase state"] }, choose: "resume phase state" });
|
|
2752
|
+
.expect({ question: "Resume", options: { include: [(t: string) => t.includes("resume phase state")] }, choose: (t: string) => t.includes("resume phase state") });
|
|
2600
2753
|
await pp(undefined, ctx);
|
|
2601
2754
|
|
|
2602
2755
|
expect(orchestrator.active).not.toBeNull();
|
|
@@ -2625,7 +2778,7 @@ describe("resume and recovery", () => {
|
|
|
2625
2778
|
menu
|
|
2626
2779
|
.expect({ question: "/pp", options: { include: ["Task"] }, choose: "Task" })
|
|
2627
2780
|
.expect({ question: "Task", options: { include: ["Resume"] }, choose: "Resume" })
|
|
2628
|
-
.expect({ question: "Resume", options: { include: ["resume stale repos"] }, choose: "resume stale repos" });
|
|
2781
|
+
.expect({ question: "Resume", options: { include: [(t: string) => t.includes("resume stale repos")] }, choose: (t: string) => t.includes("resume stale repos") });
|
|
2629
2782
|
await pp(undefined, ctx);
|
|
2630
2783
|
|
|
2631
2784
|
expect(orchestrator.active!.state.repos).toEqual([{ path: cwd, isRoot: true }]);
|
|
@@ -2852,6 +3005,49 @@ describe("brainstorm and plan review cycles", () => {
|
|
|
2852
3005
|
expect(readyMessages[readyMessages.length - 1]).toContain("artifacts/");
|
|
2853
3006
|
});
|
|
2854
3007
|
|
|
3008
|
+
const runPlanReviewReady = async (mode?: "autonomous") => {
|
|
3009
|
+
const cwd = makeTempDir();
|
|
3010
|
+
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
3011
|
+
const ctx = makeCtx({ cwd });
|
|
3012
|
+
|
|
3013
|
+
await orchestrator.startTask(ctx as any, "implement", "plan review message", undefined, undefined, mode);
|
|
3014
|
+
const taskDir = orchestrator.active!.dir;
|
|
3015
|
+
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
3016
|
+
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
3017
|
+
mkdirSync(join(taskDir, "plan-reviews"), { recursive: true });
|
|
3018
|
+
writeFileSync(
|
|
3019
|
+
join(taskDir, "plan-reviews", `${Math.floor(Date.now() / 1000)}_test_round-1.md`),
|
|
3020
|
+
"Plan review feedback",
|
|
3021
|
+
"utf-8",
|
|
3022
|
+
);
|
|
3023
|
+
orchestrator.active!.state.phase = "plan";
|
|
3024
|
+
orchestrator.active!.state.step = "synthesize";
|
|
3025
|
+
saveTask(taskDir, orchestrator.active!.state);
|
|
3026
|
+
|
|
3027
|
+
await enterReviewCycle(orchestrator, ctx, "regular");
|
|
3028
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
3029
|
+
|
|
3030
|
+
return (pi.sendUserMessage as any).mock.calls
|
|
3031
|
+
.map((c: any[]) => c[0] as string)
|
|
3032
|
+
.filter((text: string) => text.includes("ready for apply_feedback"));
|
|
3033
|
+
};
|
|
3034
|
+
|
|
3035
|
+
it("autonomous plan review-ready message mandates re-calling pp_phase_complete", async () => {
|
|
3036
|
+
const readyMessages = await runPlanReviewReady("autonomous");
|
|
3037
|
+
expect(readyMessages.length).toBeGreaterThan(0);
|
|
3038
|
+
const last = readyMessages[readyMessages.length - 1];
|
|
3039
|
+
expect(last).toContain("pp_phase_complete");
|
|
3040
|
+
expect(last).toContain("Do NOT stop or wait for the user");
|
|
3041
|
+
});
|
|
3042
|
+
|
|
3043
|
+
it("guided plan review-ready message does NOT tell the agent to re-call pp_phase_complete or auto-advance", async () => {
|
|
3044
|
+
const readyMessages = await runPlanReviewReady();
|
|
3045
|
+
expect(readyMessages.length).toBeGreaterThan(0);
|
|
3046
|
+
const last = readyMessages[readyMessages.length - 1];
|
|
3047
|
+
expect(last).not.toContain("pp_phase_complete");
|
|
3048
|
+
expect(last).not.toContain("Do NOT stop or wait for the user");
|
|
3049
|
+
});
|
|
3050
|
+
|
|
2855
3051
|
it("plan review cycle reaches apply_feedback and finalizeReviewCycle returns to user_gate", async () => {
|
|
2856
3052
|
const cwd = makeTempDir();
|
|
2857
3053
|
const { pi, orchestrator } = await setupOrchestrator(cwd);
|
|
@@ -3917,6 +4113,9 @@ describe("full user flows", () => {
|
|
|
3917
4113
|
writeFileSync(join(taskDir, "USER_REQUEST.md"), VALID_USER_REQUEST, "utf-8");
|
|
3918
4114
|
writeFileSync(join(taskDir, "RESEARCH.md"), VALID_RESEARCH, "utf-8");
|
|
3919
4115
|
|
|
4116
|
+
// brainstorm is user-driven: the brainstorm→plan transition uses the guided
|
|
4117
|
+
// menu. plan→implement and implement→review below are autonomous auto-advance.
|
|
4118
|
+
expectActiveTaskNext(menu, "Continue to plan & implement");
|
|
3920
4119
|
const ppPhaseComplete = getTool(pi, "pp_phase_complete");
|
|
3921
4120
|
await ppPhaseComplete.execute("flow-auto-1", { summary: "brainstorm done" }, undefined, undefined, ctx);
|
|
3922
4121
|
await new Promise((r) => setTimeout(r, 10));
|
|
@@ -7,6 +7,7 @@ vi.mock("./log.js", () => ({
|
|
|
7
7
|
}));
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
|
+
findLatestFamilyMatch,
|
|
10
11
|
getAllAliases,
|
|
11
12
|
getModelFamilies,
|
|
12
13
|
getModelInfo,
|
|
@@ -19,20 +20,43 @@ describe("model-registry", () => {
|
|
|
19
20
|
updateRegistryFromAvailableModels([]);
|
|
20
21
|
});
|
|
21
22
|
|
|
22
|
-
it("resolveModel
|
|
23
|
-
expect(resolveModel("anthropic/claude-sonnet-latest")).toBe("anthropic/claude-sonnet-
|
|
24
|
-
expect(resolveModel("
|
|
23
|
+
it("resolveModel passes through native-latest aliases", () => {
|
|
24
|
+
expect(resolveModel("anthropic/claude-sonnet-latest")).toBe("anthropic/claude-sonnet-latest");
|
|
25
|
+
expect(resolveModel("anthropic/claude-opus-latest")).toBe("anthropic/claude-opus-latest");
|
|
26
|
+
expect(resolveModel("anthropic/claude-haiku-latest")).toBe("anthropic/claude-haiku-latest");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("resolveModel returns input unchanged for non-native aliases without available models", () => {
|
|
30
|
+
expect(resolveModel("openai/gpt-mini-latest")).toBe("openai/gpt-mini-latest");
|
|
31
|
+
expect(resolveModel("pp-flant-anthropic/claude-opus-latest")).toBe("pp-flant-anthropic/claude-opus-latest");
|
|
25
32
|
});
|
|
26
33
|
|
|
27
34
|
it("resolveModel passes through unknown aliases", () => {
|
|
28
35
|
expect(resolveModel("custom/provider-model")).toBe("custom/provider-model");
|
|
29
36
|
});
|
|
30
37
|
|
|
31
|
-
it("resolveModel resolves flant aliases", () => {
|
|
38
|
+
it("resolveModel resolves flant aliases after updateRegistry", () => {
|
|
39
|
+
updateRegistryFromAvailableModels(["claude-opus-4-6", "gemini-3.1-pro"]);
|
|
32
40
|
expect(resolveModel("pp-flant-anthropic/claude-opus-latest")).toBe("pp-flant-anthropic/claude-opus-4-6");
|
|
33
41
|
expect(resolveModel("pp-flant-openai/gemini-pro-latest")).toBe("pp-flant-openai/gemini-3.1-pro");
|
|
34
42
|
});
|
|
35
43
|
|
|
44
|
+
it("getModelInfo detects personal-subscription Claude models", () => {
|
|
45
|
+
expect(getModelInfo("pp-flant-anthropic-sub/sub/claude-opus-4-8")).toMatchObject({ vendor: "anthropic", family: "opus", tier: "smart" });
|
|
46
|
+
expect(getModelInfo("pp-flant-anthropic-sub/sub/claude-sonnet-4-6")).toMatchObject({ vendor: "anthropic", family: "sonnet", tier: "regular" });
|
|
47
|
+
expect(getModelInfo("pp-flant-anthropic-sub/sub/claude-haiku-4-5")).toMatchObject({ vendor: "anthropic", family: "haiku", tier: "stupid" });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("resolveModel resolves subscription aliases to sub/ specs after updateRegistry", () => {
|
|
51
|
+
updateRegistryFromAvailableModels([
|
|
52
|
+
"pp-flant-anthropic-sub/sub/claude-opus-4-7",
|
|
53
|
+
"pp-flant-anthropic-sub/sub/claude-opus-4-8",
|
|
54
|
+
"pp-flant-anthropic-sub/sub/claude-haiku-4-5",
|
|
55
|
+
]);
|
|
56
|
+
expect(resolveModel("pp-flant-anthropic-sub/claude-opus-latest")).toBe("pp-flant-anthropic-sub/sub/claude-opus-4-8");
|
|
57
|
+
expect(resolveModel("pp-flant-anthropic-sub/claude-haiku-latest")).toBe("pp-flant-anthropic-sub/sub/claude-haiku-4-5");
|
|
58
|
+
});
|
|
59
|
+
|
|
36
60
|
it("getModelInfo detects all configured families", () => {
|
|
37
61
|
expect(getModelInfo("anthropic/claude-opus-4-6")).toMatchObject({ vendor: "anthropic", family: "opus", tier: "smart" });
|
|
38
62
|
expect(getModelInfo("anthropic/claude-sonnet-4-6")).toMatchObject({ vendor: "anthropic", family: "sonnet", tier: "regular" });
|
|
@@ -153,11 +177,11 @@ describe("model-registry", () => {
|
|
|
153
177
|
expect(resolveModel("openai/gpt-latest")).toBe("openai/gpt-5.6");
|
|
154
178
|
});
|
|
155
179
|
|
|
156
|
-
it("updateRegistryFromAvailableModels keeps
|
|
180
|
+
it("updateRegistryFromAvailableModels keeps native-latest for missing families", () => {
|
|
157
181
|
updateRegistryFromAvailableModels(["openai/gpt-9.1"]);
|
|
158
182
|
|
|
159
183
|
expect(resolveModel("openai/gpt-latest")).toBe("openai/gpt-9.1");
|
|
160
|
-
expect(resolveModel("anthropic/claude-opus-latest")).toBe("anthropic/claude-opus-
|
|
184
|
+
expect(resolveModel("anthropic/claude-opus-latest")).toBe("anthropic/claude-opus-latest");
|
|
161
185
|
});
|
|
162
186
|
|
|
163
187
|
it("updateRegistryFromAvailableModels ignores aliases ending with -latest", () => {
|
|
@@ -168,7 +192,7 @@ describe("model-registry", () => {
|
|
|
168
192
|
]);
|
|
169
193
|
|
|
170
194
|
expect(resolveModel("openai/gpt-latest")).toBe("openai/gpt-5.4");
|
|
171
|
-
expect(resolveModel("pp-flant-anthropic/claude-opus-latest")).toBe("pp-flant-anthropic/claude-opus-
|
|
195
|
+
expect(resolveModel("pp-flant-anthropic/claude-opus-latest")).toBe("pp-flant-anthropic/claude-opus-latest");
|
|
172
196
|
});
|
|
173
197
|
|
|
174
198
|
it("updateRegistryFromAvailableModels handles empty input", () => {
|
|
@@ -176,14 +200,14 @@ describe("model-registry", () => {
|
|
|
176
200
|
expect(resolveModel("openai/gpt-latest")).toBe("openai/gpt-9.9");
|
|
177
201
|
|
|
178
202
|
updateRegistryFromAvailableModels([]);
|
|
179
|
-
expect(resolveModel("openai/gpt-latest")).toBe("openai/gpt-
|
|
203
|
+
expect(resolveModel("openai/gpt-latest")).toBe("openai/gpt-latest");
|
|
180
204
|
});
|
|
181
205
|
|
|
182
206
|
it("updateRegistryFromAvailableModels ignores unknown bare ids", () => {
|
|
183
207
|
updateRegistryFromAvailableModels(["custom-model-1", "another-custom-model"]);
|
|
184
208
|
|
|
185
|
-
expect(resolveModel("openai/gpt-latest")).toBe("openai/gpt-
|
|
186
|
-
expect(resolveModel("anthropic/claude-opus-latest")).toBe("anthropic/claude-opus-
|
|
209
|
+
expect(resolveModel("openai/gpt-latest")).toBe("openai/gpt-latest");
|
|
210
|
+
expect(resolveModel("anthropic/claude-opus-latest")).toBe("anthropic/claude-opus-latest");
|
|
187
211
|
});
|
|
188
212
|
|
|
189
213
|
it("updateRegistryFromAvailableModels chooses highest among flant openai versions", () => {
|
|
@@ -197,16 +221,25 @@ describe("model-registry", () => {
|
|
|
197
221
|
});
|
|
198
222
|
|
|
199
223
|
it("getAllAliases returns a copy", () => {
|
|
224
|
+
updateRegistryFromAvailableModels(["openai/gpt-5.4"]);
|
|
200
225
|
const aliases = getAllAliases();
|
|
201
226
|
aliases["openai/gpt-latest"] = "openai/gpt-0.0";
|
|
202
227
|
|
|
203
228
|
expect(resolveModel("openai/gpt-latest")).toBe("openai/gpt-5.4");
|
|
204
229
|
});
|
|
205
230
|
|
|
206
|
-
it("getAllAliases contains
|
|
231
|
+
it("getAllAliases contains native-latest identity mappings", () => {
|
|
232
|
+
const aliases = getAllAliases();
|
|
233
|
+
|
|
234
|
+
expect(aliases["anthropic/claude-opus-latest"]).toBe("anthropic/claude-opus-latest");
|
|
235
|
+
expect(aliases["anthropic/claude-sonnet-latest"]).toBe("anthropic/claude-sonnet-latest");
|
|
236
|
+
expect(aliases["anthropic/claude-haiku-latest"]).toBe("anthropic/claude-haiku-latest");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("getAllAliases contains resolved aliases after updateRegistry", () => {
|
|
240
|
+
updateRegistryFromAvailableModels(["grok-4"]);
|
|
207
241
|
const aliases = getAllAliases();
|
|
208
242
|
|
|
209
|
-
expect(aliases["anthropic/claude-opus-latest"]).toBe("anthropic/claude-opus-4-6");
|
|
210
243
|
expect(aliases["pp-flant-openai/grok-latest"]).toBe("pp-flant-openai/grok-4");
|
|
211
244
|
});
|
|
212
245
|
|
|
@@ -235,4 +268,35 @@ describe("model-registry", () => {
|
|
|
235
268
|
expect(families.find((f) => f.family === "haiku")).toMatchObject({ vendor: "anthropic", tier: "stupid" });
|
|
236
269
|
expect(families.find((f) => f.family === "gemini-pro")).toMatchObject({ vendor: "google", tier: "regular" });
|
|
237
270
|
});
|
|
271
|
+
|
|
272
|
+
describe("findLatestFamilyMatch", () => {
|
|
273
|
+
it("returns latest versioned model for a native-latest alias", () => {
|
|
274
|
+
const available = [
|
|
275
|
+
"anthropic/claude-opus-4-0-20250514",
|
|
276
|
+
"anthropic/claude-opus-4-6",
|
|
277
|
+
"anthropic/claude-sonnet-4-6",
|
|
278
|
+
];
|
|
279
|
+
expect(findLatestFamilyMatch("anthropic/claude-opus-latest", available)).toBe("anthropic/claude-opus-4-6");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("returns null for unknown model spec", () => {
|
|
283
|
+
expect(findLatestFamilyMatch("custom/unknown-model", ["anthropic/claude-opus-4-6"])).toBeNull();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("returns null when no candidates match the provider", () => {
|
|
287
|
+
expect(findLatestFamilyMatch("anthropic/claude-opus-latest", ["openai/gpt-5.4"])).toBeNull();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("returns null for spec without provider", () => {
|
|
291
|
+
expect(findLatestFamilyMatch("claude-opus-latest", ["anthropic/claude-opus-4-6"])).toBeNull();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("returns the only candidate when there is one", () => {
|
|
295
|
+
expect(findLatestFamilyMatch("openai/gpt-latest", ["openai/gpt-5.4"])).toBe("openai/gpt-5.4");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("returns null for empty available list", () => {
|
|
299
|
+
expect(findLatestFamilyMatch("anthropic/claude-opus-latest", [])).toBeNull();
|
|
300
|
+
});
|
|
301
|
+
});
|
|
238
302
|
});
|
|
@@ -11,7 +11,7 @@ export interface ModelInfo {
|
|
|
11
11
|
displayName: string;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
type ProviderPrefix = "anthropic" | "openai" | "google" | "deepseek" | "x-ai" | "qwen" | "pp-flant-anthropic" | "pp-flant-openai";
|
|
14
|
+
type ProviderPrefix = "anthropic" | "openai" | "google" | "deepseek" | "x-ai" | "qwen" | "pp-flant-anthropic" | "pp-flant-anthropic-sub" | "pp-flant-openai";
|
|
15
15
|
type KnownVendor = "anthropic" | "openai" | "google" | "deepseek" | "xai" | "qwen";
|
|
16
16
|
type KnownFamily = "opus" | "sonnet" | "haiku" | "gpt" | "gpt-mini" | "gemini-pro" | "gemini-flash" | "deepseek" | "grok" | "qwen";
|
|
17
17
|
type KnownTier = "stupid" | "regular" | "smart" | "xsmart";
|
|
@@ -24,6 +24,7 @@ export interface ModelFamilyDefinition {
|
|
|
24
24
|
patterns: RegExp[];
|
|
25
25
|
aliasTemplate: string;
|
|
26
26
|
providers: ProviderPrefix[];
|
|
27
|
+
nativeLatestProviders?: ProviderPrefix[];
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export interface ModelFamilyInfo {
|
|
@@ -41,27 +42,30 @@ export const MODEL_FAMILIES: ModelFamilyDefinition[] = [
|
|
|
41
42
|
family: "opus",
|
|
42
43
|
tier: "smart",
|
|
43
44
|
displayName: "Claude Opus",
|
|
44
|
-
patterns: [/^(anthropic|pp-flant-anthropic)\/claude-opus-[a-z0-9.-]+$/],
|
|
45
|
+
patterns: [/^(anthropic|pp-flant-anthropic)\/claude-opus-[a-z0-9.-]+$/, /^pp-flant-anthropic-sub\/sub\/claude-opus-[a-z0-9.-]+$/],
|
|
45
46
|
aliasTemplate: "claude-opus-latest",
|
|
46
|
-
providers: ["anthropic", "pp-flant-anthropic"],
|
|
47
|
+
providers: ["anthropic", "pp-flant-anthropic", "pp-flant-anthropic-sub"],
|
|
48
|
+
nativeLatestProviders: ["anthropic"],
|
|
47
49
|
},
|
|
48
50
|
{
|
|
49
51
|
vendor: "anthropic",
|
|
50
52
|
family: "sonnet",
|
|
51
53
|
tier: "regular",
|
|
52
54
|
displayName: "Claude Sonnet",
|
|
53
|
-
patterns: [/^(anthropic|pp-flant-anthropic)\/claude-sonnet-[a-z0-9.-]+$/],
|
|
55
|
+
patterns: [/^(anthropic|pp-flant-anthropic)\/claude-sonnet-[a-z0-9.-]+$/, /^pp-flant-anthropic-sub\/sub\/claude-sonnet-[a-z0-9.-]+$/],
|
|
54
56
|
aliasTemplate: "claude-sonnet-latest",
|
|
55
|
-
providers: ["anthropic", "pp-flant-anthropic"],
|
|
57
|
+
providers: ["anthropic", "pp-flant-anthropic", "pp-flant-anthropic-sub"],
|
|
58
|
+
nativeLatestProviders: ["anthropic"],
|
|
56
59
|
},
|
|
57
60
|
{
|
|
58
61
|
vendor: "anthropic",
|
|
59
62
|
family: "haiku",
|
|
60
63
|
tier: "stupid",
|
|
61
64
|
displayName: "Claude Haiku",
|
|
62
|
-
patterns: [/^(anthropic|pp-flant-anthropic)\/claude-haiku-[a-z0-9.-]+$/],
|
|
65
|
+
patterns: [/^(anthropic|pp-flant-anthropic)\/claude-haiku-[a-z0-9.-]+$/, /^pp-flant-anthropic-sub\/sub\/claude-haiku-[a-z0-9.-]+$/],
|
|
63
66
|
aliasTemplate: "claude-haiku-latest",
|
|
64
|
-
providers: ["anthropic", "pp-flant-anthropic"],
|
|
67
|
+
providers: ["anthropic", "pp-flant-anthropic", "pp-flant-anthropic-sub"],
|
|
68
|
+
nativeLatestProviders: ["anthropic"],
|
|
65
69
|
},
|
|
66
70
|
{
|
|
67
71
|
vendor: "openai",
|
|
@@ -137,30 +141,20 @@ export const MODEL_FAMILIES: ModelFamilyDefinition[] = [
|
|
|
137
141
|
},
|
|
138
142
|
];
|
|
139
143
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
"pp-flant-anthropic/claude-sonnet-latest": "pp-flant-anthropic/claude-sonnet-4-6",
|
|
153
|
-
"pp-flant-anthropic/claude-haiku-latest": "pp-flant-anthropic/claude-haiku-3-5",
|
|
154
|
-
"pp-flant-openai/gpt-latest": "pp-flant-openai/gpt-5.4",
|
|
155
|
-
"pp-flant-openai/gpt-mini-latest": "pp-flant-openai/gpt-5.4-mini",
|
|
156
|
-
"pp-flant-openai/gemini-pro-latest": "pp-flant-openai/gemini-3.1-pro",
|
|
157
|
-
"pp-flant-openai/gemini-flash-latest": "pp-flant-openai/gemini-3.1-flash",
|
|
158
|
-
"pp-flant-openai/deepseek-latest": "pp-flant-openai/deepseek-v3",
|
|
159
|
-
"pp-flant-openai/grok-latest": "pp-flant-openai/grok-4",
|
|
160
|
-
"pp-flant-openai/qwen-latest": "pp-flant-openai/qwen3-coder",
|
|
161
|
-
};
|
|
144
|
+
function buildNativeLatestAliases(): Record<string, string> {
|
|
145
|
+
const map: Record<string, string> = {};
|
|
146
|
+
for (const family of MODEL_FAMILIES) {
|
|
147
|
+
const nativeSet = new Set(family.nativeLatestProviders ?? []);
|
|
148
|
+
for (const provider of family.providers) {
|
|
149
|
+
if (!nativeSet.has(provider)) continue;
|
|
150
|
+
const alias = toAlias(provider, family.aliasTemplate);
|
|
151
|
+
map[alias] = alias;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return map;
|
|
155
|
+
}
|
|
162
156
|
|
|
163
|
-
let aliasMap: Record<string, string> =
|
|
157
|
+
let aliasMap: Record<string, string> = buildNativeLatestAliases();
|
|
164
158
|
|
|
165
159
|
function pickLatest(models: string[]): string | null {
|
|
166
160
|
if (models.length === 0) return null;
|
|
@@ -244,11 +238,13 @@ export function getModelInfo(modelId: string): ModelInfo {
|
|
|
244
238
|
export function updateRegistryFromAvailableModels(availableModels: string[]): void {
|
|
245
239
|
const log = getLogger();
|
|
246
240
|
const normalizedModels = collectNormalizedModels(availableModels).filter((modelId) => !modelId.endsWith("-latest"));
|
|
247
|
-
const nextAliasMap: Record<string, string> =
|
|
241
|
+
const nextAliasMap: Record<string, string> = buildNativeLatestAliases();
|
|
248
242
|
|
|
249
243
|
let updatedCount = 0;
|
|
250
244
|
for (const family of MODEL_FAMILIES) {
|
|
245
|
+
const nativeSet = new Set(family.nativeLatestProviders ?? []);
|
|
251
246
|
for (const provider of family.providers) {
|
|
247
|
+
if (nativeSet.has(provider)) continue;
|
|
252
248
|
const alias = toAlias(provider, family.aliasTemplate);
|
|
253
249
|
const candidates = normalizedModels.filter((modelId) => {
|
|
254
250
|
if (!modelId.startsWith(`${provider}/`)) return false;
|
|
@@ -257,7 +253,7 @@ export function updateRegistryFromAvailableModels(availableModels: string[]): vo
|
|
|
257
253
|
const latest = pickLatest(candidates);
|
|
258
254
|
if (latest) {
|
|
259
255
|
nextAliasMap[alias] = latest;
|
|
260
|
-
|
|
256
|
+
updatedCount++;
|
|
261
257
|
}
|
|
262
258
|
}
|
|
263
259
|
}
|
|
@@ -270,6 +266,26 @@ export function getAllAliases(): Record<string, string> {
|
|
|
270
266
|
return { ...aliasMap };
|
|
271
267
|
}
|
|
272
268
|
|
|
269
|
+
export function findLatestFamilyMatch(
|
|
270
|
+
modelSpec: string,
|
|
271
|
+
availableSpecs: string[],
|
|
272
|
+
): string | null {
|
|
273
|
+
const family = findFamily(modelSpec);
|
|
274
|
+
if (!family) return null;
|
|
275
|
+
|
|
276
|
+
const slashIdx = modelSpec.indexOf("/");
|
|
277
|
+
if (slashIdx === -1) return null;
|
|
278
|
+
const requestedProvider = modelSpec.substring(0, slashIdx).trim().toLowerCase();
|
|
279
|
+
|
|
280
|
+
const candidates = availableSpecs.filter((spec) => {
|
|
281
|
+
const specLower = spec.toLowerCase();
|
|
282
|
+
if (!specLower.startsWith(`${requestedProvider}/`)) return false;
|
|
283
|
+
return family.patterns.some((p) => p.test(specLower));
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
return pickLatest(candidates);
|
|
287
|
+
}
|
|
288
|
+
|
|
273
289
|
export function getModelFamilies(): ModelFamilyInfo[] {
|
|
274
290
|
return MODEL_FAMILIES.map((family) => ({
|
|
275
291
|
vendor: family.vendor,
|
|
@@ -24,7 +24,7 @@ import { registerAgentDefinitions, unregisterAgentDefinitions } from "./agents/r
|
|
|
24
24
|
import { createExploreAgent } from "./agents/explore.js";
|
|
25
25
|
import { createLibrarianAgent } from "./agents/librarian.js";
|
|
26
26
|
import { createTaskAgent } from "./agents/task.js";
|
|
27
|
-
import { resolveModel, getModelInfo } from "./model-registry.js";
|
|
27
|
+
import { resolveModel, getModelInfo, findLatestFamilyMatch } from "./model-registry.js";
|
|
28
28
|
import { buildRepoContext } from "./agents/repo-context.js";
|
|
29
29
|
import { getLogger, addTaskDestination, removeTaskDestination, setLogLevel } from "./log.js";
|
|
30
30
|
import { handleSpawnResult } from "./spawn-cleanup.js";
|
|
@@ -175,6 +175,16 @@ export class Orchestrator {
|
|
|
175
175
|
(m) => m.provider.toLowerCase() === provider && m.id.toLowerCase() === modelId,
|
|
176
176
|
);
|
|
177
177
|
}
|
|
178
|
+
if (!resolved) {
|
|
179
|
+
const allSpecs = allModels.map((m) => `${m.provider}/${m.id}`);
|
|
180
|
+
const familyMatch = findLatestFamilyMatch(spec, allSpecs);
|
|
181
|
+
if (familyMatch) {
|
|
182
|
+
const fmLower = familyMatch.toLowerCase();
|
|
183
|
+
resolved = allModels.find(
|
|
184
|
+
(m) => `${m.provider.toLowerCase()}/${m.id.toLowerCase()}` === fmLower,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
178
188
|
if (!resolved) {
|
|
179
189
|
const pattern = spec.toLowerCase();
|
|
180
190
|
const matches = allModels.filter(
|
|
@@ -301,7 +311,7 @@ export class Orchestrator {
|
|
|
301
311
|
|
|
302
312
|
if (this.active.state.reviewCycle?.step === "apply_feedback") {
|
|
303
313
|
const pass = this.active.state.reviewCycle.pass;
|
|
304
|
-
return reviewCycleSystemPrompt(this.active.dir, pass, this.active.state.phase);
|
|
314
|
+
return reviewCycleSystemPrompt(this.active.dir, pass, this.active.state.phase, mode);
|
|
305
315
|
}
|
|
306
316
|
|
|
307
317
|
switch (this.active.state.phase) {
|
|
@@ -445,6 +455,7 @@ export class Orchestrator {
|
|
|
445
455
|
type === "debug" ? "debug"
|
|
446
456
|
: type === "brainstorm" ? "brainstorm"
|
|
447
457
|
: type === "review" ? "review"
|
|
458
|
+
: type === "quick" ? "quick"
|
|
448
459
|
: "implement"
|
|
449
460
|
];
|
|
450
461
|
const modelOk = await this.switchModel(ctx, modelConfig.model, modelConfig.thinking);
|