@fiale-plus/pi-rogue-bundle 0.1.9 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +24 -13
  2. package/node_modules/@fiale-plus/pi-rogue-advisor/README.md +59 -0
  3. package/node_modules/@fiale-plus/pi-rogue-advisor/advisor/index.ts +1 -0
  4. package/node_modules/@fiale-plus/pi-rogue-advisor/assets/binary-gate-model.json +24026 -0
  5. package/node_modules/@fiale-plus/pi-rogue-advisor/package.json +50 -0
  6. package/node_modules/@fiale-plus/pi-rogue-advisor/skills/advisor/SKILL.md +51 -0
  7. package/node_modules/@fiale-plus/pi-rogue-advisor/src/completions.test.ts +28 -0
  8. package/node_modules/@fiale-plus/pi-rogue-advisor/src/completions.ts +79 -0
  9. package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.test.ts +257 -0
  10. package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.ts +1334 -0
  11. package/node_modules/@fiale-plus/pi-rogue-advisor/src/index.ts +3 -0
  12. package/node_modules/@fiale-plus/pi-rogue-advisor/src/internal.ts +48 -0
  13. package/node_modules/@fiale-plus/pi-rogue-advisor/src/loop-convergence.test.ts +301 -0
  14. package/node_modules/@fiale-plus/pi-rogue-advisor/src/preflight-signals.test.ts +22 -0
  15. package/node_modules/@fiale-plus/pi-rogue-advisor/src/preflight-signals.ts +21 -0
  16. package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.test.ts +78 -0
  17. package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.ts +516 -0
  18. package/node_modules/@fiale-plus/pi-rogue-orchestration/README.md +56 -0
  19. package/node_modules/@fiale-plus/pi-rogue-orchestration/orchestration/index.ts +1 -0
  20. package/node_modules/@fiale-plus/pi-rogue-orchestration/package.json +44 -0
  21. package/node_modules/@fiale-plus/pi-rogue-orchestration/skills/orchestration/SKILL.md +44 -0
  22. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.test.ts +142 -0
  23. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.ts +96 -0
  24. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch-state.ts +70 -0
  25. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch.test.ts +143 -0
  26. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch.ts +139 -0
  27. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/completions.test.ts +23 -0
  28. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/completions.ts +53 -0
  29. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/extension.ts +23 -0
  30. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal-resolution.ts +36 -0
  31. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.test.ts +182 -0
  32. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.ts +232 -0
  33. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/index.ts +1 -0
  34. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/internal.ts +98 -0
  35. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/loop.ts +274 -0
  36. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.test.ts +35 -0
  37. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.ts +145 -0
  38. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/state.ts +24 -0
  39. package/package.json +10 -2
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@fiale-plus/pi-rogue-advisor",
3
+ "version": "0.1.5",
4
+ "description": "Pi-Rogue advisor extension for Pi — multi-model support, SOTA model suggestion, cache-aware session advisory. (Releases paused; consolidated into @fiale-plus/pi-rogue-bundle. Install the bundle instead.)",
5
+ "private": true,
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/fiale-plus/pi-rogue.git"
11
+ },
12
+ "keywords": [
13
+ "pi-package"
14
+ ],
15
+ "scripts": {
16
+ "check": "tsc -p ../../tsconfig.json --noEmit",
17
+ "test": "cd ../.. && vitest run packages/advisor/src/*.test.ts"
18
+ },
19
+ "main": "./src/index.ts",
20
+ "exports": {
21
+ ".": "./src/index.ts"
22
+ },
23
+ "pi": {
24
+ "extensions": [
25
+ "./advisor"
26
+ ],
27
+ "skills": [
28
+ "./skills"
29
+ ]
30
+ },
31
+ "peerDependencies": {
32
+ "@earendil-works/pi-coding-agent": "^0.74.0",
33
+ "@earendil-works/pi-ai": "^0.74.0",
34
+ "@earendil-works/pi-tui": "^0.74.0"
35
+ },
36
+ "dependencies": {
37
+ "typebox": "^1.1.24"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "files": [
43
+ "advisor",
44
+ "src",
45
+ "skills",
46
+ "assets",
47
+ "README.md",
48
+ "package.json"
49
+ ]
50
+ }
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: advisor
3
+ description: Zero-config strategic advisor for Pi. Auto-detects best model, phase-aware routing, preflight + post-review + cache. Use for architecture, tradeoffs, planning.
4
+ ---
5
+
6
+ # Pi-Rogue Advisor
7
+
8
+ Use this skill for non-trivial decisions before/after significant edits.
9
+
10
+ ## Quick start
11
+
12
+ - `/pi-rogue` — open cockpit and command pointers
13
+ - `/advisor status` — show current advisor settings and model route
14
+ - `/advisor <question>` — ask immediate advice
15
+ - Check-ins are lifecycle-managed by orchestration, not by the advisor command surface
16
+
17
+ ## Command surface
18
+
19
+ | Command | What it does |
20
+ |---|---|
21
+ | `/advisor` | Show status + config summary |
22
+ | `/advisor status` | Same as `/advisor` |
23
+ | `/advisor on` | Enable auto mode |
24
+ | `/advisor off` | Disable advisor |
25
+ | `/advisor mode auto\|manual\|off` | Control when advisor auto-runs |
26
+ | `/advisor review light\|strict\|off` | Set review threshold |
27
+ | `/advisor config` | Dump full config |
28
+ | `/advisor pause <N>` | Pause advisor auto-runs for the next N turns |
29
+ | `/advisor unpause` | Resume advisor auto-runs immediately |
30
+ | `/advisor model <provider/model>` | Pin model explicitly |
31
+ | `/advisor <question>` | Run one advisory response |
32
+
33
+ ## Routing and safety
34
+
35
+ - Preflight is heuristics + quick local gate first.
36
+ - Review runs after edits and/or at completion points by policy.
37
+ - No standalone check-in command: check-ins are triggered from goal/loop orchestration cadence (not from advisor internals), using higher/advanced advisor models first with regular model fallback enabled by default.
38
+
39
+ ## Keep scope clear
40
+
41
+ - Successful `on_track` review verdicts are recorded silently instead of displayed as follow-up messages.
42
+ - Goal/loop-managed check-ins gate on session activity and `checkinIntervalMinutes`, avoid overlapping calls, and use higher/advanced advisor models first with regular model fallback enabled by default.
43
+ - The advisor surface is separate from orchestration (`goal`/`loop`/`autoresearch`) and intentionally stays a small command set with explicit entries above.
44
+
45
+ ## Defaults
46
+
47
+ - `mode: auto`
48
+ - `review: light`
49
+ - `checkins: off` by default; orchestration owns cadence and enables them when a goal or loop is active
50
+ - `checkinIntervalMinutes: 30`
51
+ - `model: auto`
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { advisorArgumentCompletions, piRogueArgumentCompletions } from "./completions.js";
3
+
4
+ describe("advisor completions", () => {
5
+ it("offers top-level advisor continuations", () => {
6
+ const values = advisorArgumentCompletions("")?.map((i) => i.value);
7
+ expect(values).toEqual(expect.arrayContaining(["status", "config", "model", "review", "pause", "unpause"]));
8
+ expect(values).not.toContain("checkins");
9
+ });
10
+
11
+ it("offers nested review choices", () => {
12
+ const values = advisorArgumentCompletions("review ")?.map((i) => i.value);
13
+ expect(values).toEqual(["light", "strict", "off"]);
14
+ });
15
+
16
+ });
17
+
18
+ describe("pi-rogue cockpit completions", () => {
19
+ it("offers umbrella sections", () => {
20
+ const values = piRogueArgumentCompletions("")?.map((i) => i.value);
21
+ expect(values).toEqual(expect.arrayContaining(["status", "advisor", "orchestration", "help"]));
22
+ });
23
+
24
+ it("fans out to orchestration shortcuts", () => {
25
+ const values = piRogueArgumentCompletions("orchestration ")?.map((i) => i.value);
26
+ expect(values).toEqual(expect.arrayContaining(["goal", "loop", "autoresearch", "autoresearch-lab"]));
27
+ });
28
+ });
@@ -0,0 +1,79 @@
1
+ type CompletionItem = { value: string; label: string; description?: string };
2
+
3
+ function item(value: string, description?: string): CompletionItem {
4
+ return { value, label: value, ...(description ? { description } : {}) };
5
+ }
6
+
7
+ function complete(values: Array<[string, string?]>, prefix: string): CompletionItem[] | null {
8
+ const q = prefix.trimStart().toLowerCase();
9
+ const items = values.map(([value, description]) => item(value, description));
10
+ const filtered = q
11
+ ? items.filter((i) => i.value.startsWith(q))
12
+ : items;
13
+ return filtered.length > 0 ? filtered : null;
14
+ }
15
+
16
+ function completionsForPrefix(prefix: string, topLevel: Array<[string, string?]>, nested: Record<string, Array<[string, string?]>>): CompletionItem[] | null {
17
+ const q = prefix.trimStart().toLowerCase();
18
+ if (!q) return complete(topLevel, q);
19
+
20
+ const [head, ...rest] = q.split(/\s+/);
21
+ if (!head) return complete(topLevel, q);
22
+
23
+ if (rest.length === 0) {
24
+ const top = complete(topLevel, head);
25
+ if (top) return top;
26
+ }
27
+
28
+ const next = nested[head];
29
+ if (next) {
30
+ return complete(next, rest.join(" "));
31
+ }
32
+
33
+ return complete(topLevel, q);
34
+ }
35
+
36
+ const advisorTopLevel: Array<[string, string?]> = [
37
+ ["status", "show status and configuration"],
38
+ ["config", "show full config"],
39
+ ["on", "enable auto mode"],
40
+ ["off", "disable advisor"],
41
+ ["mode", "set auto/manual/off"],
42
+ ["review", "set light/strict/off"],
43
+ ["pause", "pause advisor auto-runs for N turns"],
44
+ ["unpause", "resume advisor auto-runs immediately"],
45
+ ["model", "set or inspect model override"],
46
+ ];
47
+
48
+ const advisorNested: Record<string, Array<[string, string?]>> = {
49
+ mode: [["auto"], ["manual"], ["off"]],
50
+ review: [["light"], ["strict"], ["off"]],
51
+ model: [["auto"], ["openai-codex/gpt-5.5"], ["anthropic/claude-opus-4-6"]],
52
+ };
53
+
54
+ const piRogueTopLevel: Array<[string, string?]> = [
55
+ ["status", "show cockpit"],
56
+ ["advisor", "advisor status"],
57
+ ["orchestration", "goal/loop/autoresearch shortcuts"],
58
+ ["help", "show cockpit help"],
59
+ ];
60
+
61
+ const piRogueNested: Record<string, Array<[string, string?]>> = {
62
+ advisor: advisorTopLevel,
63
+ orchestration: [
64
+ ["goal", "goal commands"],
65
+ ["loop", "loop commands"],
66
+ ["autoresearch", "solo research flow"],
67
+ ["autoresearch-lab", "parallel research flow"],
68
+ ["status", "show all surfaces"],
69
+ ],
70
+ help: [["advisor"], ["orchestration"], ["status"]],
71
+ };
72
+
73
+ export function advisorArgumentCompletions(prefix: string): CompletionItem[] | null {
74
+ return completionsForPrefix(prefix, advisorTopLevel, advisorNested);
75
+ }
76
+
77
+ export function piRogueArgumentCompletions(prefix: string): CompletionItem[] | null {
78
+ return completionsForPrefix(prefix, piRogueTopLevel, piRogueNested);
79
+ }
@@ -0,0 +1,257 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { completeSimple } from "@earendil-works/pi-ai";
3
+ import {
4
+ buildAdvisorCheckinPrompt,
5
+ completeWithHigherAdvisorModel,
6
+ completeWithModelFallback,
7
+ contentText,
8
+ normalizeAdvisorConfig,
9
+ shouldRunCheckin,
10
+ type AdvisorConfig,
11
+ } from "./extension.js";
12
+
13
+ vi.mock("@earendil-works/pi-ai", async () => {
14
+ const actual = await vi.importActual<typeof import("@earendil-works/pi-ai")>("@earendil-works/pi-ai");
15
+ return {
16
+ ...actual,
17
+ completeSimple: vi.fn(),
18
+ };
19
+ });
20
+
21
+ function state(overrides: Record<string, unknown> = {}) {
22
+ return {
23
+ turns: 2,
24
+ lastTask: "work on orchestration",
25
+ notes: ["made progress"],
26
+ files: [],
27
+ errors: [],
28
+ advisorCalls: 0,
29
+ cacheHits: 0,
30
+ followUp: "",
31
+ router: {},
32
+ checkin: {},
33
+ ...overrides,
34
+ } as any;
35
+ }
36
+
37
+ describe("AdvisorConfig", () => {
38
+ it("defaults to auto mode, light review, and goal-scoped check-ins off", () => {
39
+ const cfg = normalizeAdvisorConfig({});
40
+ expect(cfg.mode).toBe("auto");
41
+ expect(cfg.review).toBe("light");
42
+ expect(cfg.checkins).toBe("off");
43
+ expect(cfg.checkinIntervalMinutes).toBe(30);
44
+ expect(cfg.model).toBeUndefined();
45
+ });
46
+
47
+ it("accepts all 3 modes", () => {
48
+ for (const mode of ["auto", "manual", "off"] as const) {
49
+ const cfg: AdvisorConfig = { mode, review: "light", checkins: "mid-hour", checkinIntervalMinutes: 30 };
50
+ expect(normalizeAdvisorConfig(cfg).mode).toBe(mode);
51
+ }
52
+ });
53
+
54
+ it("accepts all 3 review levels", () => {
55
+ for (const review of ["light", "strict", "off"] as const) {
56
+ const cfg: AdvisorConfig = { mode: "auto", review, checkins: "mid-hour", checkinIntervalMinutes: 30 };
57
+ expect(normalizeAdvisorConfig(cfg).review).toBe(review);
58
+ }
59
+ });
60
+
61
+ it("bounds check-in intervals", () => {
62
+ expect(normalizeAdvisorConfig({ checkinIntervalMinutes: 1 }).checkinIntervalMinutes).toBe(10);
63
+ expect(normalizeAdvisorConfig({ checkinIntervalMinutes: 999 }).checkinIntervalMinutes).toBe(240);
64
+ });
65
+
66
+ it("accepts optional model override", () => {
67
+ const cfg = normalizeAdvisorConfig({ mode: "auto", review: "light", model: "claude-sonnet-4-6" });
68
+ expect(cfg.model).toBe("claude-sonnet-4-6");
69
+ });
70
+
71
+ it("serializes/deserializes without data loss (JSON round-trip)", () => {
72
+ const original = normalizeAdvisorConfig({ mode: "auto", review: "light", model: "claude-opus-4-6" });
73
+ const json = JSON.stringify(original);
74
+ const parsed = normalizeAdvisorConfig(JSON.parse(json) as AdvisorConfig);
75
+ expect(parsed.mode).toBe("auto");
76
+ expect(parsed.review).toBe("light");
77
+ expect(parsed.checkins).toBe("off");
78
+ expect(parsed.checkinIntervalMinutes).toBe(30);
79
+ expect(parsed.model).toBe("claude-opus-4-6");
80
+ });
81
+ });
82
+
83
+ describe("advisor message extraction", () => {
84
+ it("extracts nested structured content without object string leakage", () => {
85
+ expect(contentText({ content: [{ type: "text", text: "done" }] })).toBe("done");
86
+ expect(contentText([{ type: "toolResult", content: [{ type: "text", text: "ok" }] }])).toBe("ok");
87
+ expect(contentText({ arbitrary: "shape" })).toBe("");
88
+ });
89
+ });
90
+
91
+ describe("mid-hour check-ins", () => {
92
+ it("does not run immediately after session start", () => {
93
+ const cfg = normalizeAdvisorConfig({ checkins: "mid-hour", checkinIntervalMinutes: 30 });
94
+ const startedAt = 1_000;
95
+ const now = startedAt + 5 * 60_000;
96
+ expect(shouldRunCheckin(cfg, state(), now, startedAt)).toBeNull();
97
+ });
98
+
99
+ it("runs after interval when there was new activity", () => {
100
+ const cfg = normalizeAdvisorConfig({ checkins: "mid-hour", checkinIntervalMinutes: 30 });
101
+ const startedAt = 1_000;
102
+ const now = startedAt + 31 * 60_000;
103
+ expect(shouldRunCheckin(cfg, state(), now, startedAt)).toMatch(/mid-hour check-in/);
104
+ });
105
+
106
+ it("does not run without activity since the last check-in", () => {
107
+ const cfg = normalizeAdvisorConfig({ checkins: "mid-hour", checkinIntervalMinutes: 30 });
108
+ const lastAt = new Date(1_000).toISOString();
109
+ const now = 1_000 + 60 * 60_000;
110
+ expect(shouldRunCheckin(cfg, state({ turns: 5, checkin: { lastAt, lastTurn: 5 } }), now, 1_000)).toBeNull();
111
+ });
112
+
113
+ it("does not run when check-ins are disabled", () => {
114
+ const cfg = normalizeAdvisorConfig({ checkins: "off" });
115
+ expect(shouldRunCheckin(cfg, state(), 999999, 1)).toBeNull();
116
+ });
117
+
118
+ it("skips check-in while advisor is in temporary pause", () => {
119
+ const cfg = normalizeAdvisorConfig({ checkins: "mid-hour", checkinIntervalMinutes: 30 });
120
+ expect(
121
+ shouldRunCheckin(cfg, state({
122
+ turns: 5,
123
+ advisorPauseUntilTurn: 10,
124
+ }), 2_000_000, 1_000),
125
+ ).toBeNull();
126
+ });
127
+
128
+ it("allows check-in after pause expires", () => {
129
+ const cfg = normalizeAdvisorConfig({ checkins: "mid-hour", checkinIntervalMinutes: 30 });
130
+ const startedAt = 1_000;
131
+ const now = startedAt + 31 * 60_000;
132
+ expect(
133
+ shouldRunCheckin(cfg, state({
134
+ turns: 12,
135
+ lastTask: "work",
136
+ notes: ["note"],
137
+ advisorPauseUntilTurn: 10,
138
+ }), now, startedAt),
139
+ ).toMatch(/mid-hour check-in/);
140
+ });
141
+
142
+ it("keeps loop-triggered check-ins bounded by the minute interval", () => {
143
+ const cfg = normalizeAdvisorConfig({ checkins: "mid-hour", checkinIntervalMinutes: 30 });
144
+ const startedAt = 1_000;
145
+ const now = startedAt + 5 * 60_000;
146
+ expect(
147
+ shouldRunCheckin(cfg, state({
148
+ turns: 5,
149
+ checkin: { lastAt: new Date(startedAt).toISOString(), lastTurn: 3 },
150
+ lastTask: "work",
151
+ notes: ["note"],
152
+ }), now, startedAt),
153
+ ).toBeNull();
154
+ });
155
+
156
+ it("flushes queued check-in regardless of turn delta", () => {
157
+ const cfg = normalizeAdvisorConfig({ checkins: "mid-hour", checkinIntervalMinutes: 30 });
158
+ expect(
159
+ shouldRunCheckin(cfg, state({
160
+ checkin: {
161
+ queued: true,
162
+ queuedReason: "queued mid-session check-in",
163
+ },
164
+ })),
165
+ ).toBe("queued mid-session check-in");
166
+ });
167
+
168
+ it("keeps check-in guidance anchored to the active goal", () => {
169
+ const prompt = buildAdvisorCheckinPrompt(
170
+ "loop_tick",
171
+ [
172
+ "Orchestration:",
173
+ "- Goal: active — Autoresearch: solve advisor weaknesses",
174
+ "- Autoresearch: active — solve advisor weaknesses; cycles=1",
175
+ "- Loop: active every 5m — Run one autoresearch cycle toward the active goal.",
176
+ ].join("\n"),
177
+ "Task: solve advisor weaknesses\nNotes:\n- found shallow mid-hour feedback",
178
+ );
179
+
180
+ expect(prompt).toContain("alignment reviewer");
181
+ expect(prompt).toContain("Do not create a new task");
182
+ expect(prompt).toContain("preserve its research question");
183
+ expect(prompt).toContain("solving the named weakness");
184
+ expect(prompt).toContain("Nudge: <one concrete next action that continues the active goal>");
185
+ expect(prompt).toContain("found shallow mid-hour feedback");
186
+ });
187
+ });
188
+
189
+
190
+ describe("advisor completion fallback behavior", () => {
191
+ function mkCtx(allowHighTier: boolean, includeRegular = true) {
192
+ const high = { id: "openai-codex/gpt-5.5", provider: "openai-codex", input: ["text"] };
193
+ const regular = { id: "provider/text-light", provider: "provider", input: ["text"] };
194
+ return {
195
+ modelRegistry: {
196
+ find: (_provider: string, model: string) => {
197
+ if (!allowHighTier) return null;
198
+ if (_provider === "openai-codex" && model === "gpt-5.5") return high;
199
+ if (_provider === "anthropic" && model === "claude-opus-4-6") return { ...high, id: "anthropic/claude-opus-4-6" };
200
+ if (_provider === "anthropic" && model === "claude-sonnet-4-6") return { ...high, id: "anthropic/claude-sonnet-4-6" };
201
+ if (_provider === "openai-codex" && model === "gpt-5.4-mini") return { ...high, id: "openai-codex/gpt-5.4-mini" };
202
+ return null;
203
+ },
204
+ getAvailable: () => (includeRegular ? [regular] : []),
205
+ getApiKeyAndHeaders: async (_model: unknown) => ({ ok: true, apiKey: "k", headers: {} }),
206
+ },
207
+ } as any;
208
+ }
209
+
210
+ it("uses high/advanced models first for check-in completion", async () => {
211
+ const completeSimpleMock = vi.mocked(completeSimple as any);
212
+ completeSimpleMock.mockReset();
213
+ completeSimpleMock.mockResolvedValue({ content: [{ type: "text", text: "ok" }] });
214
+
215
+ const cfg = normalizeAdvisorConfig({ mode: "auto", review: "light" });
216
+ const result = await completeWithHigherAdvisorModel(mkCtx(true, true), cfg, "system", [{ role: "user", content: "x" }], { maxTokens: 128, reasoning: "low" as const });
217
+
218
+ expect(result).not.toBeNull();
219
+ expect(completeSimpleMock).toHaveBeenCalledTimes(1);
220
+ expect(completeSimpleMock.mock.calls[0]?.[0]?.id).toBe("openai-codex/gpt-5.5");
221
+ });
222
+
223
+ it("falls back to regular models for check-in completion when high/advanced are unavailable", async () => {
224
+ const completeSimpleMock = vi.mocked(completeSimple as any);
225
+ completeSimpleMock.mockReset();
226
+ completeSimpleMock.mockResolvedValue({ content: [{ type: "text", text: "ok" }] });
227
+
228
+ const cfg = normalizeAdvisorConfig({ mode: "auto", review: "light" });
229
+ const result = await completeWithHigherAdvisorModel(mkCtx(false, true), cfg, "system", [{ role: "user", content: "x" }], { maxTokens: 128, reasoning: "low" as const });
230
+
231
+ expect(result).not.toBeNull();
232
+ expect(completeSimpleMock).toHaveBeenCalledTimes(1);
233
+ expect(completeSimpleMock.mock.calls[0]?.[0]?.id).toBe("provider/text-light");
234
+ });
235
+
236
+ it("uses regular fallback for non-checkin completion", async () => {
237
+ const completeSimpleMock = vi.mocked(completeSimple as any);
238
+ completeSimpleMock.mockReset();
239
+ completeSimpleMock.mockResolvedValue({ content: [{ type: "text", text: "ok" }] });
240
+
241
+ const cfg = normalizeAdvisorConfig({ mode: "auto", review: "light" });
242
+ const result = await completeWithModelFallback(mkCtx(false), cfg, "system", [{ role: "user", content: "x" }], { maxTokens: 128, reasoning: "low" as const });
243
+
244
+ expect(result).not.toBeNull();
245
+ expect(result?.fallback).toBe(true);
246
+ expect(completeSimpleMock).toHaveBeenCalledTimes(1);
247
+ expect(completeSimpleMock.mock.calls[0]?.[0]?.id).toBe("provider/text-light");
248
+ });
249
+ });
250
+
251
+
252
+ describe("SOTA model suggestions", () => {
253
+ it("includes gpt-5.5 as primary option", () => {
254
+ const cfg = normalizeAdvisorConfig({ mode: "auto", review: "light" });
255
+ expect(cfg.model).toBeUndefined(); // model is optional, auto-detect
256
+ });
257
+ });