@fiale-plus/pi-rogue-advisor 0.1.7 → 0.1.8

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## What this package is
4
4
 
5
- Strategic advisor for Pi sessions with low-overhead preflight/post-review routing, model auto-detection, session memory, and optional mid-session check-ins.
5
+ Strategic advisor for Pi sessions with low-overhead preflight/post-review routing, model auto-detection, session memory, and orchestration-managed mid-session check-ins.
6
6
 
7
7
  - SOTA-first model fallback: `gpt-5.5`/`claude-opus-4-6`/`claude-sonnet-4-6` where available.
8
8
  - Keeps command-level behavior simple and explicit.
@@ -28,7 +28,6 @@ npm install --workspace packages/advisor
28
28
  | `/advisor off` | Disable advisor |
29
29
  | `/advisor mode auto\|manual\|off` | Change routing behavior |
30
30
  | `/advisor review light\|strict\|off` | Change review strictness |
31
- | `/advisor checkins on\|off\|<minutes>` | Enable/disable low-cost mid-hour check-ins |
32
31
  | `/advisor config` | Show current config |
33
32
  | `/advisor model <provider>/<model>` | Set explicit model override |
34
33
  | `/advisor <question>` | Get one-shot advisory response |
@@ -37,11 +36,11 @@ npm install --workspace packages/advisor
37
36
 
38
37
  - `mode`: `auto`
39
38
  - `review`: `light`
40
- - `checkins`: `off` (orchestration turns them on while a goal/autoresearch flow is active)
39
+ - `checkins`: `off` (orchestration turns them on when a loop is active)
41
40
  - `checkinIntervalMinutes`: `30`
42
41
  - `model`: not set (auto-detected)
43
42
 
44
- Check-ins gate on session activity, are bounded, and avoid overlapping calls. They can still be controlled explicitly with `/advisor checkins on|off|<minutes>`.
43
+ Check-ins gate on session activity, are bounded, avoid overlapping calls, and use higher/advanced advisor models first with regular model fallback enabled by default. They are lifecycle-managed by orchestration: enabling `/loop` enables them, and stopping that loop disables them.
45
44
 
46
45
  ## Stability guarantees
47
46
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiale-plus/pi-rogue-advisor",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Pi-Rogue advisor extension for Pi — multi-model support, SOTA model suggestion, cache-aware session advisory.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -12,7 +12,7 @@ Use this skill for non-trivial decisions before/after significant edits.
12
12
  - `/pi-rogue` — open cockpit and command pointers
13
13
  - `/advisor status` — show current advisor settings and model route
14
14
  - `/advisor <question>` — ask immediate advice
15
- - `/advisor checkins on|off|<minutes>` control low-power check-ins
15
+ - Check-ins are lifecycle-managed by `/loop`, not by the advisor command surface
16
16
 
17
17
  ## Command surface
18
18
 
@@ -24,7 +24,6 @@ Use this skill for non-trivial decisions before/after significant edits.
24
24
  | `/advisor off` | Disable advisor |
25
25
  | `/advisor mode auto\|manual\|off` | Control when advisor auto-runs |
26
26
  | `/advisor review light\|strict\|off` | Set review threshold |
27
- | `/advisor checkins on\|off\|<minutes>` | Configure interval check-ins |
28
27
  | `/advisor config` | Dump full config |
29
28
  | `/advisor model <provider/model>` | Pin model explicitly |
30
29
  | `/advisor <question>` | Run one advisory response |
@@ -33,7 +32,7 @@ Use this skill for non-trivial decisions before/after significant edits.
33
32
 
34
33
  - Preflight is heuristics + quick local gate first.
35
34
  - Review runs after edits and/or at completion points by policy.
36
- - No hidden long-running background daemon: check-ins are interval-gated and lightweight.
35
+ - No standalone check-in command: check-ins are triggered from loop cadence (not from advisor internals), using higher/advanced advisor models first with regular model fallback enabled by default.
37
36
 
38
37
  ## Keep scope clear
39
38
 
