@fiale-plus/pi-rogue-advisor 0.1.2 → 0.1.3

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
@@ -1,9 +1,14 @@
1
1
  # @fiale-plus/pi-rogue-advisor
2
2
 
3
- PiRogue advisor: session coaching, phase-aware routing, and decision framing for Pi.
3
+ PiRogue advisor: session coaching, phase-aware routing, mid-hour check-ins, and decision framing for Pi.
4
4
 
5
5
  The bundled binary gate model is shipped with the package and auto-seeded on install.
6
6
 
7
7
  Install locally from this repo root: `npm install`
8
8
 
9
9
  Published install: `pi install @fiale-plus/pi-rogue-advisor`
10
+
11
+ Useful commands:
12
+
13
+ - `/pi-rogue` — cockpit/status entry point
14
+ - `/advisor checkins on|off|<minutes>` — configure low-power mid-session check-ins
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiale-plus/pi-rogue-advisor",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "PiRogue advisor extension for Pi — multi-model support, SOTA model suggestion, cache-aware session advisory.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -11,6 +11,10 @@
11
11
  "keywords": [
12
12
  "pi-package"
13
13
  ],
14
+ "scripts": {
15
+ "check": "tsc -p ../../tsconfig.json --noEmit",
16
+ "test": "cd ../.. && vitest run packages/advisor/src/*.test.ts"
17
+ },
14
18
  "main": "./src/index.ts",
