@fiale-plus/pi-rogue 0.2.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.
Files changed (68) hide show
  1. package/README.md +50 -0
  2. package/node_modules/@fiale-plus/pi-core/README.md +13 -0
  3. package/node_modules/@fiale-plus/pi-core/package.json +25 -0
  4. package/node_modules/@fiale-plus/pi-core/src/context-broker.ts +109 -0
  5. package/node_modules/@fiale-plus/pi-core/src/index.ts +5 -0
  6. package/node_modules/@fiale-plus/pi-core/src/paths.ts +36 -0
  7. package/node_modules/@fiale-plus/pi-core/src/risk.test.ts +129 -0
  8. package/node_modules/@fiale-plus/pi-core/src/risk.ts +97 -0
  9. package/node_modules/@fiale-plus/pi-core/src/storage.ts +39 -0
  10. package/node_modules/@fiale-plus/pi-core/src/text.test.ts +36 -0
  11. package/node_modules/@fiale-plus/pi-core/src/text.ts +14 -0
  12. package/node_modules/@fiale-plus/pi-rogue-advisor/README.md +59 -0
  13. package/node_modules/@fiale-plus/pi-rogue-advisor/advisor/index.ts +1 -0
  14. package/node_modules/@fiale-plus/pi-rogue-advisor/assets/binary-gate-model.json +24026 -0
  15. package/node_modules/@fiale-plus/pi-rogue-advisor/package.json +50 -0
  16. package/node_modules/@fiale-plus/pi-rogue-advisor/skills/advisor/SKILL.md +51 -0
  17. package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate-features.test.ts +19 -0
  18. package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate-features.ts +248 -0
  19. package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate.test.ts +66 -0
  20. package/node_modules/@fiale-plus/pi-rogue-advisor/src/completions.test.ts +28 -0
  21. package/node_modules/@fiale-plus/pi-rogue-advisor/src/completions.ts +79 -0
  22. package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.test.ts +364 -0
  23. package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.ts +1677 -0
  24. package/node_modules/@fiale-plus/pi-rogue-advisor/src/index.ts +3 -0
  25. package/node_modules/@fiale-plus/pi-rogue-advisor/src/internal.ts +63 -0
  26. package/node_modules/@fiale-plus/pi-rogue-advisor/src/loop-convergence.test.ts +512 -0
  27. package/node_modules/@fiale-plus/pi-rogue-advisor/src/preflight-signals.test.ts +22 -0
  28. package/node_modules/@fiale-plus/pi-rogue-advisor/src/preflight-signals.ts +21 -0
  29. package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.test.ts +126 -0
  30. package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.ts +580 -0
  31. package/node_modules/@fiale-plus/pi-rogue-advisor/src/state-versioning.test.ts +227 -0
  32. package/node_modules/@fiale-plus/pi-rogue-context-broker/README.md +53 -0
  33. package/node_modules/@fiale-plus/pi-rogue-context-broker/package.json +31 -0
  34. package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.test.ts +749 -0
  35. package/node_modules/@fiale-plus/pi-rogue-context-broker/src/extension.ts +818 -0
  36. package/node_modules/@fiale-plus/pi-rogue-context-broker/src/file.ts +191 -0
  37. package/node_modules/@fiale-plus/pi-rogue-context-broker/src/index.test.ts +302 -0
  38. package/node_modules/@fiale-plus/pi-rogue-context-broker/src/index.ts +369 -0
  39. package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.test.ts +122 -0
  40. package/node_modules/@fiale-plus/pi-rogue-context-broker/src/sqlite.ts +561 -0
  41. package/node_modules/@fiale-plus/pi-rogue-orchestration/README.md +56 -0
  42. package/node_modules/@fiale-plus/pi-rogue-orchestration/orchestration/index.ts +1 -0
  43. package/node_modules/@fiale-plus/pi-rogue-orchestration/package.json +44 -0
  44. package/node_modules/@fiale-plus/pi-rogue-orchestration/skills/orchestration/SKILL.md +44 -0
  45. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.test.ts +142 -0
  46. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.ts +102 -0
  47. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch-state.ts +70 -0
  48. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch.test.ts +143 -0
  49. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch.ts +139 -0
  50. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/completions.test.ts +23 -0
  51. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/completions.ts +53 -0
  52. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/extension.ts +23 -0
  53. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal-resolution.ts +36 -0
  54. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.test.ts +182 -0
  55. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.ts +232 -0
  56. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/index.ts +1 -0
  57. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/internal.ts +98 -0
  58. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/loop.ts +274 -0
  59. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.test.ts +35 -0
  60. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.ts +145 -0
  61. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/state.ts +24 -0
  62. package/package.json +51 -0
  63. package/src/context-broker-file.ts +1 -0
  64. package/src/context-broker-sqlite.ts +1 -0
  65. package/src/context-broker.ts +1 -0
  66. package/src/extension.test.ts +68 -0
  67. package/src/extension.ts +27 -0
  68. package/src/index.ts +1 -0