@@ -43,6 +42,6 @@ The advisor surface is separate from orchestration (`goal`/`loop`/`autoresearch`
43
42
 
44
43
  - `mode: auto`
45
44
  - `review: light`
46
- - `checkins: off` by default; orchestration enables them while a goal/autoresearch flow is active
45
+ - `checkins: off` by default; loop orchestration owns cadence and enables them when active
47
46
  - `checkinIntervalMinutes: 30`
48
47
  - `model: auto`
@@ -4,7 +4,8 @@ import { advisorArgumentCompletions, piRogueArgumentCompletions } from "./comple
4
4
  describe("advisor completions", () => {
5
5
  it("offers top-level advisor continuations", () => {
6
6
  const values = advisorArgumentCompletions("")?.map((i) => i.value);
7
- expect(values).toEqual(expect.arrayContaining(["status", "config", "checkins", "review"]));
7
+ expect(values).toEqual(expect.arrayContaining(["status", "config", "model", "review"]));
8
+ expect(values).not.toContain("checkins");
8
9
  });
9
10
 
10
11
  it("offers nested review choices", () => {
@@ -12,10 +13,6 @@ describe("advisor completions", () => {
12
13
  expect(values).toEqual(["light", "strict", "off"]);
13
14
  });
14
15
 
15
- it("offers check-in choices", () => {
16
- const values = advisorArgumentCompletions("checkins ")?.map((i) => i.value);
17
- expect(values).toEqual(expect.arrayContaining(["on", "off", "30", "60"]));
18
- });
19
16
  });
20
17
 
21
18
  describe("pi-rogue cockpit completions", () => {
@@ -40,24 +40,19 @@ const advisorTopLevel: Array<[string, string?]> = [
40
40
  ["off", "disable advisor"],
41
41
  ["mode", "set auto/manual/off"],
42
42
  ["review", "set light/strict/off"],
43
- ["checkins", "configure mid-hour check-ins"],
44
- ["checkin", "alias for checkins"],
45
43
  ["model", "set or inspect model override"],
46
44
  ];
47
45
 
48
46
  const advisorNested: Record<string, Array<[string, string?]>> = {
49
47
  mode: [["auto"], ["manual"], ["off"]],
50
48
  review: [["light"], ["strict"], ["off"]],
51
- checkins: [["on"], ["off"], ["10"], ["15"], ["30"], ["60"]],
52
- checkin: [["on"], ["off"], ["10"], ["15"], ["30"], ["60"]],
53
49
  model: [["auto"], ["openai-codex/gpt-5.5"], ["anthropic/claude-opus-4-6"]],
54
50
  };
55
51
 
56
52
  const piRogueTopLevel: Array<[string, string?]> = [
57
53
  ["status", "show cockpit"],
58
- ["advisor", "advisor status and check-ins"],
54
+ ["advisor", "advisor status"],
59
55
  ["orchestration", "goal/loop/autoresearch shortcuts"],
60
- ["checkins", "advisor check-ins"],
61
56
  ["help", "show cockpit help"],
62
57
  ];
63
58
 
@@ -70,8 +65,7 @@ const piRogueNested: Record<string, Array<[string, string?]>> = {
70
65
  ["autoresearch-lab", "parallel research flow"],
71
66
  ["status", "show all surfaces"],
72
67
  ],
73
- checkins: advisorNested.checkins,
74
- help: [["advisor"], ["orchestration"], ["checkins"], ["status"]],
68
+ help: [["advisor"], ["orchestration"], ["status"]],
75
69
  };
76
70
 
77
71
  export function advisorArgumentCompletions(prefix: string): CompletionItem[] | null {
@@ -1,5 +1,14 @@
1
- import { describe, it, expect } from "vitest";
2
- import { normalizeAdvisorConfig, shouldRunCheckin, type AdvisorConfig } from "./extension.js";
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { completeSimple } from "@earendil-works/pi-ai";
3
+ import { normalizeAdvisorConfig, shouldRunCheckin, type AdvisorConfig, completeWithHigherAdvisorModel, completeWithModelFallback } from "./extension.js";
4
+
5
+ vi.mock("@earendil-works/pi-ai", async () => {
6
+ const actual = await vi.importActual<typeof import("@earendil-works/pi-ai")>("@earendil-works/pi-ai");
7
+ return {
8
+ ...actual,
9
+ completeSimple: vi.fn(),
10
+ };
11
+ });
3
12
 
4
13
  function state(overrides: Record<string, unknown> = {}) {
5
14
  return {
@@ -89,6 +98,80 @@ describe("mid-hour check-ins", () => {
89
98
  const cfg = normalizeAdvisorConfig({ checkins: "off" });
90
99
  expect(shouldRunCheckin(cfg, state(), 999999, 1)).toBeNull();
91
100
  });
101
+
102
+ it("flushes queued check-in regardless of turn delta", () => {
103
+ const cfg = normalizeAdvisorConfig({ checkins: "mid-hour", checkinIntervalMinutes: 30 });
104
+ expect(
105
+ shouldRunCheckin(cfg, state({
106
+ checkin: {
107
+ queued: true,
108
+ queuedReason: "queued mid-session check-in",
109
+ },
110
+ })),
111
+ ).toBe("queued mid-session check-in");
112
+ });
113
+ });
114
+
115
+
116
+ describe("advisor completion fallback behavior", () => {
117
+ function mkCtx(allowHighTier: boolean, includeRegular = true) {
118
+ const high = { id: "openai-codex/gpt-5.5", provider: "openai-codex", input: ["text"] };
119
+ const regular = { id: "provider/text-light", provider: "provider", input: ["text"] };
120
+ return {
121
+ modelRegistry: {
122
+ find: (_provider: string, model: string) => {
123
+ if (!allowHighTier) return null;
124
+ if (_provider === "openai-codex" && model === "gpt-5.5") return high;
125
+ if (_provider === "anthropic" && model === "claude-opus-4-6") return { ...high, id: "anthropic/claude-opus-4-6" };
126
+ if (_provider === "anthropic" && model === "claude-sonnet-4-6") return { ...high, id: "anthropic/claude-sonnet-4-6" };
127
+ if (_provider === "openai-codex" && model === "gpt-5.4-mini") return { ...high, id: "openai-codex/gpt-5.4-mini" };
128
+ return null;
129
+ },
130
+ getAvailable: () => (includeRegular ? [regular] : []),
131
+ getApiKeyAndHeaders: async (_model: unknown) => ({ ok: true, apiKey: "k", headers: {} }),
132
+ },
133
+ } as any;
134
+ }
135
+
136
+ it("uses high/advanced models first for check-in completion", async () => {
137
+ const completeSimpleMock = vi.mocked(completeSimple as any);
138
+ completeSimpleMock.mockReset();
139
+ completeSimpleMock.mockResolvedValue({ content: [{ type: "text", text: "ok" }] });
140
+
141
+ const cfg = normalizeAdvisorConfig({ mode: "auto", review: "light" });
142
+ const result = await completeWithHigherAdvisorModel(mkCtx(true, true), cfg, "system", [{ role: "user", content: "x" }], { maxTokens: 128, reasoning: "low" as const });
143
+
144
+ expect(result).not.toBeNull();
145
+ expect(completeSimpleMock).toHaveBeenCalledTimes(1);
146
+ expect(completeSimpleMock.mock.calls[0]?.[0]?.id).toBe("openai-codex/gpt-5.5");
147
+ });
148
+
149
+ it("falls back to regular models for check-in completion when high/advanced are unavailable", async () => {
150
+ const completeSimpleMock = vi.mocked(completeSimple as any);
151
+ completeSimpleMock.mockReset();
152
+ completeSimpleMock.mockResolvedValue({ content: [{ type: "text", text: "ok" }] });
153
+
154
+ const cfg = normalizeAdvisorConfig({ mode: "auto", review: "light" });
155
+ const result = await completeWithHigherAdvisorModel(mkCtx(false, true), cfg, "system", [{ role: "user", content: "x" }], { maxTokens: 128, reasoning: "low" as const });
156
+
157
+ expect(result).not.toBeNull();
158
+ expect(completeSimpleMock).toHaveBeenCalledTimes(1);
159
+ expect(completeSimpleMock.mock.calls[0]?.[0]?.id).toBe("provider/text-light");
160
+ });
161
+
162
+ it("uses regular fallback for non-checkin completion", async () => {
163
+ const completeSimpleMock = vi.mocked(completeSimple as any);
164
+ completeSimpleMock.mockReset();
165
+ completeSimpleMock.mockResolvedValue({ content: [{ type: "text", text: "ok" }] });
166
+
167
+ const cfg = normalizeAdvisorConfig({ mode: "auto", review: "light" });
168
+ const result = await completeWithModelFallback(mkCtx(false), cfg, "system", [{ role: "user", content: "x" }], { maxTokens: 128, reasoning: "low" as const });
169
+
170
+ expect(result).not.toBeNull();
171
+ expect(result?.fallback).toBe(true);
172
+ expect(completeSimpleMock).toHaveBeenCalledTimes(1);
173
+ expect(completeSimpleMock.mock.calls[0]?.[0]?.id).toBe("provider/text-light");
174
+ });
92
175
  });
93
176
 
94
177
 
package/src/extension.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
- import { basename } from "node:path";
2
+ import { homedir } from "node:os";
3
+ import { basename, join } from "node:path";
3
4
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
5
  import { Box, Text } from "@earendil-works/pi-tui";
5
6
  import { completeSimple, type ThinkingLevel } from "@earendil-works/pi-ai";
@@ -31,6 +32,8 @@ export interface AdvisorConfig {
31
32
  checkins: "mid-hour" | "off";
32
33
  /** Minutes between check-ins; bounded and cheap-gated by recent activity. */
33
34
  checkinIntervalMinutes: number;
35
+ /** Optional start time (ms since epoch) for the active check-in stream. */
36
+ checkinStartedAt?: number;
34
37
  /** Optional model override. Auto-detects SOTA (gpt-5.5, claude-opus-4-6…) if unset */
35
38
  model?: string;
36
39
  }
@@ -47,16 +50,14 @@ const STATE_PATH = featureFile("advisor", "state.json");
47
50
  const CACHE_PATH = featureFile("advisor", "cache.json");
48
51
  const CURRENT_PATH = featureFile("advisor", "current.md");
49
52
  const HISTORY_PATH = featureFile("advisor", "history.jsonl");
53
+ const ORCHESTRATION_DIR = join(homedir(), ".pi", "agent", "fiale-plus", "orchestration");
50
54
 
51
55
  const MAX_CACHE = 64;
52
56
  const MAX_NOTES = 12;
53
57
  const MAX_FILES = 8;
54
58
  const MAX_ERRORS = 5;
55
- const CHECKIN_POLL_MS = 5 * 60_000;
56
59
  const MIN_CHECKIN_INTERVAL_MINUTES = 10;
57
60
  const MAX_CHECKIN_INTERVAL_MINUTES = 240;
58
- const checkinTimers = new Map<string, NodeJS.Timeout>();
59
- const checkinStartedAt = new Map<string, number>();
60
61
  const checkinLocks = new Set<string>();
61
62
 
62
63
  // ── SOTA models (ordered by preference) ───────────────────────────────────
@@ -85,6 +86,8 @@ interface SessionState {
85
86
  lastAt?: string;
86
87
  lastTurn?: number;
87
88
  lastReason?: string;
89
+ queued?: boolean;
90
+ queuedReason?: string;
88
91
  };
89
92
  }
90
93
 
@@ -99,7 +102,7 @@ function defaultState(): SessionState {
99
102
  cacheHits: 0,
100
103
  followUp: "",
101
104
  router: {},
102
- checkin: {},
105
+ checkin: { queued: false },
103
106
  };
104
107
  }
105
108
 
@@ -118,11 +121,13 @@ function writeJson(path: string, v: unknown) {
118
121
 
119
122
  export function normalizeAdvisorConfig(raw: Partial<AdvisorConfig> = {}): AdvisorConfig {
120
123
  const interval = Number(raw.checkinIntervalMinutes ?? DEFAULT_CONFIG.checkinIntervalMinutes);
124
+ const startedAt = Number(raw.checkinStartedAt);
121
125
  return {
122
126
  mode: (raw.mode === "manual" || raw.mode === "off") ? raw.mode : "auto",
123
127
  review: (raw.review === "strict" || raw.review === "off") ? raw.review : "light",
124
128
  checkins: raw.checkins === "mid-hour" ? "mid-hour" : DEFAULT_CONFIG.checkins,
125
129
  checkinIntervalMinutes: Math.min(MAX_CHECKIN_INTERVAL_MINUTES, Math.max(MIN_CHECKIN_INTERVAL_MINUTES, Number.isFinite(interval) ? Math.round(interval) : DEFAULT_CONFIG.checkinIntervalMinutes)),
130
+ checkinStartedAt: Number.isFinite(startedAt) ? startedAt : undefined,
126
131
  model: raw.model || undefined,
127
132
  };
128
133
  }
@@ -154,6 +159,8 @@ function loadState(): SessionState {
154
159
  lastAt: raw.checkin?.lastAt,
155
160
  lastTurn: raw.checkin?.lastTurn,
156
161
  lastReason: raw.checkin?.lastReason,
162
+ queued: Boolean(raw.checkin?.queued),
163
+ queuedReason: raw.checkin?.queuedReason,
157
164
  },
158
165
  };
159
166
  }
@@ -299,6 +306,40 @@ function sessionKey(ctx: any): string {
299
306
  return basename(String(sessionFile)).replace(/\.[^.]+$/, "");
300
307
  }
301
308
 
309
+ type OrchestrationSnapshot = {
310
+ goal: string;
311
+ loop: { enabled?: boolean; interval?: string; instruction?: string };
312
+ research: { instruction?: string; interval?: string; cycles?: number; doneAttempts?: number; lastResult?: string };
313
+ };
314
+
315
+ function readOrchestrationSnapshot(ctx: any): OrchestrationSnapshot {
316
+ const dir = join(ORCHESTRATION_DIR, sessionKey(ctx));
317
+ return {
318
+ goal: readText(join(dir, "goal.md")).trim(),
319
+ loop: readJson(join(dir, "loop.json"), {}),
320
+ research: readJson(join(dir, "autoresearch.json"), {}),
321
+ };
322
+ }
323
+
324
+ function orchestrationSnapshotText(ctx: any): string {
325
+ const snapshot = readOrchestrationSnapshot(ctx);
326
+ const goalActive = Boolean(snapshot.goal);
327
+ const loopActive = Boolean(snapshot.loop.enabled && snapshot.loop.instruction);
328
+ const researchActive = Boolean(snapshot.research.instruction);
329
+ const status = goalActive && !loopActive && !researchActive
330
+ ? "setup gap — goal exists but no active autoresearch/loop progression"
331
+ : goalActive
332
+ ? "progression configured"
333
+ : "no active goal";
334
+ return [
335
+ "Orchestration:",
336
+ `- Goal: ${goalActive ? `active — ${truncate(snapshot.goal, 140)}` : "off"}`,
337
+ `- Autoresearch: ${researchActive ? `active — cycles=${snapshot.research.cycles ?? 0}, doneAttempts=${snapshot.research.doneAttempts ?? 0}${snapshot.research.lastResult ? `, last=${snapshot.research.lastResult}` : ""}` : "off"}`,
338
+ `- Loop: ${loopActive ? `active every ${snapshot.loop.interval || "?"} — ${truncate(snapshot.loop.instruction || "", 120)}` : "off"}`,
339
+ `- Status: ${status}`,
340
+ ].join("\n");
341
+ }
342
+
302
343
  function setPiRogueStatus(ctx: any, config = loadConfig(), state = loadState()): void {
303
344
  const normalized = normalizeAdvisorConfig(config);
304
345
  const checkin = normalized.checkins === "off" ? "checkins off" : `checkins ${normalized.checkinIntervalMinutes}m`;
@@ -310,68 +351,101 @@ export function shouldRunCheckin(config: AdvisorConfig, state: SessionState, now
310
351
  const normalized = normalizeAdvisorConfig(config);
311
352
  if (normalized.mode === "off" || normalized.mode === "manual") return null;
312
353
  if (normalized.checkins === "off") return null;
354
+ if (state.checkin.queued) {
355
+ return state.checkin.queuedReason || "Queued mid-session check-in.";
356
+ }
313
357
  if (!state.lastTask && state.notes.length === 0) return null;
314
358
  const lastTurn = state.checkin.lastTurn ?? 0;
315
359
  if (state.turns <= lastTurn) return null;
316
360
  const lastAt = state.checkin.lastAt ? Date.parse(state.checkin.lastAt) : 0;
317
361
  const intervalMs = normalized.checkinIntervalMinutes * 60_000;
318
- const since = lastAt || startedAt;
362
+ const streamStartedAt = Number.isFinite(normalized.checkinStartedAt ?? NaN) ? (normalized.checkinStartedAt as number) : startedAt;
363
+ const since = Math.max(lastAt, streamStartedAt);
319
364
  if (since && now - since < intervalMs) return null;
320
365
  return `mid-hour check-in after ${state.turns - lastTurn} new turn(s)`;
321
366
  }
322
367
 
323
- function stopCheckinTimer(key: string): void {
324
- const timer = checkinTimers.get(key);
325
- if (timer) {
326
- clearInterval(timer);
327
- checkinTimers.delete(key);
368
+
369
+ function isAdvisorIdle(ctx: any): boolean {
370
+ try {
371
+ return typeof ctx?.isIdle === "function" ? ctx.isIdle() : true;
372
+ } catch {
373
+ return true;
328
374
  }
329
375
  }
330
376
 
377
+ export async function requestAdvisorLoopCheckin(pi: ExtensionAPI, ctx: any, source = "loop_tick"): Promise<boolean> {
378
+ return maybeAdvisorCheckin(pi, ctx, source);
379
+ }
380
+
331
381
  async function maybeAdvisorCheckin(pi: ExtensionAPI, ctx: any, source: string): Promise<boolean> {
332
382
  const key = sessionKey(ctx);
333
383
  if (checkinLocks.has(key)) return false;
334
384
 
335
385
  const config = loadConfig();
336
386
  const state = loadState();
337
- const startedAt = checkinStartedAt.get(key) ?? Date.now();
338
- const reason = shouldRunCheckin(config, state, Date.now(), startedAt);
339
- setPiRogueStatus(ctx, config, state);
340
- if (!reason) return false;
387
+ const reason = shouldRunCheckin(config, state, Date.now(), Date.now());
388
+ if (!reason) {
389
+ if (state.checkin.queued) {
390
+ state.checkin.queued = false;
391
+ saveState(state);
392
+ setPiRogueStatus(ctx, config, state);
393
+ }
394
+ return false;
395
+ }
396
+
397
+ if (!isAdvisorIdle(ctx)) {
398
+ if (!state.checkin.queued) {
399
+ state.checkin.queued = true;
400
+ state.checkin.queuedReason = reason;
401
+ saveState(state);
402
+ setPiRogueStatus(ctx, config, state);
403
+ }
404
+ return false;
405
+ }
341
406
 
342
407
  checkinLocks.add(key);
343
408
  try {
344
- const response = await askAdvisor(
345
- pi,
409
+ const completed = await completeWithHigherAdvisorModel(
346
410
  ctx,
347
- `Mid-session check-in (${source}): briefly assess whether the current session is on track, stuck, or missing a higher-leverage next step. Return one concrete nudge.`,
348
- "review",
349
- true,
411
+ config,
412
+ [
413
+ `Mid-session check-in (${source}): briefly assess whether the current session is on track, stuck, or missing a higher-leverage next step.`,
414
+ orchestrationSnapshotText(ctx),
415
+ "If a goal exists but autoresearch/loop progression is off, call out the setup gap. Do not start or change orchestration; return one concrete nudge.",
416
+ ].join("\n\n"),
417
+ [
418
+ {
419
+ role: "user",
420
+ content: [
421
+ `Mid-session check-in (${source}): briefly assess whether the current session is on track, stuck, or missing a higher-leverage next step.`,
422
+ orchestrationSnapshotText(ctx),
423
+ "If a goal exists but autoresearch/loop progression is off, call out the setup gap. Do not start or change orchestration; return one concrete nudge.",
424
+ ].join("\n\n"),
425
+ timestamp: new Date().toISOString(),
426
+ },
427
+ ],
428
+ { maxTokens: 600, reasoning: "medium" as ThinkingLevel },
350
429
  );
351
- if (response.error) return false;
430
+ if (!completed) return false;
352
431
 
353
432
  const next = loadState();
354
- next.checkin = { lastAt: new Date().toISOString(), lastTurn: next.turns, lastReason: reason };
433
+ next.checkin = {
434
+ lastAt: new Date().toISOString(),
435
+ lastTurn: next.turns,
436
+ lastReason: reason,
437
+ queued: false,
438
+ };
355
439
  saveState(next);
356
440
  setPiRogueStatus(ctx, config, next);
357
- sendAdvisorHint(pi, "review", "mid-hour check-in", response.text, [response.text]);
441
+ sendAdvisorHint(pi, "review", "mid-hour check-in", completed.text, [completed.text]);
358
442
  return true;
359
443
  } finally {
360
444
  checkinLocks.delete(key);
361
445
  }
362
446
  }
363
447
 
364
- function syncCheckinTimer(pi: ExtensionAPI, ctx: any): void {
365
- const key = sessionKey(ctx);
366
- stopCheckinTimer(key);
367
- checkinStartedAt.set(key, Date.now());
368
- setPiRogueStatus(ctx);
369
- const config = loadConfig();
370
- if (config.mode === "off" || config.mode === "manual" || config.checkins === "off") return;
371
- checkinTimers.set(key, setInterval(() => { void maybeAdvisorCheckin(pi, ctx, "timer"); }, CHECKIN_POLL_MS));
372
- }
373
-
374
- function piRogueCockpitText(config: AdvisorConfig, state: SessionState, currentNote: string): string {
448
+ function piRogueCockpitText(config: AdvisorConfig, state: SessionState, currentNote: string, orchestration = ""): string {
375
449
  const normalized = normalizeAdvisorConfig(config);
376
450
  return [
377
451
  "☠︎ Pi-Rogue cockpit",
@@ -379,35 +453,95 @@ function piRogueCockpitText(config: AdvisorConfig, state: SessionState, currentN
379
453
  `Mode: ${normalized.mode} | Review: ${normalized.review} | Check-ins: ${normalized.checkins === "off" ? "off" : `${normalized.checkinIntervalMinutes}m`}`,
380
454
  `Turns: ${state.turns} | Advisor calls: ${state.advisorCalls} | Cache hits: ${state.cacheHits}`,
381
455
  state.checkin.lastAt ? `Last check-in: ${new Date(state.checkin.lastAt).toLocaleString()} (${state.checkin.lastReason || "mid-hour"})` : "Last check-in: never",
456
+ state.checkin.queued ? `Queued check-in: ${state.checkin.queuedReason || "due"}` : "",
457
+ orchestration,
382
458
  "",
383
- "Commands: /advisor status · /advisor checkins on|off|<minutes> · /goal · /loop status · /autoresearch status",
384
- ].join("\n");
385
- }
459
+ "Commands: /advisor status · /goal · /loop status · /autoresearch status",
460
+ ].filter(Boolean).join("\n");
461
+ }
462
+
463
+ // ── Model resolution (higher/advanced first, then optional regular fallback) ──
464
+ type ResolvedAdvisorModel = { model: any; auth: any; label: string; fallback?: boolean };
465
+ type ModelResolutionOptions = { allowRegularFallback?: boolean };
466
+
467
+ export async function resolveModelCandidates(ctx: any, config: AdvisorConfig, options: ModelResolutionOptions = {}): Promise<ResolvedAdvisorModel[]> {
468
+ const { allowRegularFallback = true } = options;
469
+ const candidates: ResolvedAdvisorModel[] = [];
470
+ const seen = new Set<string>();
471
+ const add = async (found: any, label: string, fallback = false) => {
472
+ if (!found) return;
473
+ const key = String(found.id || label);
474
+ if (seen.has(key)) return;
475
+ const auth = await ctx.modelRegistry?.getApiKeyAndHeaders(found);
476
+ if (auth?.ok && auth.apiKey) {
477
+ seen.add(key);
478
+ candidates.push({ model: found, auth, label, fallback });
479
+ }
480
+ };
386
481
 
387
- // ── Model resolution (auto-fallback through SOTA chain) ────────────────────
388
- async function resolveModel(ctx: any, config: AdvisorConfig): Promise<{ model: any; auth: any; label: string } | null> {
389
- // Try user's configured model first
482
+ // Try configured higher/advanced advisor model first.
390
483
  if (config.model && config.model.includes("/")) {
391
484
  const [p, ...m] = config.model.split("/");
392
- const found = ctx.modelRegistry?.find(p, m.join("/"));
393
- if (found) {
394
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(found);
395
- if (auth?.ok && auth.apiKey) return { model: found, auth, label: p + "/" + m.join("/") };
396
- }
485
+ await add(ctx.modelRegistry?.find(p, m.join("/")), p + "/" + m.join("/"));
397
486
  }
398
- // Fall through SOTA chain
487
+
488
+ // Fall through SOTA chain.
399
489
  for (const sota of SOTA_CHAIN) {
400
- const found = ctx.modelRegistry?.find(sota.provider, sota.model);
401
- if (!found) continue;
402
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(found);
403
- if (auth?.ok && auth.apiKey) return { model: found, auth, label: sota.label };
490
+ await add(ctx.modelRegistry?.find(sota.provider, sota.model), sota.label);
491
+ }
492
+
493
+ if (allowRegularFallback) {
494
+ // Final fallback: any configured text model, i.e. the regular session-capable model.
495
+ for (const m of (ctx.modelRegistry?.getAvailable() ?? []).filter((model: any) => model.input?.includes?.("text"))) {
496
+ await add(m, m.id || "regular model", true);
497
+ }
498
+ }
499
+
500
+ return candidates;
501
+ }
502
+
503
+ async function resolveModel(ctx: any, config: AdvisorConfig): Promise<ResolvedAdvisorModel | null> {
504
+ return (await resolveModelCandidates(ctx, config))[0] ?? null;
505
+ }
506
+
507
+ export async function completeWithModelFallback(ctx: any, config: AdvisorConfig, systemPrompt: string, messages: any[], options: { maxTokens: number; reasoning: ThinkingLevel }): Promise<{ text: string; model: string; fallback?: boolean } | null> {
508
+ let lastError = "";
509
+ for (const resolved of await resolveModelCandidates(ctx, config)) {
510
+ try {
511
+ const resp = await completeSimple(resolved.model, { systemPrompt, messages }, {
512
+ apiKey: resolved.auth.apiKey,
513
+ headers: resolved.auth.headers,
514
+ maxTokens: options.maxTokens,
515
+ reasoning: options.reasoning,
516
+ });
517
+ return { text: responseText(resp) || "(empty)", model: resolved.label, fallback: resolved.fallback };
518
+ } catch (error) {
519
+ lastError = error instanceof Error ? error.message : String(error);
520
+ }
404
521
  }
405
- // Any text model
406
- const avail = (ctx.modelRegistry?.getAvailable() ?? []).filter((m: any) => m.input?.includes?.("text"));
407
- if (avail.length) {
408
- const m = avail[0];
409
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(m);
410
- if (auth?.ok && auth.apiKey) return { model: m, auth, label: m.id || "unknown" };
522
+ return lastError ? { text: `No advisor/check-in model completed successfully (${lastError}).`, model: "none" } : null;
523
+ }
524
+
525
+ export async function completeWithHigherAdvisorModel(
526
+ ctx: any,
527
+ config: AdvisorConfig,
528
+ systemPrompt: string,
529
+ messages: any[],
530
+ options: { maxTokens: number; reasoning: ThinkingLevel; allowRegularFallback?: boolean },
531
+ ): Promise<{ text: string; model: string } | null> {
532
+ const { allowRegularFallback = true } = options;
533
+ for (const resolved of await resolveModelCandidates(ctx, config, { allowRegularFallback })) {
534
+ try {
535
+ const resp = await completeSimple(resolved.model, { systemPrompt, messages }, {
536
+ apiKey: resolved.auth.apiKey,
537
+ headers: resolved.auth.headers,
538
+ maxTokens: options.maxTokens,
539
+ reasoning: options.reasoning,
540
+ });
541
+ return { text: responseText(resp) || "(empty)", model: resolved.label };
542
+ } catch {
543
+ // keep trying remaining candidates
544
+ }
411
545
  }
412
546
  return null;
413
547
  }
@@ -421,22 +555,17 @@ async function askAdvisor(pi: ExtensionAPI, ctx: any, question: string, scope: s
421
555
  const cache = loadCache();
422
556
  if (cache[ck]) { state.cacheHits++; saveState(state); return { text: cache[ck], cached: true }; }
423
557
 
424
- const resolved = await resolveModel(ctx, config);
425
- if (!resolved) return { text: "No model available. Install one via pi config.", error: "no_model" };
426
-
427
558
  const msgs = [
428
559
  { role: "user", content: [ `Question: ${question}`, scope ? `Scope: ${scope}` : "", includeWork && brief(state) ? `Session:\n${brief(state)}` : "" ].filter(Boolean).join("\n"), timestamp: new Date().toISOString() },
429
560
  ] as any[];
430
561
 
431
- const resp = await completeSimple(resolved.model, { systemPrompt: ADVISOR_SYSTEM, messages: msgs as any }, {
432
- apiKey: resolved.auth.apiKey, headers: resolved.auth.headers,
433
- maxTokens: 600, reasoning: "medium" as ThinkingLevel,
434
- });
435
- const text = responseText(resp) || "(empty)";
562
+ const completed = await completeWithModelFallback(ctx, config, ADVISOR_SYSTEM, msgs, { maxTokens: 600, reasoning: "medium" as ThinkingLevel });
563
+ if (!completed) return { text: "No model available. Install one via pi config.", error: "no_model" };
564
+ const text = completed.text;
436
565
  if (text && text !== "(empty)") { cache[ck] = text; saveCache(cache); }
437
566
  state.advisorCalls++;
438
567
  saveState(state);
439
- return { text, model: resolved.label };
568
+ return { text, model: completed.model, fallback: completed.fallback };
440
569
  }
441
570
 
442
571
  async function doReview(pi: ExtensionAPI, ctx: any, trigger: string, delta: string, meta: { fileChanged: boolean; failed: boolean; isAgentEnd: boolean }) {
@@ -492,8 +621,6 @@ async function doReview(pi: ExtensionAPI, ctx: any, trigger: string, delta: stri
492
621
  const cache = loadCache();
493
622
  if (cache[rk]) return; // already reviewed this
494
623
 
495
- const resolved = await resolveModel(ctx, config);
496
- if (!resolved) return;
497
624
  const msgs = [
498
625
  { role: "user", content: [
499
626
  `Trigger: ${trigger}`,
@@ -504,11 +631,8 @@ async function doReview(pi: ExtensionAPI, ctx: any, trigger: string, delta: stri
504
631
  `Brief:\n${b}`,
505
632
  ].join("\n"), timestamp: new Date().toISOString() },
506
633
  ] as any[];
507
- const resp = await completeSimple(resolved.model, { systemPrompt: REVIEW_SYSTEM, messages: msgs as any }, {
508
- apiKey: resolved.auth.apiKey, headers: resolved.auth.headers,
509
- maxTokens: 400, reasoning: "low" as ThinkingLevel,
510
- });
511
- const raw = responseText(resp);
634
+ const completed = await completeWithModelFallback(ctx, config, REVIEW_SYSTEM, msgs, { maxTokens: 400, reasoning: "low" as ThinkingLevel });
635
+ const raw = completed?.text;
512
636
  if (!raw) return;
513
637
 
514
638
  cache[rk] = raw;
@@ -544,13 +668,15 @@ export function registerAdvisor(pi: ExtensionAPI): void {
544
668
  }
545
669
 
546
670
  pi.on("session_start", (_event, ctx) => {
547
- syncCheckinTimer(pi, ctx);
671
+ const key = sessionKey(ctx);
672
+ checkinLocks.delete(key);
673
+ setPiRogueStatus(ctx, loadConfig(), loadState());
674
+ // No timer is owned by advisor itself anymore; check-ins are triggered
675
+ // from active goal/loop/autoresearch flow progression.
548
676
  });
549
677
 
550
678
  pi.on("session_shutdown", (_event, ctx) => {
551
679
  const key = sessionKey(ctx);
552
- stopCheckinTimer(key);
553
- checkinStartedAt.delete(key);
554
680
  checkinLocks.delete(key);
555
681
  ctx.ui.setStatus("pi-rogue", undefined);
556
682
  });
@@ -654,8 +780,10 @@ export function registerAdvisor(pi: ExtensionAPI): void {
654
780
  // ── Post-review (agent_end) ────────────────────────────────────────────
655
781
  pi.on("agent_end", async (event: any, ctx: any) => {
656
782
  const cfg = loadConfig();
657
- if (cfg.mode === "off" || cfg.review === "off") return;
658
- const state = loadState();
783
+ if (cfg.mode === "off") return;
784
+ void maybeAdvisorCheckin(pi, ctx, "agent_end");
785
+
786
+ if (cfg.review === "off") return;
659
787
  const msgs = (event.messages || []).filter((m: any) => m.role === "assistant" || m.role === "toolResult");
660
788
  const last = msgs[msgs.length - 1];
661
789
  const delta = contentText(last?.content) || "(none)";
@@ -666,7 +794,7 @@ export function registerAdvisor(pi: ExtensionAPI): void {
666
794
 
667
795
  // ── /pi-rogue cockpit ──────────────────────────────────────────────────
668
796
  pi.registerCommand("pi-rogue", {
669
- description: "Show Pi-Rogue cockpit: advisor, check-ins, and orchestration command pointers",
797
+ description: "Show Pi-Rogue cockpit: advisor and orchestration command pointers",
670
798
  getArgumentCompletions: (prefix: string) => piRogueArgumentCompletions(prefix),
671
799
  handler: async (args, ctx) => {
672
800
  const cfg = loadConfig();
@@ -675,7 +803,7 @@ export function registerAdvisor(pi: ExtensionAPI): void {
675
803
  setPiRogueStatus(ctx, cfg, state);
676
804
 
677
805
  if (!arg || arg === "status" || arg === "help") {
678
- ctx.ui.notify(piRogueCockpitText(cfg, state, readText(CURRENT_PATH).trim()), "info");
806
+ ctx.ui.notify(piRogueCockpitText(cfg, state, readText(CURRENT_PATH).trim(), orchestrationSnapshotText(ctx)), "info");
679
807
  return;
680
808
  }
681
809
 
@@ -684,8 +812,9 @@ export function registerAdvisor(pi: ExtensionAPI): void {
684
812
  "Advisor surface:",
685
813
  " /advisor status",
686
814
  " /advisor config",
687
- " /advisor checkins on|off|<minutes>",
688
815
  " /advisor <question>",
816
+ "",
817
+ "Check-ins are orchestration-managed: start /loop to activate them.",
689
818
  ].join("\n"), "info");
690
819
  return;
691
820
  }
@@ -703,13 +832,14 @@ export function registerAdvisor(pi: ExtensionAPI): void {
703
832
 
704
833
  if (arg.startsWith("checkins")) {
705
834
  ctx.ui.notify([
706
- `Advisor check-ins: ${cfg.checkins === "off" ? "off" : `${cfg.checkinIntervalMinutes}m`}`,
707
- "Use /advisor checkins on|off|<minutes> to change it.",
835
+ `Check-ins: ${cfg.checkins === "off" ? "off" : `${cfg.checkinIntervalMinutes}m`}`,
836
+ "Managed by orchestration: /loop activates them; stopping the loop disables them.",
837
+ orchestrationSnapshotText(ctx),
708
838
  ].join("\n"), "info");
709
839
  return;
710
840
  }
711
841
 
712
- ctx.ui.notify(piRogueCockpitText(cfg, state, readText(CURRENT_PATH).trim()), "info");
842
+ ctx.ui.notify(piRogueCockpitText(cfg, state, readText(CURRENT_PATH).trim(), orchestrationSnapshotText(ctx)), "info");
713
843
  },
714
844
  });
715
845
 
@@ -731,22 +861,48 @@ export function registerAdvisor(pi: ExtensionAPI): void {
731
861
  note ? `🧭 ${truncate(note, 200)}` : "",
732
862
  route ? `Router: ${summarizeRoute(route)}${route.safety ? " · safety" : ""}` : "",
733
863
  "",
734
- `Mode: ${cfg.mode} | Review: ${cfg.review} | Check-ins: ${cfg.checkins === "off" ? "off" : `${cfg.checkinIntervalMinutes}m`} | Model: ${resolved?.label || cfg.model || "auto"}`,
864
+ `Mode: ${cfg.mode} | Review: ${cfg.review} | Check-ins: ${cfg.checkins === "off" ? "off" : `${cfg.checkinIntervalMinutes}m`} (orchestration-managed) | Model: ${resolved?.label || cfg.model || "auto"}`,
735
865
  `Turns: ${state.turns} | Calls: ${state.advisorCalls} | Cache hits: ${state.cacheHits}`,
736
866
  state.checkin.lastAt ? `Last check-in: ${new Date(state.checkin.lastAt).toLocaleString()} (${state.checkin.lastReason || "mid-hour"})` : "Last check-in: never",
867
+ state.checkin.queued ? `Queued check-in: ${state.checkin.queuedReason || "due"}` : "",
868
+ orchestrationSnapshotText(ctx),
737
869
  "",
738
- "Commands: /advisor on|off | /advisor status | /advisor checkins on|off|<minutes> | /advisor config | <question>",
870
+ "Commands: /advisor on|off | /advisor status | /advisor config | <question>",
739
871
  "Tip: SOTA models auto-detected. No config needed.",
740
872
  ].filter(Boolean).join("\n"), "info");
741
873
  return;
742
874
  }
743
875
 
744
- if (cmd === "on" && cfg.mode === "off") { const next = { ...cfg, mode: "auto" as const }; saveConfig(next); syncCheckinTimer(pi, ctx); ctx.ui.notify("Advisor enabled (auto mode).", "info"); return; }
745
- if (cmd === "off") { const next = { ...cfg, mode: "off" as const }; saveConfig(next); stopCheckinTimer(sessionKey(ctx)); setPiRogueStatus(ctx, next, state); ctx.ui.notify("Advisor disabled.", "info"); return; }
876
+ if (cmd === "on" && cfg.mode === "off") {
877
+ const next = { ...cfg, mode: "auto" as const };
878
+ saveConfig(next);
879
+ setPiRogueStatus(ctx, next, state);
880
+ ctx.ui.notify("Advisor enabled (auto mode).", "info");
881
+ return;
882
+ }
883
+ if (cmd === "off") {
884
+ const next = { ...cfg, mode: "off" as const };
885
+ saveConfig(next);
886
+ setPiRogueStatus(ctx, next, state);
887
+ ctx.ui.notify("Advisor disabled.", "info");
888
+ return;
889
+ }
746
890
  if (cmd === "mode") {
747
891
  const v = rest[0];
748
- if (v === "auto" || v === "manual") { const next: AdvisorConfig = { ...cfg, mode: v }; saveConfig(next); syncCheckinTimer(pi, ctx); ctx.ui.notify(`Mode set to ${v}.`, "info"); return; }
749
- if (v === "off") { const next = { ...cfg, mode: "off" as const }; saveConfig(next); stopCheckinTimer(sessionKey(ctx)); setPiRogueStatus(ctx, next, state); ctx.ui.notify("Advisor disabled.", "info"); return; }
892
+ if (v === "auto" || v === "manual") {
893
+ const next: AdvisorConfig = { ...cfg, mode: v };
894
+ saveConfig(next);
895
+ setPiRogueStatus(ctx, next, state);
896
+ ctx.ui.notify(`Mode set to ${v}.`, "info");
897
+ return;
898
+ }
899
+ if (v === "off") {
900
+ const next = { ...cfg, mode: "off" as const };
901
+ saveConfig(next);
902
+ setPiRogueStatus(ctx, next, state);
903
+ ctx.ui.notify("Advisor disabled.", "info");
904
+ return;
905
+ }
750
906
  ctx.ui.notify("Usage: /advisor mode auto|manual|off", "error");
751
907
  return;
752
908
  }
@@ -769,12 +925,12 @@ export function registerAdvisor(pi: ExtensionAPI): void {
769
925
  }
770
926
  if (cmd === "config") {
771
927
  ctx.ui.notify([
772
- "Advisor config (5 fields, all optional):",
928
+ "Advisor config (check-ins are orchestration-managed):",
773
929
  ` mode: "${cfg.mode}" — auto (preflight+post+cache) | manual | off`,
774
930
  ` review: "${cfg.review}" — light (changes/errors) | strict (every 3) | off`,
775
- ` checkins: "${cfg.checkins}" — mid-hour | off`,
931
+ ` checkins: "${cfg.checkins}" — set by active /loop lifecycle`,
776
932
  ` checkinIntervalMinutes: ${cfg.checkinIntervalMinutes}`,
777
- ` model: "${cfg.model || "auto"}" — optional override`,
933
+ ` model: "${cfg.model || "auto"}" — optional override for higher/advanced advisor model`,
778
934
  "",
779
935
  "Router logs: evals/advisor-router.jsonl",
780
936
  "Run /advisor <question> for immediate advice.",
@@ -788,31 +944,12 @@ export function registerAdvisor(pi: ExtensionAPI): void {
788
944
  return;
789
945
  }
790
946
  if (cmd === "checkins" || cmd === "checkin") {
791
- const v = rest[0];
792
- if (v === "off") {
793
- const next = { ...cfg, checkins: "off" as const };
794
- saveConfig(next);
795
- stopCheckinTimer(sessionKey(ctx));
796
- setPiRogueStatus(ctx, next, state);
797
- ctx.ui.notify("Advisor mid-hour check-ins disabled.", "info");
798
- return;
799
- }
800
- if (v === "on" || v === "mid-hour") {
801
- const next = { ...cfg, checkins: "mid-hour" as const };
802
- saveConfig(next);
803
- syncCheckinTimer(pi, ctx);
804
- ctx.ui.notify(`Advisor mid-hour check-ins enabled every ${next.checkinIntervalMinutes}m.`, "info");
805
- return;
806
- }
807
- const minutes = Number(v);
808
- if (Number.isFinite(minutes)) {
809
- const next = normalizeAdvisorConfig({ ...cfg, checkins: "mid-hour", checkinIntervalMinutes: minutes });
810
- saveConfig(next);
811
- syncCheckinTimer(pi, ctx);
812
- ctx.ui.notify(`Advisor mid-hour check-ins every ${next.checkinIntervalMinutes}m.`, "info");
813
- return;
814
- }
815
- ctx.ui.notify("Usage: /advisor checkins on|off|<minutes>", "error");
947
+ ctx.ui.notify([
948
+ "Advisor check-ins are orchestration-managed now.",
949
+ `Current: ${cfg.checkins === "off" ? "off" : `${cfg.checkinIntervalMinutes}m`}`,
950
+ "Create or resume /loop to activate scheduled higher-model check-ins; stop the loop to disable them.",
951
+ orchestrationSnapshotText(ctx),
952
+ ].join("\n"), "info");
816
953
  return;
817
954
  }
818
955
 
package/src/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export { default, registerAdvisor } from "./extension.js";
2
+ export { requestAdvisorLoopCheckin } from "./extension.js";
2
3
  export * from "./router.js";