15
19
  "exports": {
16
20
  ".": "./src/index.ts"
@@ -5,15 +5,17 @@ description: Zero-config strategic advisor for Pi. Auto-detects best model, phas
5
5
 
6
6
  # PiRogue Advisor
7
7
 
8
- Works out of the box. Just install and use `/advisor`.
8
+ Works out of the box. Just install and use `/advisor` or `/pi-rogue`.
9
9
 
10
10
  > 96 strategic calls saved ~$53 on GPT-5.5 over 3,071 turns — see [`docs/savings.md`](../../docs/savings.md)
11
11
 
12
12
  ## Quick start
13
13
 
14
+ - `/pi-rogue` — cockpit/status entry point
14
15
  - `/advisor` — status + config
15
16
  - `/advisor <question>` — get immediate advice
16
17
  - `/advisor on|off` — enable/disable
18
+ - `/advisor checkins on|off|<minutes>` — configure low-power mid-hour check-ins
17
19
 
18
20
  Zero config needed. Falls back through SOTA models (gpt-5.5 → claude-opus-4-6 → sonnet-4-6) automatically.
19
21
 
@@ -34,14 +36,15 @@ Skip: reads, small edits, one-liners.
34
36
  | `/advisor off` | Disable |
35
37
  | `/advisor mode auto\|manual\|off` | Set advisor mode |
36
38
  | `/advisor model <provider/model>` | Set specific model (e.g. `openai-codex/gpt-5.5`) |
37
- | `/advisor status` | Full status with model info |
38
- | `/advisor config` | Show current 3-field config |
39
+ | `/advisor status` | Full status with model and check-in info |
40
+ | `/advisor config` | Show current config |
39
41
  | `/advisor review light\|strict\|off` | Set review aggressiveness |
42
+ | `/advisor checkins on\|off\|<minutes>` | Configure low-power mid-hour check-ins |
40
43
 
41
- ## Config (3 fields, all optional)
44
+ ## Config (5 fields, all optional)
42
45
 
43
- Defaults: `mode: auto, review: light`
46
+ Defaults: `mode: auto, review: light, checkins: mid-hour, checkinIntervalMinutes: 30`
44
47
 
45
48
  ```json
46
- { "mode": "auto", "review": "light" }
49
+ { "mode": "auto", "review": "light", "checkins": "mid-hour", "checkinIntervalMinutes": 30 }
47
50
  ```
@@ -1,54 +1,100 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import type { AdvisorConfig } from "./extension.js";
2
+ import { normalizeAdvisorConfig, shouldRunCheckin, type AdvisorConfig } from "./extension.js";
3
+
4
+ function state(overrides: Record<string, unknown> = {}) {
5
+ return {
6
+ turns: 2,
7
+ lastTask: "work on orchestration",
8
+ notes: ["made progress"],
9
+ files: [],
10
+ errors: [],
11
+ advisorCalls: 0,
12
+ cacheHits: 0,
13
+ followUp: "",
14
+ router: {},
15
+ checkin: {},
16
+ ...overrides,
17
+ } as any;
18
+ }
3
19
 
4
20
  describe("AdvisorConfig", () => {
5
- it("defaults to auto mode and light review with no model override", () => {
6
- const cfg: AdvisorConfig = { mode: "auto", review: "light" };
21
+ it("defaults to auto mode, light review, and mid-hour check-ins", () => {
22
+ const cfg = normalizeAdvisorConfig({});
7
23
  expect(cfg.mode).toBe("auto");
8
24
  expect(cfg.review).toBe("light");
25
+ expect(cfg.checkins).toBe("mid-hour");
26
+ expect(cfg.checkinIntervalMinutes).toBe(30);
9
27
  expect(cfg.model).toBeUndefined();
10
28
  });
11
29
 
12
30
  it("accepts all 3 modes", () => {
13
31
  for (const mode of ["auto", "manual", "off"] as const) {
14
- const cfg: AdvisorConfig = { mode, review: "light" };
15
- expect(cfg.mode).toBe(mode);
32
+ const cfg: AdvisorConfig = { mode, review: "light", checkins: "mid-hour", checkinIntervalMinutes: 30 };
33
+ expect(normalizeAdvisorConfig(cfg).mode).toBe(mode);
16
34
  }
17
35
  });
18
36
 
19
37
  it("accepts all 3 review levels", () => {
20
38
  for (const review of ["light", "strict", "off"] as const) {
21
- const cfg: AdvisorConfig = { mode: "auto", review };
22
- expect(cfg.review).toBe(review);
39
+ const cfg: AdvisorConfig = { mode: "auto", review, checkins: "mid-hour", checkinIntervalMinutes: 30 };
40
+ expect(normalizeAdvisorConfig(cfg).review).toBe(review);
23
41
  }
24
42
  });
25
43
 
44
+ it("bounds check-in intervals", () => {
45
+ expect(normalizeAdvisorConfig({ checkinIntervalMinutes: 1 }).checkinIntervalMinutes).toBe(10);
46
+ expect(normalizeAdvisorConfig({ checkinIntervalMinutes: 999 }).checkinIntervalMinutes).toBe(240);
47
+ });
48
+
26
49
  it("accepts optional model override", () => {
27
- const cfg: AdvisorConfig = { mode: "auto", review: "light", model: "claude-sonnet-4-6" };
50
+ const cfg = normalizeAdvisorConfig({ mode: "auto", review: "light", model: "claude-sonnet-4-6" });
28
51
  expect(cfg.model).toBe("claude-sonnet-4-6");
29
52
  });
30
53
 
31
54
  it("serializes/deserializes without data loss (JSON round-trip)", () => {
32
- const original: AdvisorConfig = { mode: "auto", review: "light", model: "claude-opus-4-6" };
55
+ const original = normalizeAdvisorConfig({ mode: "auto", review: "light", model: "claude-opus-4-6" });
33
56
  const json = JSON.stringify(original);
34
- const parsed = JSON.parse(json) as AdvisorConfig;
57
+ const parsed = normalizeAdvisorConfig(JSON.parse(json) as AdvisorConfig);
35
58
  expect(parsed.mode).toBe("auto");
36
59
  expect(parsed.review).toBe("light");
60
+ expect(parsed.checkins).toBe("mid-hour");
61
+ expect(parsed.checkinIntervalMinutes).toBe(30);
37
62
  expect(parsed.model).toBe("claude-opus-4-6");
38
63
  });
64
+ });
65
+
66
+ describe("mid-hour check-ins", () => {
67
+ it("does not run immediately after session start", () => {
68
+ const cfg = normalizeAdvisorConfig({ checkinIntervalMinutes: 30 });
69
+ const startedAt = 1_000;
70
+ const now = startedAt + 5 * 60_000;
71
+ expect(shouldRunCheckin(cfg, state(), now, startedAt)).toBeNull();
72
+ });
39
73
 
40
- it("has exactly 3 properties (mode, review, model)", () => {
41
- const keys = Object.keys({ mode: "auto", review: "light" } as AdvisorConfig);
42
- expect(keys.length).toBeLessThanOrEqual(3);
43
- const allKeys = ["mode", "review", "model"];
44
- const configKeys = Object.keys({ mode: "auto", review: "light" } as AdvisorConfig);
45
- expect(configKeys.every((k) => allKeys.includes(k))).toBe(true);
74
+ it("runs after interval when there was new activity", () => {
75
+ const cfg = normalizeAdvisorConfig({ checkinIntervalMinutes: 30 });
76
+ const startedAt = 1_000;
77
+ const now = startedAt + 31 * 60_000;
78
+ expect(shouldRunCheckin(cfg, state(), now, startedAt)).toMatch(/mid-hour check-in/);
79
+ });
80
+
81
+ it("does not run without activity since the last check-in", () => {
82
+ const cfg = normalizeAdvisorConfig({ checkinIntervalMinutes: 30 });
83
+ const lastAt = new Date(1_000).toISOString();
84
+ const now = 1_000 + 60 * 60_000;
85
+ expect(shouldRunCheckin(cfg, state({ turns: 5, checkin: { lastAt, lastTurn: 5 } }), now, 1_000)).toBeNull();
86
+ });
87
+
88
+ it("does not run when check-ins are disabled", () => {
89
+ const cfg = normalizeAdvisorConfig({ checkins: "off" });
90
+ expect(shouldRunCheckin(cfg, state(), 999999, 1)).toBeNull();
46
91
  });
47
92
  });
48
93
 
94
+
49
95
  describe("SOTA model suggestions", () => {
50
96
  it("includes gpt-5.5 as primary option", () => {
51
- const cfg: AdvisorConfig = { mode: "auto", review: "light" };
97
+ const cfg = normalizeAdvisorConfig({ mode: "auto", review: "light" });
52
98
  expect(cfg.model).toBeUndefined(); // model is optional, auto-detect
53
99
  });
54
100
  });
package/src/extension.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { basename } from "node:path";
2
3
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
4
  import { Box, Text } from "@earendil-works/pi-tui";
4
5
  import { completeSimple, type ThinkingLevel } from "@earendil-works/pi-ai";
@@ -25,6 +26,10 @@ export interface AdvisorConfig {
25
26
  mode: "auto" | "manual" | "off";
26
27
  /** "light" (file changes/errors only) | "strict" (every 3 turns) | "off" */
27
28
  review: "light" | "strict" | "off";
29
+ /** Opportunistic advisor check-ins during long sessions. */
30
+ checkins: "mid-hour" | "off";
31
+ /** Minutes between check-ins; bounded and cheap-gated by recent activity. */
32
+ checkinIntervalMinutes: number;
28
33
  /** Optional model override. Auto-detects SOTA (gpt-5.5, claude-opus-4-6…) if unset */
29
34
  model?: string;
30
35
  }
@@ -32,6 +37,8 @@ export interface AdvisorConfig {
32
37
  const DEFAULT_CONFIG: AdvisorConfig = {
33
38
  mode: "auto",
34
39
  review: "light",
40
+ checkins: "mid-hour",
41
+ checkinIntervalMinutes: 30,
35
42
  };
36
43
 
37
44
  const CONFIG_PATH = featureFile("advisor", "config.json");
@@ -44,6 +51,12 @@ const MAX_CACHE = 64;
44
51
  const MAX_NOTES = 12;
45
52
  const MAX_FILES = 8;
46
53
  const MAX_ERRORS = 5;
54
+ const CHECKIN_POLL_MS = 5 * 60_000;
55
+ const MIN_CHECKIN_INTERVAL_MINUTES = 10;
56
+ const MAX_CHECKIN_INTERVAL_MINUTES = 240;
57
+ const checkinTimers = new Map<string, NodeJS.Timeout>();
58
+ const checkinStartedAt = new Map<string, number>();
59
+ const checkinLocks = new Set<string>();
47
60
 
48
61
  // ── SOTA models (ordered by preference) ───────────────────────────────────
49
62
  const SOTA_CHAIN: Array<{ provider: string; model: string; label: string }> = [
@@ -67,6 +80,11 @@ interface SessionState {
67
80
  preflight?: AdvisorRouteDecision;
68
81
  review?: AdvisorRouteDecision;
69
82
  };
83
+ checkin: {
84
+ lastAt?: string;
85
+ lastTurn?: number;
86
+ lastReason?: string;
87
+ };
70
88
  }
71
89
 
72
90
  function defaultState(): SessionState {
@@ -80,6 +98,7 @@ function defaultState(): SessionState {
80
98
  cacheHits: 0,
81
99
  followUp: "",
82
100
  router: {},
101
+ checkin: {},
83
102
  };
84
103
  }
85
104
 
@@ -96,15 +115,21 @@ function writeJson(path: string, v: unknown) {
96
115
  writeText(path, JSON.stringify(v, null, 2) + "\n");
97
116
  }
98
117
 
99
- function loadConfig(): AdvisorConfig {
100
- const raw = readJson<Partial<AdvisorConfig>>(CONFIG_PATH, {});
118
+ export function normalizeAdvisorConfig(raw: Partial<AdvisorConfig> = {}): AdvisorConfig {
119
+ const interval = Number(raw.checkinIntervalMinutes ?? DEFAULT_CONFIG.checkinIntervalMinutes);
101
120
  return {
102
121
  mode: (raw.mode === "manual" || raw.mode === "off") ? raw.mode : "auto",
103
122
  review: (raw.review === "strict" || raw.review === "off") ? raw.review : "light",
123
+ checkins: raw.checkins === "off" ? "off" : "mid-hour",
124
+ checkinIntervalMinutes: Math.min(MAX_CHECKIN_INTERVAL_MINUTES, Math.max(MIN_CHECKIN_INTERVAL_MINUTES, Number.isFinite(interval) ? Math.round(interval) : DEFAULT_CONFIG.checkinIntervalMinutes)),
104
125
  model: raw.model || undefined,
105
126
  };
106
127
  }
107
128
 
129
+ function loadConfig(): AdvisorConfig {
130
+ return normalizeAdvisorConfig(readJson<Partial<AdvisorConfig>>(CONFIG_PATH, {}));
131
+ }
132
+
108
133
  function saveConfig(c: AdvisorConfig) {
109
134
  writeJson(CONFIG_PATH, c);
110
135
  }
@@ -124,6 +149,11 @@ function loadState(): SessionState {
124
149
  preflight: raw.router?.preflight,
125
150
  review: raw.router?.review,
126
151
  },
152
+ checkin: {
153
+ lastAt: raw.checkin?.lastAt,
154
+ lastTurn: raw.checkin?.lastTurn,
155
+ lastReason: raw.checkin?.lastReason,
156
+ },
127
157
  };
128
158
  }
129
159
 
@@ -262,6 +292,97 @@ function mergeRouteReview(configReview: AdvisorConfig["review"], route?: ReviewP
262
292
  return mergeReviewPolicy(configReview, route);
263
293
  }
264
294
 
295
+ function sessionKey(ctx: any): string {
296
+ const sessionFile = ctx?.sessionManager?.getSessionFile?.();
297
+ if (!sessionFile) return "session";
298
+ return basename(String(sessionFile)).replace(/\.[^.]+$/, "");
299
+ }
300
+
301
+ function setPiRogueStatus(ctx: any, config = loadConfig(), state = loadState()): void {
302
+ const normalized = normalizeAdvisorConfig(config);
303
+ const checkin = normalized.checkins === "off" ? "checkins off" : `checkins ${normalized.checkinIntervalMinutes}m`;
304
+ const last = state.checkin.lastAt ? ` · last ${new Date(state.checkin.lastAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}` : "";
305
+ ctx.ui.setStatus("pi-rogue", `☠︎ advisor ${normalized.mode}/${normalized.review} · ${checkin}${last}`);
306
+ }
307
+
308
+ export function shouldRunCheckin(config: AdvisorConfig, state: SessionState, now = Date.now(), startedAt = now): string | null {
309
+ const normalized = normalizeAdvisorConfig(config);
310
+ if (normalized.mode === "off" || normalized.mode === "manual") return null;
311
+ if (normalized.checkins === "off") return null;
312
+ if (!state.lastTask && state.notes.length === 0) return null;
313
+ const lastTurn = state.checkin.lastTurn ?? 0;
314
+ if (state.turns <= lastTurn) return null;
315
+ const lastAt = state.checkin.lastAt ? Date.parse(state.checkin.lastAt) : 0;
316
+ const intervalMs = normalized.checkinIntervalMinutes * 60_000;
317
+ const since = lastAt || startedAt;
318
+ if (since && now - since < intervalMs) return null;
319
+ return `mid-hour check-in after ${state.turns - lastTurn} new turn(s)`;
320
+ }
321
+
322
+ function stopCheckinTimer(key: string): void {
323
+ const timer = checkinTimers.get(key);
324
+ if (timer) {
325
+ clearInterval(timer);
326
+ checkinTimers.delete(key);
327
+ }
328
+ }
329
+
330
+ async function maybeAdvisorCheckin(pi: ExtensionAPI, ctx: any, source: string): Promise<boolean> {
331
+ const key = sessionKey(ctx);
332
+ if (checkinLocks.has(key)) return false;
333
+
334
+ const config = loadConfig();
335
+ const state = loadState();
336
+ const startedAt = checkinStartedAt.get(key) ?? Date.now();
337
+ const reason = shouldRunCheckin(config, state, Date.now(), startedAt);
338
+ setPiRogueStatus(ctx, config, state);
339
+ if (!reason) return false;
340
+
341
+ checkinLocks.add(key);
342
+ try {
343
+ const response = await askAdvisor(
344
+ pi,
345
+ ctx,
346
+ `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.`,
347
+ "review",
348
+ true,
349
+ );
350
+ if (response.error) return false;
351
+
352
+ const next = loadState();
353
+ next.checkin = { lastAt: new Date().toISOString(), lastTurn: next.turns, lastReason: reason };
354
+ saveState(next);
355
+ setPiRogueStatus(ctx, config, next);
356
+ sendAdvisorHint(pi, "review", "mid-hour check-in", response.text, [response.text]);
357
+ return true;
358
+ } finally {
359
+ checkinLocks.delete(key);
360
+ }
361
+ }
362
+
363
+ function syncCheckinTimer(pi: ExtensionAPI, ctx: any): void {
364
+ const key = sessionKey(ctx);
365
+ stopCheckinTimer(key);
366
+ checkinStartedAt.set(key, Date.now());
367
+ setPiRogueStatus(ctx);
368
+ const config = loadConfig();
369
+ if (config.mode === "off" || config.mode === "manual" || config.checkins === "off") return;
370
+ checkinTimers.set(key, setInterval(() => { void maybeAdvisorCheckin(pi, ctx, "timer"); }, CHECKIN_POLL_MS));
371
+ }
372
+
373
+ function piRogueCockpitText(config: AdvisorConfig, state: SessionState, currentNote: string): string {
374
+ const normalized = normalizeAdvisorConfig(config);
375
+ return [
376
+ "☠︎ PiRogue cockpit",
377
+ currentNote ? `Advisor: ${truncate(currentNote, 220)}` : "Advisor: no current note",
378
+ `Mode: ${normalized.mode} | Review: ${normalized.review} | Check-ins: ${normalized.checkins === "off" ? "off" : `${normalized.checkinIntervalMinutes}m`}`,
379
+ `Turns: ${state.turns} | Advisor calls: ${state.advisorCalls} | Cache hits: ${state.cacheHits}`,
380
+ state.checkin.lastAt ? `Last check-in: ${new Date(state.checkin.lastAt).toLocaleString()} (${state.checkin.lastReason || "mid-hour"})` : "Last check-in: never",
381
+ "",
382
+ "Commands: /advisor status · /advisor checkins on|off|<minutes> · /goal · /loop status · /autoresearch status",
383
+ ].join("\n");
384
+ }
385
+
265
386
  // ── Model resolution (auto-fallback through SOTA chain) ────────────────────
266
387
  async function resolveModel(ctx: any, config: AdvisorConfig): Promise<{ model: any; auth: any; label: string } | null> {
267
388
  // Try user's configured model first
@@ -417,12 +538,22 @@ async function doReview(pi: ExtensionAPI, ctx: any, trigger: string, delta: stri
417
538
  // ── Extension entry point ──────────────────────────────────────────────────
418
539
 
419
540
  export function registerAdvisor(pi: ExtensionAPI): void {
420
- const config = loadConfig();
421
-
422
541
  for (const customType of ["advisor:model", "advisor:rules", "advisor:llm"] as const) {
423
542
  pi.registerMessageRenderer(customType, renderAdvisorHint);
424
543
  }
425
544
 
545
+ pi.on("session_start", (_event, ctx) => {
546
+ syncCheckinTimer(pi, ctx);
547
+ });
548
+
549
+ pi.on("session_shutdown", (_event, ctx) => {
550
+ const key = sessionKey(ctx);
551
+ stopCheckinTimer(key);
552
+ checkinStartedAt.delete(key);
553
+ checkinLocks.delete(key);
554
+ ctx.ui.setStatus("pi-rogue", undefined);
555
+ });
556
+
426
557
  // ── Tool ───────────────────────────────────────────────────────────────
427
558
  pi.registerTool({
428
559
  name: "advisor",
@@ -442,8 +573,10 @@ export function registerAdvisor(pi: ExtensionAPI): void {
442
573
 
443
574
  // ── Preflight (heuristics only — no LLM call, <1ms) ──────────────────
444
575
  pi.on("before_agent_start", async (event: any, ctx: any) => {
445
- if (config.mode === "off" || config.mode === "manual") return { systemPrompt: event.systemPrompt };
576
+ const cfg = loadConfig();
577
+ if (cfg.mode === "off" || cfg.mode === "manual") return { systemPrompt: event.systemPrompt };
446
578
  const state = loadState();
579
+ setPiRogueStatus(ctx, cfg, state);
447
580
  const prompt = typeof event.prompt === "string" && event.prompt.trim() ? squish(event.prompt, 1000) : "";
448
581
  if (prompt) state.lastTask = prompt;
449
582
  const briefText = brief(state);
@@ -499,7 +632,8 @@ export function registerAdvisor(pi: ExtensionAPI): void {
499
632
 
500
633
  // ── Post-review (turn_end) ─────────────────────────────────────────────
501
634
  pi.on("turn_end", async (event: any, ctx: any) => {
502
- if (config.mode === "off") return;
635
+ const cfg = loadConfig();
636
+ if (cfg.mode === "off") return;
503
637
  const state = loadState();
504
638
  state.turns++;
505
639
  const tools = (event.toolResults || []).map((t: any) => String(t?.toolName || t?.name || "tool"));
@@ -508,15 +642,18 @@ export function registerAdvisor(pi: ExtensionAPI): void {
508
642
  const text = squish(contentText(event.message?.content));
509
643
  if (text && text !== state.notes[state.notes.length - 1]) state.notes.push(text);
510
644
  saveState(state);
645
+ setPiRogueStatus(ctx, cfg, state);
646
+ void maybeAdvisorCheckin(pi, ctx, "turn_end");
511
647
 
512
- if (config.review !== "off") {
648
+ if (cfg.review !== "off") {
513
649
  await doReview(pi, ctx, `turn-${state.turns}`, text, { fileChanged, failed, isAgentEnd: false });
514
650
  }
515
651
  });
516
652
 
517
653
  // ── Post-review (agent_end) ────────────────────────────────────────────
518
654
  pi.on("agent_end", async (event: any, ctx: any) => {
519
- if (config.mode === "off" || config.review === "off") return;
655
+ const cfg = loadConfig();
656
+ if (cfg.mode === "off" || cfg.review === "off") return;
520
657
  const state = loadState();
521
658
  const msgs = (event.messages || []).filter((m: any) => m.role === "assistant" || m.role === "toolResult");
522
659
  const last = msgs[msgs.length - 1];
@@ -526,6 +663,17 @@ export function registerAdvisor(pi: ExtensionAPI): void {
526
663
  await doReview(pi, ctx, "agent-end", delta, { fileChanged, failed, isAgentEnd: true });
527
664
  });
528
665
 
666
+ // ── /pi-rogue cockpit ──────────────────────────────────────────────────
667
+ pi.registerCommand("pi-rogue", {
668
+ description: "Show PiRogue cockpit: advisor, check-ins, and orchestration command pointers",
669
+ handler: async (_args, ctx) => {
670
+ const cfg = loadConfig();
671
+ const state = loadState();
672
+ setPiRogueStatus(ctx, cfg, state);
673
+ ctx.ui.notify(piRogueCockpitText(cfg, state, readText(CURRENT_PATH).trim()), "info");
674
+ },
675
+ });
676
+
529
677
  // ── /advisor command ───────────────────────────────────────────────────
530
678
  pi.registerCommand("advisor", {
531
679
  description: "Senior engineering advisor. Usage: /advisor [on|off|status|config|question]",
@@ -543,21 +691,22 @@ export function registerAdvisor(pi: ExtensionAPI): void {
543
691
  note ? `🧭 ${truncate(note, 200)}` : "",
544
692
  route ? `Router: ${summarizeRoute(route)}${route.safety ? " · safety" : ""}` : "",
545
693
  "",
546
- `Mode: ${cfg.mode} | Review: ${cfg.review} | Model: ${resolved?.label || cfg.model || "auto"}`,
694
+ `Mode: ${cfg.mode} | Review: ${cfg.review} | Check-ins: ${cfg.checkins === "off" ? "off" : `${cfg.checkinIntervalMinutes}m`} | Model: ${resolved?.label || cfg.model || "auto"}`,
547
695
  `Turns: ${state.turns} | Calls: ${state.advisorCalls} | Cache hits: ${state.cacheHits}`,
696
+ state.checkin.lastAt ? `Last check-in: ${new Date(state.checkin.lastAt).toLocaleString()} (${state.checkin.lastReason || "mid-hour"})` : "Last check-in: never",
548
697
  "",
549
- "Commands: /advisor on|off | /advisor status | /advisor config | <question>",
698
+ "Commands: /advisor on|off | /advisor status | /advisor checkins on|off|<minutes> | /advisor config | <question>",
550
699
  "Tip: SOTA models auto-detected. No config needed.",
551
700
  ].filter(Boolean).join("\n"), "info");
552
701
  return;
553
702
  }
554
703
 
555
- if (cmd === "on" && cfg.mode === "off") { saveConfig({ ...cfg, mode: "auto" }); ctx.ui.notify("Advisor enabled (auto mode).", "info"); return; }
556
- if (cmd === "off") { saveConfig({ ...cfg, mode: "off" }); ctx.ui.notify("Advisor disabled.", "info"); return; }
704
+ 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; }
705
+ 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; }
557
706
  if (cmd === "mode") {
558
707
  const v = rest[0];
559
- if (v === "auto" || v === "manual") { saveConfig({ ...cfg, mode: v }); ctx.ui.notify(`Mode set to ${v}.`, "info"); return; }
560
- if (v === "off") { saveConfig({ ...cfg, mode: "off" }); ctx.ui.notify("Advisor disabled.", "info"); return; }
708
+ 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; }
709
+ 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; }
561
710
  ctx.ui.notify("Usage: /advisor mode auto|manual|off", "error");
562
711
  return;
563
712
  }
@@ -580,9 +729,11 @@ export function registerAdvisor(pi: ExtensionAPI): void {
580
729
  }
581
730
  if (cmd === "config") {
582
731
  ctx.ui.notify([
583
- "Advisor config (3 fields, all optional):",
732
+ "Advisor config (5 fields, all optional):",
584
733
  ` mode: "${cfg.mode}" — auto (preflight+post+cache) | manual | off`,
585
734
  ` review: "${cfg.review}" — light (changes/errors) | strict (every 3) | off`,
735
+ ` checkins: "${cfg.checkins}" — mid-hour | off`,
736
+ ` checkinIntervalMinutes: ${cfg.checkinIntervalMinutes}`,
586
737
  ` model: "${cfg.model || "auto"}" — optional override`,
587
738
  "",
588
739
  "Router logs: evals/advisor-router.jsonl",
@@ -592,10 +743,38 @@ export function registerAdvisor(pi: ExtensionAPI): void {
592
743
  }
593
744
  if (cmd === "review") {
594
745
  const v = rest[0];
595
- if (v === "light" || v === "strict" || v === "off") { saveConfig({ ...cfg, review: v }); ctx.ui.notify(`Review set to ${v}.`, "info"); return; }
746
+ if (v === "light" || v === "strict" || v === "off") { const next: AdvisorConfig = { ...cfg, review: v }; saveConfig(next); setPiRogueStatus(ctx, next, state); ctx.ui.notify(`Review set to ${v}.`, "info"); return; }
596
747
  ctx.ui.notify("Usage: /advisor review light|strict|off", "error");
597
748
  return;
598
749
  }
750
+ if (cmd === "checkins" || cmd === "checkin") {
751
+ const v = rest[0];
752
+ if (v === "off") {
753
+ const next = { ...cfg, checkins: "off" as const };
754
+ saveConfig(next);
755
+ stopCheckinTimer(sessionKey(ctx));
756
+ setPiRogueStatus(ctx, next, state);
757
+ ctx.ui.notify("Advisor mid-hour check-ins disabled.", "info");
758
+ return;
759
+ }
760
+ if (v === "on" || v === "mid-hour") {
761
+ const next = { ...cfg, checkins: "mid-hour" as const };
762
+ saveConfig(next);
763
+ syncCheckinTimer(pi, ctx);
764
+ ctx.ui.notify(`Advisor mid-hour check-ins enabled every ${next.checkinIntervalMinutes}m.`, "info");
765
+ return;
766
+ }
767
+ const minutes = Number(v);
768
+ if (Number.isFinite(minutes)) {
769
+ const next = normalizeAdvisorConfig({ ...cfg, checkins: "mid-hour", checkinIntervalMinutes: minutes });
770
+ saveConfig(next);
771
+ syncCheckinTimer(pi, ctx);
772
+ ctx.ui.notify(`Advisor mid-hour check-ins every ${next.checkinIntervalMinutes}m.`, "info");
773
+ return;
774
+ }
775
+ ctx.ui.notify("Usage: /advisor checkins on|off|<minutes>", "error");
776
+ return;
777
+ }
599
778
 
600
779
  // Anything else: treat as a question to the advisor
601
780
  const r = await askAdvisor(pi, ctx, a, "slash", true);