@fiale-plus/pi-rogue-advisor 0.1.2 → 0.1.4
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/completions.test.ts +31 -0
- package/src/completions.ts +83 -0
- package/src/extension.test.ts +63 -17
- package/src/extension.ts +235 -16
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.4",
|
|
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
|
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { advisorArgumentCompletions, piRogueArgumentCompletions } from "./completions.js";
|
|
3
|
+
|
|
4
|
+
describe("advisor completions", () => {
|
|
5
|
+
it("offers top-level advisor continuations", () => {
|
|
6
|
+
const values = advisorArgumentCompletions("")?.map((i) => i.value);
|
|
7
|
+
expect(values).toEqual(expect.arrayContaining(["status", "config", "checkins", "review"]));
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("offers nested review choices", () => {
|
|
11
|
+
const values = advisorArgumentCompletions("review ")?.map((i) => i.value);
|
|
12
|
+
expect(values).toEqual(["light", "strict", "off"]);
|
|
13
|
+
});
|
|
14
|
+
|
|
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
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("pi-rogue cockpit completions", () => {
|
|
22
|
+
it("offers umbrella sections", () => {
|
|
23
|
+
const values = piRogueArgumentCompletions("")?.map((i) => i.value);
|
|
24
|
+
expect(values).toEqual(expect.arrayContaining(["status", "advisor", "orchestration", "help"]));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("fans out to orchestration shortcuts", () => {
|
|
28
|
+
const values = piRogueArgumentCompletions("orchestration ")?.map((i) => i.value);
|
|
29
|
+
expect(values).toEqual(expect.arrayContaining(["goal", "loop", "autoresearch", "autoresearch-lab"]));
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
type CompletionItem = { value: string; label: string; description?: string };
|
|
2
|
+
|
|
3
|
+
function item(value: string, description?: string): CompletionItem {
|
|
4
|
+
return { value, label: value, ...(description ? { description } : {}) };
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function complete(values: Array<[string, string?]>, prefix: string): CompletionItem[] | null {
|
|
8
|
+
const q = prefix.trimStart().toLowerCase();
|
|
9
|
+
const items = values.map(([value, description]) => item(value, description));
|
|
10
|
+
const filtered = q
|
|
11
|
+
? items.filter((i) => i.value.startsWith(q))
|
|
12
|
+
: items;
|
|
13
|
+
return filtered.length > 0 ? filtered : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function completionsForPrefix(prefix: string, topLevel: Array<[string, string?]>, nested: Record<string, Array<[string, string?]>>): CompletionItem[] | null {
|
|
17
|
+
const q = prefix.trimStart().toLowerCase();
|
|
18
|
+
if (!q) return complete(topLevel, q);
|
|
19
|
+
|
|
20
|
+
const [head, ...rest] = q.split(/\s+/);
|
|
21
|
+
if (!head) return complete(topLevel, q);
|
|
22
|
+
|
|
23
|
+
if (rest.length === 0) {
|
|
24
|
+
const top = complete(topLevel, head);
|
|
25
|
+
if (top) return top;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const next = nested[head];
|
|
29
|
+
if (next) {
|
|
30
|
+
return complete(next, rest.join(" "));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return complete(topLevel, q);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const advisorTopLevel: Array<[string, string?]> = [
|
|
37
|
+
["status", "show status and configuration"],
|
|
38
|
+
["config", "show full config"],
|
|
39
|
+
["on", "enable auto mode"],
|
|
40
|
+
["off", "disable advisor"],
|
|
41
|
+
["mode", "set auto/manual/off"],
|
|
42
|
+
["review", "set light/strict/off"],
|
|
43
|
+
["checkins", "configure mid-hour check-ins"],
|
|
44
|
+
["checkin", "alias for checkins"],
|
|
45
|
+
["model", "set or inspect model override"],
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const advisorNested: Record<string, Array<[string, string?]>> = {
|
|
49
|
+
mode: [["auto"], ["manual"], ["off"]],
|
|
50
|
+
review: [["light"], ["strict"], ["off"]],
|
|
51
|
+
checkins: [["on"], ["off"], ["10"], ["15"], ["30"], ["60"]],
|
|
52
|
+
checkin: [["on"], ["off"], ["10"], ["15"], ["30"], ["60"]],
|
|
53
|
+
model: [["auto"], ["openai-codex/gpt-5.5"], ["anthropic/claude-opus-4-6"]],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const piRogueTopLevel: Array<[string, string?]> = [
|
|
57
|
+
["status", "show cockpit"],
|
|
58
|
+
["advisor", "advisor status and check-ins"],
|
|
59
|
+
["orchestration", "goal/loop/autoresearch shortcuts"],
|
|
60
|
+
["checkins", "advisor check-ins"],
|
|
61
|
+
["help", "show cockpit help"],
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const piRogueNested: Record<string, Array<[string, string?]>> = {
|
|
65
|
+
advisor: advisorTopLevel,
|
|
66
|
+
orchestration: [
|
|
67
|
+
["goal", "goal commands"],
|
|
68
|
+
["loop", "loop commands"],
|
|
69
|
+
["autoresearch", "solo research flow"],
|
|
70
|
+
["autoresearch-lab", "parallel research flow"],
|
|
71
|
+
["status", "show all surfaces"],
|
|
72
|
+
],
|
|
73
|
+
checkins: advisorNested.checkins,
|
|
74
|
+
help: [["advisor"], ["orchestration"], ["checkins"], ["status"]],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export function advisorArgumentCompletions(prefix: string): CompletionItem[] | null {
|
|
78
|
+
return completionsForPrefix(prefix, advisorTopLevel, advisorNested);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function piRogueArgumentCompletions(prefix: string): CompletionItem[] | null {
|
|
82
|
+
return completionsForPrefix(prefix, piRogueTopLevel, piRogueNested);
|
|
83
|
+
}
|
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,9 +1,11 @@
|
|
|
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";
|
|
5
6
|
import { Type } from "typebox";
|
|
6
7
|
import { featureFile, readText, truncate, writeText } from "./internal.js";
|
|
8
|
+
import { advisorArgumentCompletions, piRogueArgumentCompletions } from "./completions.js";
|
|
7
9
|
import {
|
|
8
10
|
appendRouteLog,
|
|
9
11
|
binaryGatePredict,
|
|
@@ -25,6 +27,10 @@ export interface AdvisorConfig {
|
|
|
25
27
|
mode: "auto" | "manual" | "off";
|
|
26
28
|
/** "light" (file changes/errors only) | "strict" (every 3 turns) | "off" */
|
|
27
29
|
review: "light" | "strict" | "off";
|
|
30
|
+
/** Opportunistic advisor check-ins during long sessions. */
|
|
31
|
+
checkins: "mid-hour" | "off";
|
|
32
|
+
/** Minutes between check-ins; bounded and cheap-gated by recent activity. */
|
|
33
|
+
checkinIntervalMinutes: number;
|
|
28
34
|
/** Optional model override. Auto-detects SOTA (gpt-5.5, claude-opus-4-6…) if unset */
|
|
29
35
|
model?: string;
|
|
30
36
|
}
|
|
@@ -32,6 +38,8 @@ export interface AdvisorConfig {
|
|
|
32
38
|
const DEFAULT_CONFIG: AdvisorConfig = {
|
|
33
39
|
mode: "auto",
|
|
34
40
|
review: "light",
|
|
41
|
+
checkins: "mid-hour",
|
|
42
|
+
checkinIntervalMinutes: 30,
|
|
35
43
|
};
|
|
36
44
|
|
|
37
45
|
const CONFIG_PATH = featureFile("advisor", "config.json");
|
|
@@ -44,6 +52,12 @@ const MAX_CACHE = 64;
|
|
|
44
52
|
const MAX_NOTES = 12;
|
|
45
53
|
const MAX_FILES = 8;
|
|
46
54
|
const MAX_ERRORS = 5;
|
|
55
|
+
const CHECKIN_POLL_MS = 5 * 60_000;
|
|
56
|
+
const MIN_CHECKIN_INTERVAL_MINUTES = 10;
|
|
57
|
+
const MAX_CHECKIN_INTERVAL_MINUTES = 240;
|
|
58
|
+
const checkinTimers = new Map<string, NodeJS.Timeout>();
|
|
59
|
+
const checkinStartedAt = new Map<string, number>();
|
|
60
|
+
const checkinLocks = new Set<string>();
|
|
47
61
|
|
|
48
62
|
// ── SOTA models (ordered by preference) ───────────────────────────────────
|
|
49
63
|
const SOTA_CHAIN: Array<{ provider: string; model: string; label: string }> = [
|
|
@@ -67,6 +81,11 @@ interface SessionState {
|
|
|
67
81
|
preflight?: AdvisorRouteDecision;
|
|
68
82
|
review?: AdvisorRouteDecision;
|
|
69
83
|
};
|
|
84
|
+
checkin: {
|
|
85
|
+
lastAt?: string;
|
|
86
|
+
lastTurn?: number;
|
|
87
|
+
lastReason?: string;
|
|
88
|
+
};
|
|
70
89
|
}
|
|
71
90
|
|
|
72
91
|
function defaultState(): SessionState {
|
|
@@ -80,6 +99,7 @@ function defaultState(): SessionState {
|
|
|
80
99
|
cacheHits: 0,
|
|
81
100
|
followUp: "",
|
|
82
101
|
router: {},
|
|
102
|
+
checkin: {},
|
|
83
103
|
};
|
|
84
104
|
}
|
|
85
105
|
|
|
@@ -96,15 +116,21 @@ function writeJson(path: string, v: unknown) {
|
|
|
96
116
|
writeText(path, JSON.stringify(v, null, 2) + "\n");
|
|
97
117
|
}
|
|
98
118
|
|
|
99
|
-
function
|
|
100
|
-
const
|
|
119
|
+
export function normalizeAdvisorConfig(raw: Partial<AdvisorConfig> = {}): AdvisorConfig {
|
|
120
|
+
const interval = Number(raw.checkinIntervalMinutes ?? DEFAULT_CONFIG.checkinIntervalMinutes);
|
|
101
121
|
return {
|
|
102
122
|
mode: (raw.mode === "manual" || raw.mode === "off") ? raw.mode : "auto",
|
|
103
123
|
review: (raw.review === "strict" || raw.review === "off") ? raw.review : "light",
|
|
124
|
+
checkins: raw.checkins === "off" ? "off" : "mid-hour",
|
|
125
|
+
checkinIntervalMinutes: Math.min(MAX_CHECKIN_INTERVAL_MINUTES, Math.max(MIN_CHECKIN_INTERVAL_MINUTES, Number.isFinite(interval) ? Math.round(interval) : DEFAULT_CONFIG.checkinIntervalMinutes)),
|
|
104
126
|
model: raw.model || undefined,
|
|
105
127
|
};
|
|
106
128
|
}
|
|
107
129
|
|
|
130
|
+
function loadConfig(): AdvisorConfig {
|
|
131
|
+
return normalizeAdvisorConfig(readJson<Partial<AdvisorConfig>>(CONFIG_PATH, {}));
|
|
132
|
+
}
|
|
133
|
+
|
|
108
134
|
function saveConfig(c: AdvisorConfig) {
|
|
109
135
|
writeJson(CONFIG_PATH, c);
|
|
110
136
|
}
|
|
@@ -124,6 +150,11 @@ function loadState(): SessionState {
|
|
|
124
150
|
preflight: raw.router?.preflight,
|
|
125
151
|
review: raw.router?.review,
|
|
126
152
|
},
|
|
153
|
+
checkin: {
|
|
154
|
+
lastAt: raw.checkin?.lastAt,
|
|
155
|
+
lastTurn: raw.checkin?.lastTurn,
|
|
156
|
+
lastReason: raw.checkin?.lastReason,
|
|
157
|
+
},
|
|
127
158
|
};
|
|
128
159
|
}
|
|
129
160
|
|
|
@@ -262,6 +293,97 @@ function mergeRouteReview(configReview: AdvisorConfig["review"], route?: ReviewP
|
|
|
262
293
|
return mergeReviewPolicy(configReview, route);
|
|
263
294
|
}
|
|
264
295
|
|
|
296
|
+
function sessionKey(ctx: any): string {
|
|
297
|
+
const sessionFile = ctx?.sessionManager?.getSessionFile?.();
|
|
298
|
+
if (!sessionFile) return "session";
|
|
299
|
+
return basename(String(sessionFile)).replace(/\.[^.]+$/, "");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function setPiRogueStatus(ctx: any, config = loadConfig(), state = loadState()): void {
|
|
303
|
+
const normalized = normalizeAdvisorConfig(config);
|
|
304
|
+
const checkin = normalized.checkins === "off" ? "checkins off" : `checkins ${normalized.checkinIntervalMinutes}m`;
|
|
305
|
+
const last = state.checkin.lastAt ? ` · last ${new Date(state.checkin.lastAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}` : "";
|
|
306
|
+
ctx.ui.setStatus("pi-rogue", `☠︎ advisor ${normalized.mode}/${normalized.review} · ${checkin}${last}`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function shouldRunCheckin(config: AdvisorConfig, state: SessionState, now = Date.now(), startedAt = now): string | null {
|
|
310
|
+
const normalized = normalizeAdvisorConfig(config);
|
|
311
|
+
if (normalized.mode === "off" || normalized.mode === "manual") return null;
|
|
312
|
+
if (normalized.checkins === "off") return null;
|
|
313
|
+
if (!state.lastTask && state.notes.length === 0) return null;
|
|
314
|
+
const lastTurn = state.checkin.lastTurn ?? 0;
|
|
315
|
+
if (state.turns <= lastTurn) return null;
|
|
316
|
+
const lastAt = state.checkin.lastAt ? Date.parse(state.checkin.lastAt) : 0;
|
|
317
|
+
const intervalMs = normalized.checkinIntervalMinutes * 60_000;
|
|
318
|
+
const since = lastAt || startedAt;
|
|
319
|
+
if (since && now - since < intervalMs) return null;
|
|
320
|
+
return `mid-hour check-in after ${state.turns - lastTurn} new turn(s)`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function stopCheckinTimer(key: string): void {
|
|
324
|
+
const timer = checkinTimers.get(key);
|
|
325
|
+
if (timer) {
|
|
326
|
+
clearInterval(timer);
|
|
327
|
+
checkinTimers.delete(key);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function maybeAdvisorCheckin(pi: ExtensionAPI, ctx: any, source: string): Promise<boolean> {
|
|
332
|
+
const key = sessionKey(ctx);
|
|
333
|
+
if (checkinLocks.has(key)) return false;
|
|
334
|
+
|
|
335
|
+
const config = loadConfig();
|
|
336
|
+
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;
|
|
341
|
+
|
|
342
|
+
checkinLocks.add(key);
|
|
343
|
+
try {
|
|
344
|
+
const response = await askAdvisor(
|
|
345
|
+
pi,
|
|
346
|
+
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,
|
|
350
|
+
);
|
|
351
|
+
if (response.error) return false;
|
|
352
|
+
|
|
353
|
+
const next = loadState();
|
|
354
|
+
next.checkin = { lastAt: new Date().toISOString(), lastTurn: next.turns, lastReason: reason };
|
|
355
|
+
saveState(next);
|
|
356
|
+
setPiRogueStatus(ctx, config, next);
|
|
357
|
+
sendAdvisorHint(pi, "review", "mid-hour check-in", response.text, [response.text]);
|
|
358
|
+
return true;
|
|
359
|
+
} finally {
|
|
360
|
+
checkinLocks.delete(key);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
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 {
|
|
375
|
+
const normalized = normalizeAdvisorConfig(config);
|
|
376
|
+
return [
|
|
377
|
+
"☠︎ PiRogue cockpit",
|
|
378
|
+
currentNote ? `Advisor: ${truncate(currentNote, 220)}` : "Advisor: no current note",
|
|
379
|
+
`Mode: ${normalized.mode} | Review: ${normalized.review} | Check-ins: ${normalized.checkins === "off" ? "off" : `${normalized.checkinIntervalMinutes}m`}`,
|
|
380
|
+
`Turns: ${state.turns} | Advisor calls: ${state.advisorCalls} | Cache hits: ${state.cacheHits}`,
|
|
381
|
+
state.checkin.lastAt ? `Last check-in: ${new Date(state.checkin.lastAt).toLocaleString()} (${state.checkin.lastReason || "mid-hour"})` : "Last check-in: never",
|
|
382
|
+
"",
|
|
383
|
+
"Commands: /advisor status · /advisor checkins on|off|<minutes> · /goal · /loop status · /autoresearch status",
|
|
384
|
+
].join("\n");
|
|
385
|
+
}
|
|
386
|
+
|
|
265
387
|
// ── Model resolution (auto-fallback through SOTA chain) ────────────────────
|
|
266
388
|
async function resolveModel(ctx: any, config: AdvisorConfig): Promise<{ model: any; auth: any; label: string } | null> {
|
|
267
389
|
// Try user's configured model first
|
|
@@ -417,12 +539,22 @@ async function doReview(pi: ExtensionAPI, ctx: any, trigger: string, delta: stri
|
|
|
417
539
|
// ── Extension entry point ──────────────────────────────────────────────────
|
|
418
540
|
|
|
419
541
|
export function registerAdvisor(pi: ExtensionAPI): void {
|
|
420
|
-
const config = loadConfig();
|
|
421
|
-
|
|
422
542
|
for (const customType of ["advisor:model", "advisor:rules", "advisor:llm"] as const) {
|
|
423
543
|
pi.registerMessageRenderer(customType, renderAdvisorHint);
|
|
424
544
|
}
|
|
425
545
|
|
|
546
|
+
pi.on("session_start", (_event, ctx) => {
|
|
547
|
+
syncCheckinTimer(pi, ctx);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
551
|
+
const key = sessionKey(ctx);
|
|
552
|
+
stopCheckinTimer(key);
|
|
553
|
+
checkinStartedAt.delete(key);
|
|
554
|
+
checkinLocks.delete(key);
|
|
555
|
+
ctx.ui.setStatus("pi-rogue", undefined);
|
|
556
|
+
});
|
|
557
|
+
|
|
426
558
|
// ── Tool ───────────────────────────────────────────────────────────────
|
|
427
559
|
pi.registerTool({
|
|
428
560
|
name: "advisor",
|
|
@@ -442,8 +574,10 @@ export function registerAdvisor(pi: ExtensionAPI): void {
|
|
|
442
574
|
|
|
443
575
|
// ── Preflight (heuristics only — no LLM call, <1ms) ──────────────────
|
|
444
576
|
pi.on("before_agent_start", async (event: any, ctx: any) => {
|
|
445
|
-
|
|
577
|
+
const cfg = loadConfig();
|
|
578
|
+
if (cfg.mode === "off" || cfg.mode === "manual") return { systemPrompt: event.systemPrompt };
|
|
446
579
|
const state = loadState();
|
|
580
|
+
setPiRogueStatus(ctx, cfg, state);
|
|
447
581
|
const prompt = typeof event.prompt === "string" && event.prompt.trim() ? squish(event.prompt, 1000) : "";
|
|
448
582
|
if (prompt) state.lastTask = prompt;
|
|
449
583
|
const briefText = brief(state);
|
|
@@ -499,7 +633,8 @@ export function registerAdvisor(pi: ExtensionAPI): void {
|
|
|
499
633
|
|
|
500
634
|
// ── Post-review (turn_end) ─────────────────────────────────────────────
|
|
501
635
|
pi.on("turn_end", async (event: any, ctx: any) => {
|
|
502
|
-
|
|
636
|
+
const cfg = loadConfig();
|
|
637
|
+
if (cfg.mode === "off") return;
|
|
503
638
|
const state = loadState();
|
|
504
639
|
state.turns++;
|
|
505
640
|
const tools = (event.toolResults || []).map((t: any) => String(t?.toolName || t?.name || "tool"));
|
|
@@ -508,15 +643,18 @@ export function registerAdvisor(pi: ExtensionAPI): void {
|
|
|
508
643
|
const text = squish(contentText(event.message?.content));
|
|
509
644
|
if (text && text !== state.notes[state.notes.length - 1]) state.notes.push(text);
|
|
510
645
|
saveState(state);
|
|
646
|
+
setPiRogueStatus(ctx, cfg, state);
|
|
647
|
+
void maybeAdvisorCheckin(pi, ctx, "turn_end");
|
|
511
648
|
|
|
512
|
-
if (
|
|
649
|
+
if (cfg.review !== "off") {
|
|
513
650
|
await doReview(pi, ctx, `turn-${state.turns}`, text, { fileChanged, failed, isAgentEnd: false });
|
|
514
651
|
}
|
|
515
652
|
});
|
|
516
653
|
|
|
517
654
|
// ── Post-review (agent_end) ────────────────────────────────────────────
|
|
518
655
|
pi.on("agent_end", async (event: any, ctx: any) => {
|
|
519
|
-
|
|
656
|
+
const cfg = loadConfig();
|
|
657
|
+
if (cfg.mode === "off" || cfg.review === "off") return;
|
|
520
658
|
const state = loadState();
|
|
521
659
|
const msgs = (event.messages || []).filter((m: any) => m.role === "assistant" || m.role === "toolResult");
|
|
522
660
|
const last = msgs[msgs.length - 1];
|
|
@@ -526,9 +664,59 @@ export function registerAdvisor(pi: ExtensionAPI): void {
|
|
|
526
664
|
await doReview(pi, ctx, "agent-end", delta, { fileChanged, failed, isAgentEnd: true });
|
|
527
665
|
});
|
|
528
666
|
|
|
667
|
+
// ── /pi-rogue cockpit ──────────────────────────────────────────────────
|
|
668
|
+
pi.registerCommand("pi-rogue", {
|
|
669
|
+
description: "Show PiRogue cockpit: advisor, check-ins, and orchestration command pointers",
|
|
670
|
+
getArgumentCompletions: (prefix: string) => piRogueArgumentCompletions(prefix),
|
|
671
|
+
handler: async (args, ctx) => {
|
|
672
|
+
const cfg = loadConfig();
|
|
673
|
+
const state = loadState();
|
|
674
|
+
const arg = String(args ?? "").trim().toLowerCase();
|
|
675
|
+
setPiRogueStatus(ctx, cfg, state);
|
|
676
|
+
|
|
677
|
+
if (!arg || arg === "status" || arg === "help") {
|
|
678
|
+
ctx.ui.notify(piRogueCockpitText(cfg, state, readText(CURRENT_PATH).trim()), "info");
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (arg.startsWith("advisor")) {
|
|
683
|
+
ctx.ui.notify([
|
|
684
|
+
"Advisor surface:",
|
|
685
|
+
" /advisor status",
|
|
686
|
+
" /advisor config",
|
|
687
|
+
" /advisor checkins on|off|<minutes>",
|
|
688
|
+
" /advisor <question>",
|
|
689
|
+
].join("\n"), "info");
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (arg.startsWith("orchestration")) {
|
|
694
|
+
ctx.ui.notify([
|
|
695
|
+
"Orchestration surface:",
|
|
696
|
+
" /goal show|clear|list|set <text>",
|
|
697
|
+
" /loop status|off|clear|stop|<interval> <instruction>",
|
|
698
|
+
" /autoresearch status|clear|<instruction>",
|
|
699
|
+
" /autoresearch-lab status|clear|<instruction>",
|
|
700
|
+
].join("\n"), "info");
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (arg.startsWith("checkins")) {
|
|
705
|
+
ctx.ui.notify([
|
|
706
|
+
`Advisor check-ins: ${cfg.checkins === "off" ? "off" : `${cfg.checkinIntervalMinutes}m`}`,
|
|
707
|
+
"Use /advisor checkins on|off|<minutes> to change it.",
|
|
708
|
+
].join("\n"), "info");
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
ctx.ui.notify(piRogueCockpitText(cfg, state, readText(CURRENT_PATH).trim()), "info");
|
|
713
|
+
},
|
|
714
|
+
});
|
|
715
|
+
|
|
529
716
|
// ── /advisor command ───────────────────────────────────────────────────
|
|
530
717
|
pi.registerCommand("advisor", {
|
|
531
718
|
description: "Senior engineering advisor. Usage: /advisor [on|off|status|config|question]",
|
|
719
|
+
getArgumentCompletions: (prefix: string) => advisorArgumentCompletions(prefix),
|
|
532
720
|
handler: async (args, ctx) => {
|
|
533
721
|
const a = String(args ?? "").trim().toLowerCase();
|
|
534
722
|
const [cmd, ...rest] = a.split(/\s+/);
|
|
@@ -543,21 +731,22 @@ export function registerAdvisor(pi: ExtensionAPI): void {
|
|
|
543
731
|
note ? `🧭 ${truncate(note, 200)}` : "",
|
|
544
732
|
route ? `Router: ${summarizeRoute(route)}${route.safety ? " · safety" : ""}` : "",
|
|
545
733
|
"",
|
|
546
|
-
`Mode: ${cfg.mode} | Review: ${cfg.review} | Model: ${resolved?.label || cfg.model || "auto"}`,
|
|
734
|
+
`Mode: ${cfg.mode} | Review: ${cfg.review} | Check-ins: ${cfg.checkins === "off" ? "off" : `${cfg.checkinIntervalMinutes}m`} | Model: ${resolved?.label || cfg.model || "auto"}`,
|
|
547
735
|
`Turns: ${state.turns} | Calls: ${state.advisorCalls} | Cache hits: ${state.cacheHits}`,
|
|
736
|
+
state.checkin.lastAt ? `Last check-in: ${new Date(state.checkin.lastAt).toLocaleString()} (${state.checkin.lastReason || "mid-hour"})` : "Last check-in: never",
|
|
548
737
|
"",
|
|
549
|
-
"Commands: /advisor on|off | /advisor status | /advisor config | <question>",
|
|
738
|
+
"Commands: /advisor on|off | /advisor status | /advisor checkins on|off|<minutes> | /advisor config | <question>",
|
|
550
739
|
"Tip: SOTA models auto-detected. No config needed.",
|
|
551
740
|
].filter(Boolean).join("\n"), "info");
|
|
552
741
|
return;
|
|
553
742
|
}
|
|
554
743
|
|
|
555
|
-
if (cmd === "on" && cfg.mode === "off") {
|
|
556
|
-
if (cmd === "off") {
|
|
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; }
|
|
557
746
|
if (cmd === "mode") {
|
|
558
747
|
const v = rest[0];
|
|
559
|
-
if (v === "auto" || v === "manual") {
|
|
560
|
-
if (v === "off") {
|
|
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; }
|
|
561
750
|
ctx.ui.notify("Usage: /advisor mode auto|manual|off", "error");
|
|
562
751
|
return;
|
|
563
752
|
}
|
|
@@ -580,9 +769,11 @@ export function registerAdvisor(pi: ExtensionAPI): void {
|
|
|
580
769
|
}
|
|
581
770
|
if (cmd === "config") {
|
|
582
771
|
ctx.ui.notify([
|
|
583
|
-
"Advisor config (
|
|
772
|
+
"Advisor config (5 fields, all optional):",
|
|
584
773
|
` mode: "${cfg.mode}" — auto (preflight+post+cache) | manual | off`,
|
|
585
774
|
` review: "${cfg.review}" — light (changes/errors) | strict (every 3) | off`,
|
|
775
|
+
` checkins: "${cfg.checkins}" — mid-hour | off`,
|
|
776
|
+
` checkinIntervalMinutes: ${cfg.checkinIntervalMinutes}`,
|
|
586
777
|
` model: "${cfg.model || "auto"}" — optional override`,
|
|
587
778
|
"",
|
|
588
779
|
"Router logs: evals/advisor-router.jsonl",
|
|
@@ -592,10 +783,38 @@ export function registerAdvisor(pi: ExtensionAPI): void {
|
|
|
592
783
|
}
|
|
593
784
|
if (cmd === "review") {
|
|
594
785
|
const v = rest[0];
|
|
595
|
-
if (v === "light" || v === "strict" || v === "off") {
|
|
786
|
+
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
787
|
ctx.ui.notify("Usage: /advisor review light|strict|off", "error");
|
|
597
788
|
return;
|
|
598
789
|
}
|
|
790
|
+
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");
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
599
818
|
|
|
600
819
|
// Anything else: treat as a question to the advisor
|
|
601
820
|
const r = await askAdvisor(pi, ctx, a, "slash", true);
|