@@ -0,0 +1,364 @@
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
+ parseReviewPayload,
10
+ sanitizeAdvisorText,
11
+ shouldRunCheckin,
12
+ consumeTaskScopedFollowUp,
13
+ isTaskContinuation,
14
+ type AdvisorConfig,
15
+ } from "./extension.js";
16
+
17
+ vi.mock("@earendil-works/pi-ai", async () => {
18
+ const actual = await vi.importActual<typeof import("@earendil-works/pi-ai")>("@earendil-works/pi-ai");
19
+ return {
20
+ ...actual,
21
+ completeSimple: vi.fn(),
22
+ };
23
+ });
24
+
25
+ function state(overrides: Record<string, unknown> = {}) {
26
+ return {
27
+ turns: 2,
28
+ lastTask: "work on orchestration",
29
+ notes: ["made progress"],
30
+ files: [],
31
+ errors: [],
32
+ advisorCalls: 0,
33
+ cacheHits: 0,
34
+ followUp: "",
35
+ router: {},
36
+ checkin: {},
37
+ ...overrides,
38
+ } as any;
39
+ }
40
+
41
+ describe("AdvisorConfig", () => {
42
+ it("defaults to auto mode, light review, and goal-scoped check-ins off", () => {
43
+ const cfg = normalizeAdvisorConfig({});
44
+ expect(cfg.mode).toBe("auto");
45
+ expect(cfg.review).toBe("light");
46
+ expect(cfg.checkins).toBe("off");
47
+ expect(cfg.checkinIntervalMinutes).toBe(30);
48
+ expect(cfg.model).toBeUndefined();
49
+ });
50
+
51
+ it("accepts all 3 modes", () => {
52
+ for (const mode of ["auto", "manual", "off"] as const) {
53
+ const cfg: AdvisorConfig = { mode, review: "light", checkins: "mid-hour", checkinIntervalMinutes: 30 };
54
+ expect(normalizeAdvisorConfig(cfg).mode).toBe(mode);
55
+ }
56
+ });
57
+
58
+ it("accepts all 3 review levels", () => {
59
+ for (const review of ["light", "strict", "off"] as const) {
60
+ const cfg: AdvisorConfig = { mode: "auto", review, checkins: "mid-hour", checkinIntervalMinutes: 30 };
61
+ expect(normalizeAdvisorConfig(cfg).review).toBe(review);
62
+ }
63
+ });
64
+
65
+ it("bounds check-in intervals", () => {
66
+ expect(normalizeAdvisorConfig({ checkinIntervalMinutes: 1 }).checkinIntervalMinutes).toBe(10);
67
+ expect(normalizeAdvisorConfig({ checkinIntervalMinutes: 999 }).checkinIntervalMinutes).toBe(240);
68
+ });
69
+
70
+ it("accepts optional model override", () => {
71
+ const cfg = normalizeAdvisorConfig({ mode: "auto", review: "light", model: "claude-sonnet-4-6" });
72
+ expect(cfg.model).toBe("claude-sonnet-4-6");
73
+ });
74
+
75
+ it("serializes/deserializes without data loss (JSON round-trip)", () => {
76
+ const original = normalizeAdvisorConfig({ mode: "auto", review: "light", model: "claude-opus-4-6" });
77
+ const json = JSON.stringify(original);
78
+ const parsed = normalizeAdvisorConfig(JSON.parse(json) as AdvisorConfig);
79
+ expect(parsed.mode).toBe("auto");
80
+ expect(parsed.review).toBe("light");
81
+ expect(parsed.checkins).toBe("off");
82
+ expect(parsed.checkinIntervalMinutes).toBe(30);
83
+ expect(parsed.model).toBe("claude-opus-4-6");
84
+ });
85
+ });
86
+
87
+ describe("advisor message extraction", () => {
88
+ it("extracts nested structured content without object string leakage", () => {
89
+ expect(contentText({ content: [{ type: "text", text: "done" }] })).toBe("done");
90
+ expect(contentText([{ type: "toolResult", content: [{ type: "text", text: "ok" }] }])).toBe("ok");
91
+ expect(contentText({ arbitrary: "shape" })).toBe("");
92
+ });
93
+
94
+ it("redacts transient clipboard image paths from advisor-facing text", () => {
95
+ const text = "see /var/folders/fm/rwczdnws5j58x7kbyn3vcx_h0000gn/T/clipboard-2026-06-04-012248-DEE3A154.png please";
96
+ expect(sanitizeAdvisorText(text)).toBe("see [clipboard image] please");
97
+ expect(contentText({ content: [{ type: "text", text }] })).toBe("see [clipboard image] please");
98
+ });
99
+
100
+ it("does not redact ordinary repo or temp file paths", () => {
101
+ const text = "inspect /Users/pavel/repos/fiale-plus/pi-rogue/packages/advisor/src/extension.ts and /tmp/benchmark-results.json";
102
+ expect(sanitizeAdvisorText(text)).toBe(text);
103
+ });
104
+ });
105
+
106
+ describe("review output schema parsing", () => {
107
+ it("parses normal task correction payload and normalizes task actions", () => {
108
+ const parsed = parseReviewPayload(JSON.stringify({
109
+ task: "restore advisor task preservation",
110
+ verdict: "course_correct",
111
+ task_actions: ["Keep task focused on active objective", "Add routing guardrail"],
112
+ advisory_signals: ["Model can still benefit from extra check on pivot severity"],
113
+ pivot: { recommended: true, blocking: false, rationale: "Could be optimized to a smaller model for throughput" },
114
+ summary: "Task still needs focus guard.",
115
+ reason: "Focus drift risk",
116
+ }), "active fallback task");
117
+
118
+ expect(parsed).not.toBeNull();
119
+ expect(parsed?.verdict).toBe("course_correct");
120
+ expect(parsed?.activeTask).toBe("restore advisor task preservation");
121
+ expect(parsed?.taskActions).toEqual(["Keep task focused on active objective", "Add routing guardrail"]);
122
+ expect(parsed?.advisorySignals).toEqual(["Model can still benefit from extra check on pivot severity"]);
123
+ expect(parsed?.pivot.blocking).toBe(false);
124
+ });
125
+
126
+ it("parses advisory-only signals without task actions", () => {
127
+ const parsed = parseReviewPayload(JSON.stringify({
128
+ task: "complete review loop",
129
+ verdict: "course_correct",
130
+ task_actions: [],
131
+ advisory_signals: ["HF token rotation mention in history is non-actionable here"],
132
+ pivot: { recommended: true, blocking: false, rationale: "None" },
133
+ summary: "No action items",
134
+ reason: "Advisory",
135
+ }), "active fallback task");
136
+
137
+ expect(parsed?.taskActions).toEqual([]);
138
+ expect(parsed?.advisorySignals).toEqual(["HF token rotation mention in history is non-actionable here"]);
139
+ expect(parsed?.pivot.recommended).toBe(true);
140
+ expect(parsed?.pivot.blocking).toBe(false);
141
+ });
142
+
143
+ it("flags blocking pivots only for strict risk reasons", () => {
144
+ const parsed = parseReviewPayload(JSON.stringify({
145
+ task: "complete review loop",
146
+ verdict: "course_correct",
147
+ task_actions: ["Adjust checks"],
148
+ pivot: {
149
+ recommended: true,
150
+ blocking: true,
151
+ rationale: "Security/data loss risk: requested to skip token rotation cleanup while secrets are unchanged",
152
+ },
153
+ }), "active fallback task");
154
+
155
+ expect(parsed?.pivot.recommended).toBe(true);
156
+ expect(parsed?.pivot.blocking).toBe(true);
157
+ });
158
+
159
+ it("keeps non-blocking pivots advisory", () => {
160
+ const parsed = parseReviewPayload(JSON.stringify({
161
+ task: "complete review loop",
162
+ verdict: "course_correct",
163
+ task_actions: ["Adjust checks"],
164
+ pivot: {
165
+ recommended: true,
166
+ blocking: false,
167
+ rationale: "Could switch to a smaller model for faster iteration",
168
+ },
169
+ }), "active fallback task");
170
+
171
+ expect(parsed?.pivot.recommended).toBe(true);
172
+ expect(parsed?.pivot.blocking).toBe(false);
173
+ });
174
+
175
+ it("stale task-change helper aligns follow-up only with matching task", () => {
176
+ const nextTask = "run advisor review on original task";
177
+ const staleState = {
178
+ followUp: "run focused check",
179
+ followUpTask: "run advisor review on original task",
180
+ reviewSignals: [],
181
+ reviewSignalsTask: undefined,
182
+ } as any;
183
+ expect(consumeTaskScopedFollowUp(staleState, nextTask)).toBe("run focused check");
184
+ expect(staleState.followUp).toBe("");
185
+
186
+ const driftState = {
187
+ followUp: "run focused check",
188
+ followUpTask: "rotate hf token for benchmark credentials",
189
+ reviewSignals: ["old signal"],
190
+ reviewSignalsTask: "rotate hf token for benchmark credentials",
191
+ } as any;
192
+ expect(consumeTaskScopedFollowUp(driftState, nextTask)).toBe("");
193
+ expect(driftState.followUp).toBe("");
194
+ expect(isTaskContinuation("run advisor review on original task", "rotate hf token for benchmark credentials")).toBe(false);
195
+ });
196
+ });
197
+
198
+ describe("mid-hour check-ins", () => {
199
+ it("does not run immediately after session start", () => {
200
+ const cfg = normalizeAdvisorConfig({ checkins: "mid-hour", checkinIntervalMinutes: 30 });
201
+ const startedAt = 1_000;
202
+ const now = startedAt + 5 * 60_000;
203
+ expect(shouldRunCheckin(cfg, state(), now, startedAt)).toBeNull();
204
+ });
205
+
206
+ it("runs after interval when there was new activity", () => {
207
+ const cfg = normalizeAdvisorConfig({ checkins: "mid-hour", checkinIntervalMinutes: 30 });
208
+ const startedAt = 1_000;
209
+ const now = startedAt + 31 * 60_000;
210
+ expect(shouldRunCheckin(cfg, state(), now, startedAt)).toMatch(/mid-hour check-in/);
211
+ });
212
+
213
+ it("does not run without activity since the last check-in", () => {
214
+ const cfg = normalizeAdvisorConfig({ checkins: "mid-hour", checkinIntervalMinutes: 30 });
215
+ const lastAt = new Date(1_000).toISOString();
216
+ const now = 1_000 + 60 * 60_000;
217
+ expect(shouldRunCheckin(cfg, state({ turns: 5, checkin: { lastAt, lastTurn: 5 } }), now, 1_000)).toBeNull();
218
+ });
219
+
220
+ it("does not run when check-ins are disabled", () => {
221
+ const cfg = normalizeAdvisorConfig({ checkins: "off" });
222
+ expect(shouldRunCheckin(cfg, state(), 999999, 1)).toBeNull();
223
+ });
224
+
225
+ it("skips check-in while advisor is in temporary pause", () => {
226
+ const cfg = normalizeAdvisorConfig({ checkins: "mid-hour", checkinIntervalMinutes: 30 });
227
+ expect(
228
+ shouldRunCheckin(cfg, state({
229
+ turns: 5,
230
+ advisorPauseUntilTurn: 10,
231
+ }), 2_000_000, 1_000),
232
+ ).toBeNull();
233
+ });
234
+
235
+ it("allows check-in after pause expires", () => {
236
+ const cfg = normalizeAdvisorConfig({ checkins: "mid-hour", checkinIntervalMinutes: 30 });
237
+ const startedAt = 1_000;
238
+ const now = startedAt + 31 * 60_000;
239
+ expect(
240
+ shouldRunCheckin(cfg, state({
241
+ turns: 12,
242
+ lastTask: "work",
243
+ notes: ["note"],
244
+ advisorPauseUntilTurn: 10,
245
+ }), now, startedAt),
246
+ ).toMatch(/mid-hour check-in/);
247
+ });
248
+
249
+ it("keeps loop-triggered check-ins bounded by the minute interval", () => {
250
+ const cfg = normalizeAdvisorConfig({ checkins: "mid-hour", checkinIntervalMinutes: 30 });
251
+ const startedAt = 1_000;
252
+ const now = startedAt + 5 * 60_000;
253
+ expect(
254
+ shouldRunCheckin(cfg, state({
255
+ turns: 5,
256
+ checkin: { lastAt: new Date(startedAt).toISOString(), lastTurn: 3 },
257
+ lastTask: "work",
258
+ notes: ["note"],
259
+ }), now, startedAt),
260
+ ).toBeNull();
261
+ });
262
+
263
+ it("flushes queued check-in regardless of turn delta", () => {
264
+ const cfg = normalizeAdvisorConfig({ checkins: "mid-hour", checkinIntervalMinutes: 30 });
265
+ expect(
266
+ shouldRunCheckin(cfg, state({
267
+ checkin: {
268
+ queued: true,
269
+ queuedReason: "queued mid-session check-in",
270
+ },
271
+ })),
272
+ ).toBe("queued mid-session check-in");
273
+ });
274
+
275
+ it("keeps check-in guidance anchored to the active goal", () => {
276
+ const prompt = buildAdvisorCheckinPrompt(
277
+ "loop_tick",
278
+ [
279
+ "Orchestration:",
280
+ "- Goal: active — Autoresearch: solve advisor weaknesses",
281
+ "- Autoresearch: active — solve advisor weaknesses; cycles=1",
282
+ "- Loop: active every 5m — Run one autoresearch cycle toward the active goal.",
283
+ ].join("\n"),
284
+ "Task: solve advisor weaknesses\nNotes:\n- found shallow mid-hour feedback",
285
+ );
286
+
287
+ expect(prompt).toContain("alignment reviewer");
288
+ expect(prompt).toContain("Do not create a new task");
289
+ expect(prompt).toContain("preserve its research question");
290
+ expect(prompt).toContain("solving the named weakness");
291
+ expect(prompt).toContain("Nudge: <one concrete next action that continues the active goal>");
292
+ expect(prompt).toContain("found shallow mid-hour feedback");
293
+ });
294
+ });
295
+
296
+
297
+ describe("advisor completion fallback behavior", () => {
298
+ function mkCtx(allowHighTier: boolean, includeRegular = true) {
299
+ const high = { id: "openai-codex/gpt-5.5", provider: "openai-codex", input: ["text"] };
300
+ const regular = { id: "provider/text-light", provider: "provider", input: ["text"] };
301
+ return {
302
+ modelRegistry: {
303
+ find: (_provider: string, model: string) => {
304
+ if (!allowHighTier) return null;
305
+ if (_provider === "openai-codex" && model === "gpt-5.5") return high;
306
+ if (_provider === "anthropic" && model === "claude-opus-4-6") return { ...high, id: "anthropic/claude-opus-4-6" };
307
+ if (_provider === "anthropic" && model === "claude-sonnet-4-6") return { ...high, id: "anthropic/claude-sonnet-4-6" };
308
+ if (_provider === "openai-codex" && model === "gpt-5.4-mini") return { ...high, id: "openai-codex/gpt-5.4-mini" };
309
+ return null;
310
+ },
311
+ getAvailable: () => (includeRegular ? [regular] : []),
312
+ getApiKeyAndHeaders: async (_model: unknown) => ({ ok: true, apiKey: "k", headers: {} }),
313
+ },
314
+ } as any;
315
+ }
316
+
317
+ it("uses high/advanced models first for check-in completion", async () => {
318
+ const completeSimpleMock = vi.mocked(completeSimple as any);
319
+ completeSimpleMock.mockReset();
320
+ completeSimpleMock.mockResolvedValue({ content: [{ type: "text", text: "ok" }] });
321
+
322
+ const cfg = normalizeAdvisorConfig({ mode: "auto", review: "light" });
323
+ const result = await completeWithHigherAdvisorModel(mkCtx(true, true), cfg, "system", [{ role: "user", content: "x" }], { maxTokens: 128, reasoning: "low" as const });
324
+
325
+ expect(result).not.toBeNull();
326
+ expect(completeSimpleMock).toHaveBeenCalledTimes(1);
327
+ expect(completeSimpleMock.mock.calls[0]?.[0]?.id).toBe("openai-codex/gpt-5.5");
328
+ });
329
+
330
+ it("falls back to regular models for check-in completion when high/advanced are unavailable", async () => {
331
+ const completeSimpleMock = vi.mocked(completeSimple as any);
332
+ completeSimpleMock.mockReset();
333
+ completeSimpleMock.mockResolvedValue({ content: [{ type: "text", text: "ok" }] });
334
+
335
+ const cfg = normalizeAdvisorConfig({ mode: "auto", review: "light" });
336
+ const result = await completeWithHigherAdvisorModel(mkCtx(false, true), cfg, "system", [{ role: "user", content: "x" }], { maxTokens: 128, reasoning: "low" as const });
337
+
338
+ expect(result).not.toBeNull();
339
+ expect(completeSimpleMock).toHaveBeenCalledTimes(1);
340
+ expect(completeSimpleMock.mock.calls[0]?.[0]?.id).toBe("provider/text-light");
341
+ });
342
+
343
+ it("uses regular fallback for non-checkin completion", async () => {
344
+ const completeSimpleMock = vi.mocked(completeSimple as any);
345
+ completeSimpleMock.mockReset();
346
+ completeSimpleMock.mockResolvedValue({ content: [{ type: "text", text: "ok" }] });
347
+
348
+ const cfg = normalizeAdvisorConfig({ mode: "auto", review: "light" });
349
+ const result = await completeWithModelFallback(mkCtx(false), cfg, "system", [{ role: "user", content: "x" }], { maxTokens: 128, reasoning: "low" as const });
350
+
351
+ expect(result).not.toBeNull();
352
+ expect(result?.fallback).toBe(true);
353
+ expect(completeSimpleMock).toHaveBeenCalledTimes(1);
354
+ expect(completeSimpleMock.mock.calls[0]?.[0]?.id).toBe("provider/text-light");
355
+ });
356
+ });
357
+
358
+
359
+ describe("SOTA model suggestions", () => {
360
+ it("includes gpt-5.5 as primary option", () => {
361
+ const cfg = normalizeAdvisorConfig({ mode: "auto", review: "light" });
362
+ expect(cfg.model).toBeUndefined(); // model is optional, auto-detect
363
+ });
364
+ });