@fiale-plus/pi-rogue-advisor 0.1.1 → 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 +6 -1
- package/package.json +5 -1
- package/skills/advisor/SKILL.md +9 -6
- package/src/extension.test.ts +63 -17
- package/src/extension.ts +205 -18
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.
|
|
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"
|
package/skills/advisor/SKILL.md
CHANGED
|
@@ -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
|
|
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 (
|
|
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
|
```
|
package/src/extension.test.ts
CHANGED
|
@@ -1,54 +1,100 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import type
|
|
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
|
|
6
|
-
const cfg
|
|
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
|
|
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
|
|
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("
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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
|
|
100
|
-
const
|
|
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
|
}
|
|
@@ -114,7 +139,7 @@ function loadState(): SessionState {
|
|
|
114
139
|
return {
|
|
115
140
|
turns: raw.turns ?? 0,
|
|
116
141
|
lastTask: raw.lastTask ?? "",
|
|
117
|
-
notes: (raw.notes ?? []).slice(-MAX_NOTES),
|
|
142
|
+
notes: (raw.notes ?? []).map(noteText).filter(Boolean).slice(-MAX_NOTES),
|
|
118
143
|
files: (raw.files ?? []).slice(-MAX_FILES),
|
|
119
144
|
errors: (raw.errors ?? []).slice(-MAX_ERRORS),
|
|
120
145
|
advisorCalls: raw.advisorCalls ?? 0,
|
|
@@ -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
|
|
|
@@ -178,6 +208,14 @@ function squish(t: unknown, max = 200): string {
|
|
|
178
208
|
return s.length <= max ? s : s.slice(0, max - 1).trimEnd() + "…";
|
|
179
209
|
}
|
|
180
210
|
|
|
211
|
+
function noteText(note: unknown): string {
|
|
212
|
+
const text = contentText(note);
|
|
213
|
+
if (/^\[object Object\](,\[object Object\])*$/.test(text)) return "";
|
|
214
|
+
if (text) return squish(text, 500);
|
|
215
|
+
if (note && typeof note === "object") return squish(JSON.stringify(note), 500);
|
|
216
|
+
return text;
|
|
217
|
+
}
|
|
218
|
+
|
|
181
219
|
type AdvisorHintDetails = {
|
|
182
220
|
decision?: "continue" | "review" | "defer";
|
|
183
221
|
reason?: string;
|
|
@@ -254,6 +292,97 @@ function mergeRouteReview(configReview: AdvisorConfig["review"], route?: ReviewP
|
|
|
254
292
|
return mergeReviewPolicy(configReview, route);
|
|
255
293
|
}
|
|
256
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
|
+
|
|
257
386
|
// ── Model resolution (auto-fallback through SOTA chain) ────────────────────
|
|
258
387
|
async function resolveModel(ctx: any, config: AdvisorConfig): Promise<{ model: any; auth: any; label: string } | null> {
|
|
259
388
|
// Try user's configured model first
|
|
@@ -409,12 +538,22 @@ async function doReview(pi: ExtensionAPI, ctx: any, trigger: string, delta: stri
|
|
|
409
538
|
// ── Extension entry point ──────────────────────────────────────────────────
|
|
410
539
|
|
|
411
540
|
export function registerAdvisor(pi: ExtensionAPI): void {
|
|
412
|
-
const config = loadConfig();
|
|
413
|
-
|
|
414
541
|
for (const customType of ["advisor:model", "advisor:rules", "advisor:llm"] as const) {
|
|
415
542
|
pi.registerMessageRenderer(customType, renderAdvisorHint);
|
|
416
543
|
}
|
|
417
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
|
+
|
|
418
557
|
// ── Tool ───────────────────────────────────────────────────────────────
|
|
419
558
|
pi.registerTool({
|
|
420
559
|
name: "advisor",
|
|
@@ -434,8 +573,10 @@ export function registerAdvisor(pi: ExtensionAPI): void {
|
|
|
434
573
|
|
|
435
574
|
// ── Preflight (heuristics only — no LLM call, <1ms) ──────────────────
|
|
436
575
|
pi.on("before_agent_start", async (event: any, ctx: any) => {
|
|
437
|
-
|
|
576
|
+
const cfg = loadConfig();
|
|
577
|
+
if (cfg.mode === "off" || cfg.mode === "manual") return { systemPrompt: event.systemPrompt };
|
|
438
578
|
const state = loadState();
|
|
579
|
+
setPiRogueStatus(ctx, cfg, state);
|
|
439
580
|
const prompt = typeof event.prompt === "string" && event.prompt.trim() ? squish(event.prompt, 1000) : "";
|
|
440
581
|
if (prompt) state.lastTask = prompt;
|
|
441
582
|
const briefText = brief(state);
|
|
@@ -491,24 +632,28 @@ export function registerAdvisor(pi: ExtensionAPI): void {
|
|
|
491
632
|
|
|
492
633
|
// ── Post-review (turn_end) ─────────────────────────────────────────────
|
|
493
634
|
pi.on("turn_end", async (event: any, ctx: any) => {
|
|
494
|
-
|
|
635
|
+
const cfg = loadConfig();
|
|
636
|
+
if (cfg.mode === "off") return;
|
|
495
637
|
const state = loadState();
|
|
496
638
|
state.turns++;
|
|
497
639
|
const tools = (event.toolResults || []).map((t: any) => String(t?.toolName || t?.name || "tool"));
|
|
498
640
|
const fileChanged = tools.some((t: string) => /^(edit|write)$/i.test(t));
|
|
499
641
|
const failed = (event.toolResults || []).some((t: any) => isActualFailure(t));
|
|
500
|
-
const text = squish(event.message?.content
|
|
642
|
+
const text = squish(contentText(event.message?.content));
|
|
501
643
|
if (text && text !== state.notes[state.notes.length - 1]) state.notes.push(text);
|
|
502
644
|
saveState(state);
|
|
645
|
+
setPiRogueStatus(ctx, cfg, state);
|
|
646
|
+
void maybeAdvisorCheckin(pi, ctx, "turn_end");
|
|
503
647
|
|
|
504
|
-
if (
|
|
648
|
+
if (cfg.review !== "off") {
|
|
505
649
|
await doReview(pi, ctx, `turn-${state.turns}`, text, { fileChanged, failed, isAgentEnd: false });
|
|
506
650
|
}
|
|
507
651
|
});
|
|
508
652
|
|
|
509
653
|
// ── Post-review (agent_end) ────────────────────────────────────────────
|
|
510
654
|
pi.on("agent_end", async (event: any, ctx: any) => {
|
|
511
|
-
|
|
655
|
+
const cfg = loadConfig();
|
|
656
|
+
if (cfg.mode === "off" || cfg.review === "off") return;
|
|
512
657
|
const state = loadState();
|
|
513
658
|
const msgs = (event.messages || []).filter((m: any) => m.role === "assistant" || m.role === "toolResult");
|
|
514
659
|
const last = msgs[msgs.length - 1];
|
|
@@ -518,6 +663,17 @@ export function registerAdvisor(pi: ExtensionAPI): void {
|
|
|
518
663
|
await doReview(pi, ctx, "agent-end", delta, { fileChanged, failed, isAgentEnd: true });
|
|
519
664
|
});
|
|
520
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
|
+
|
|
521
677
|
// ── /advisor command ───────────────────────────────────────────────────
|
|
522
678
|
pi.registerCommand("advisor", {
|
|
523
679
|
description: "Senior engineering advisor. Usage: /advisor [on|off|status|config|question]",
|
|
@@ -535,21 +691,22 @@ export function registerAdvisor(pi: ExtensionAPI): void {
|
|
|
535
691
|
note ? `🧭 ${truncate(note, 200)}` : "",
|
|
536
692
|
route ? `Router: ${summarizeRoute(route)}${route.safety ? " · safety" : ""}` : "",
|
|
537
693
|
"",
|
|
538
|
-
`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"}`,
|
|
539
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",
|
|
540
697
|
"",
|
|
541
|
-
"Commands: /advisor on|off | /advisor status | /advisor config | <question>",
|
|
698
|
+
"Commands: /advisor on|off | /advisor status | /advisor checkins on|off|<minutes> | /advisor config | <question>",
|
|
542
699
|
"Tip: SOTA models auto-detected. No config needed.",
|
|
543
700
|
].filter(Boolean).join("\n"), "info");
|
|
544
701
|
return;
|
|
545
702
|
}
|
|
546
703
|
|
|
547
|
-
if (cmd === "on" && cfg.mode === "off") {
|
|
548
|
-
if (cmd === "off") {
|
|
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; }
|
|
549
706
|
if (cmd === "mode") {
|
|
550
707
|
const v = rest[0];
|
|
551
|
-
if (v === "auto" || v === "manual") {
|
|
552
|
-
if (v === "off") {
|
|
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; }
|
|
553
710
|
ctx.ui.notify("Usage: /advisor mode auto|manual|off", "error");
|
|
554
711
|
return;
|
|
555
712
|
}
|
|
@@ -572,9 +729,11 @@ export function registerAdvisor(pi: ExtensionAPI): void {
|
|
|
572
729
|
}
|
|
573
730
|
if (cmd === "config") {
|
|
574
731
|
ctx.ui.notify([
|
|
575
|
-
"Advisor config (
|
|
732
|
+
"Advisor config (5 fields, all optional):",
|
|
576
733
|
` mode: "${cfg.mode}" — auto (preflight+post+cache) | manual | off`,
|
|
577
734
|
` review: "${cfg.review}" — light (changes/errors) | strict (every 3) | off`,
|
|
735
|
+
` checkins: "${cfg.checkins}" — mid-hour | off`,
|
|
736
|
+
` checkinIntervalMinutes: ${cfg.checkinIntervalMinutes}`,
|
|
578
737
|
` model: "${cfg.model || "auto"}" — optional override`,
|
|
579
738
|
"",
|
|
580
739
|
"Router logs: evals/advisor-router.jsonl",
|
|
@@ -584,10 +743,38 @@ export function registerAdvisor(pi: ExtensionAPI): void {
|
|
|
584
743
|
}
|
|
585
744
|
if (cmd === "review") {
|
|
586
745
|
const v = rest[0];
|
|
587
|
-
if (v === "light" || v === "strict" || v === "off") {
|
|
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; }
|
|
588
747
|
ctx.ui.notify("Usage: /advisor review light|strict|off", "error");
|
|
589
748
|
return;
|
|
590
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
|
+
}
|
|
591
778
|
|
|
592
779
|
// Anything else: treat as a question to the advisor
|
|
593
780
|
const r = await askAdvisor(pi, ctx, a, "slash", true);
|