@fiale-plus/pi-rogue-bundle 0.1.9 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +24 -13
  2. package/node_modules/@fiale-plus/pi-rogue-advisor/README.md +59 -0
  3. package/node_modules/@fiale-plus/pi-rogue-advisor/advisor/index.ts +1 -0
  4. package/node_modules/@fiale-plus/pi-rogue-advisor/assets/binary-gate-model.json +24026 -0
  5. package/node_modules/@fiale-plus/pi-rogue-advisor/package.json +50 -0
  6. package/node_modules/@fiale-plus/pi-rogue-advisor/skills/advisor/SKILL.md +51 -0
  7. package/node_modules/@fiale-plus/pi-rogue-advisor/src/completions.test.ts +28 -0
  8. package/node_modules/@fiale-plus/pi-rogue-advisor/src/completions.ts +79 -0
  9. package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.test.ts +257 -0
  10. package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.ts +1334 -0
  11. package/node_modules/@fiale-plus/pi-rogue-advisor/src/index.ts +3 -0
  12. package/node_modules/@fiale-plus/pi-rogue-advisor/src/internal.ts +48 -0
  13. package/node_modules/@fiale-plus/pi-rogue-advisor/src/loop-convergence.test.ts +301 -0
  14. package/node_modules/@fiale-plus/pi-rogue-advisor/src/preflight-signals.test.ts +22 -0
  15. package/node_modules/@fiale-plus/pi-rogue-advisor/src/preflight-signals.ts +21 -0
  16. package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.test.ts +78 -0
  17. package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.ts +516 -0
  18. package/node_modules/@fiale-plus/pi-rogue-orchestration/README.md +56 -0
  19. package/node_modules/@fiale-plus/pi-rogue-orchestration/orchestration/index.ts +1 -0
  20. package/node_modules/@fiale-plus/pi-rogue-orchestration/package.json +44 -0
  21. package/node_modules/@fiale-plus/pi-rogue-orchestration/skills/orchestration/SKILL.md +44 -0
  22. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.test.ts +142 -0
  23. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.ts +96 -0
  24. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch-state.ts +70 -0
  25. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch.test.ts +143 -0
  26. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch.ts +139 -0
  27. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/completions.test.ts +23 -0
  28. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/completions.ts +53 -0
  29. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/extension.ts +23 -0
  30. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal-resolution.ts +36 -0
  31. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.test.ts +182 -0
  32. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.ts +232 -0
  33. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/index.ts +1 -0
  34. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/internal.ts +98 -0
  35. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/loop.ts +274 -0
  36. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.test.ts +35 -0
  37. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.ts +145 -0
  38. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/state.ts +24 -0
  39. package/package.json +10 -2
@@ -0,0 +1,1334 @@
1
+ import { createHash } from "node:crypto";
2
+ import { homedir } from "node:os";
3
+ import { basename, join } from "node:path";
4
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
+ import { Box, Text } from "@earendil-works/pi-tui";
6
+ import { completeSimple, type ThinkingLevel } from "@earendil-works/pi-ai";
7
+ import { Type } from "typebox";
8
+ import { featureFile, readText, truncate, writeText } from "./internal.js";
9
+ import { advisorArgumentCompletions, piRogueArgumentCompletions } from "./completions.js";
10
+ import {
11
+ appendRouteLog,
12
+ binaryGatePredict,
13
+ formatAdvisorDisplay,
14
+ heuristicRoute,
15
+ mergeReviewPolicy,
16
+ routeNote,
17
+ summarizeRoute,
18
+ type AdvisorRouteDecision,
19
+ type AdvisorRouteInput,
20
+ type ReviewPolicy,
21
+ } from "./router.js";
22
+ import { classifyIntent, classifyMode } from "./preflight-signals.js";
23
+
24
+ // ── Config: 3 optional fields ────────────────────────────────────────────
25
+
26
+ export interface AdvisorConfig {
27
+ /** "auto" (preflight+post+cache), "manual" (just /advisor), "off" */
28
+ mode: "auto" | "manual" | "off";
29
+ /** "light" (file changes/errors only) | "strict" (every 3 turns) | "off" */
30
+ review: "light" | "strict" | "off";
31
+ /** Opportunistic advisor check-ins during long sessions. */
32
+ checkins: "mid-hour" | "off";
33
+ /** Minutes between check-ins; bounded and cheap-gated by recent activity. */
34
+ checkinIntervalMinutes: number;
35
+ /** Optional start time (ms since epoch) for the active check-in stream. */
36
+ checkinStartedAt?: number;
37
+ /** Optional model override. Auto-detects SOTA (gpt-5.5, claude-opus-4-6…) if unset */
38
+ model?: string;
39
+ }
40
+
41
+ const DEFAULT_CONFIG: AdvisorConfig = {
42
+ mode: "auto",
43
+ review: "light",
44
+ checkins: "off",
45
+ checkinIntervalMinutes: 30,
46
+ };
47
+
48
+ const CONFIG_PATH = featureFile("advisor", "config.json");
49
+ const STATE_PATH = featureFile("advisor", "state.json");
50
+ const CACHE_PATH = featureFile("advisor", "cache.json");
51
+ const CURRENT_PATH = featureFile("advisor", "current.md");
52
+ const HISTORY_PATH = featureFile("advisor", "history.jsonl");
53
+ const ORCHESTRATION_DIR = join(homedir(), ".pi", "agent", "fiale-plus", "orchestration");
54
+
55
+ const MAX_CACHE = 64;
56
+ const MAX_NOTES = 12;
57
+ const MAX_FILES = 8;
58
+ const MAX_ERRORS = 5;
59
+ const MIN_CHECKIN_INTERVAL_MINUTES = 10;
60
+ const MAX_CHECKIN_INTERVAL_MINUTES = 240;
61
+ const checkinLocks = new Set<string>();
62
+
63
+ // ── SOTA models (ordered by preference) ───────────────────────────────────
64
+ const SOTA_CHAIN: Array<{ provider: string; model: string; label: string }> = [
65
+ { provider: "openai-codex", model: "gpt-5.5", label: "GPT-5.5 (Codex)" },
66
+ { provider: "anthropic", model: "claude-opus-4-6", label: "Claude Opus 4.6" },
67
+ { provider: "anthropic", model: "claude-sonnet-4-6", label: "Claude Sonnet 4.6" },
68
+ { provider: "openai-codex", model: "gpt-5.4-mini", label: "GPT-5.4 Mini" },
69
+ ];
70
+
71
+ // ── Internal state ────────────────────────────────────────────────────────
72
+ interface SessionState {
73
+ turns: number;
74
+ lastTask: string;
75
+ notes: string[];
76
+ files: string[];
77
+ errors: string[];
78
+ advisorCalls: number;
79
+ cacheHits: number;
80
+ followUp: string;
81
+ router: {
82
+ preflight?: AdvisorRouteDecision;
83
+ review?: AdvisorRouteDecision;
84
+ };
85
+ checkin: {
86
+ lastAt?: string;
87
+ lastTurn?: number;
88
+ lastReason?: string;
89
+ queued?: boolean;
90
+ queuedReason?: string;
91
+ };
92
+ reviewControl: ReviewControlState;
93
+ advisorPauseUntilTurn?: number;
94
+ }
95
+ function defaultReviewControl(): ReviewControlState {
96
+ return {
97
+ status: "idle",
98
+ pending: false,
99
+ consumed: true,
100
+ running: false,
101
+ };
102
+ }
103
+
104
+ function defaultState(): SessionState {
105
+ return {
106
+ turns: 0,
107
+ lastTask: "",
108
+ notes: [],
109
+ files: [],
110
+ errors: [],
111
+ advisorCalls: 0,
112
+ cacheHits: 0,
113
+ followUp: "",
114
+ router: {},
115
+ checkin: { queued: false },
116
+ reviewControl: defaultReviewControl(),
117
+ };
118
+ }
119
+
120
+ // ── File I/O ──────────────────────────────────────────────────────────────
121
+ function readJson<T>(path: string, fallback: T): T {
122
+ try {
123
+ return JSON.parse(readText(path) || "null") ?? fallback;
124
+ } catch {
125
+ return fallback;
126
+ }
127
+ }
128
+
129
+ function writeJson(path: string, v: unknown) {
130
+ writeText(path, JSON.stringify(v, null, 2) + "\n");
131
+ }
132
+
133
+ export function normalizeAdvisorConfig(raw: Partial<AdvisorConfig> = {}): AdvisorConfig {
134
+ const interval = Number(raw.checkinIntervalMinutes ?? DEFAULT_CONFIG.checkinIntervalMinutes);
135
+ const startedAt = Number(raw.checkinStartedAt);
136
+ return {
137
+ mode: (raw.mode === "manual" || raw.mode === "off") ? raw.mode : "auto",
138
+ review: (raw.review === "strict" || raw.review === "off") ? raw.review : "light",
139
+ checkins: raw.checkins === "mid-hour" ? "mid-hour" : DEFAULT_CONFIG.checkins,
140
+ checkinIntervalMinutes: Math.min(
141
+ MAX_CHECKIN_INTERVAL_MINUTES,
142
+ Math.max(
143
+ MIN_CHECKIN_INTERVAL_MINUTES,
144
+ Number.isFinite(interval) ? Math.round(interval) : DEFAULT_CONFIG.checkinIntervalMinutes,
145
+ ),
146
+ ),
147
+ checkinStartedAt: Number.isFinite(startedAt) ? startedAt : undefined,
148
+ model: raw.model || undefined,
149
+ };
150
+ }
151
+
152
+ function loadConfig(): AdvisorConfig {
153
+ return normalizeAdvisorConfig(readJson<Partial<AdvisorConfig>>(CONFIG_PATH, {}));
154
+ }
155
+
156
+ function saveConfig(c: AdvisorConfig) {
157
+ writeJson(CONFIG_PATH, c);
158
+ }
159
+
160
+ function loadState(): SessionState {
161
+ const raw = readJson<Partial<SessionState>>(STATE_PATH, {});
162
+ const control = raw.reviewControl;
163
+ const pauseUntil = Number(raw.advisorPauseUntilTurn);
164
+ return {
165
+ turns: raw.turns ?? 0,
166
+ lastTask: raw.lastTask ?? "",
167
+ notes: (raw.notes ?? []).map(noteText).filter(Boolean).slice(-MAX_NOTES),
168
+ files: (raw.files ?? []).slice(-MAX_FILES),
169
+ errors: (raw.errors ?? []).slice(-MAX_ERRORS),
170
+ advisorCalls: raw.advisorCalls ?? 0,
171
+ cacheHits: raw.cacheHits ?? 0,
172
+ followUp: raw.followUp ?? "",
173
+ router: {
174
+ preflight: raw.router?.preflight,
175
+ review: raw.router?.review,
176
+ },
177
+ checkin: {
178
+ lastAt: raw.checkin?.lastAt,
179
+ lastTurn: raw.checkin?.lastTurn,
180
+ lastReason: raw.checkin?.lastReason,
181
+ queued: Boolean(raw.checkin?.queued),
182
+ queuedReason: raw.checkin?.queuedReason,
183
+ },
184
+ reviewControl: {
185
+ status: (control?.status === "needed" || control?.status === "running" || control?.status === "consumed" || control?.status === "idle") ? control.status : "idle",
186
+ pending: Boolean(control?.pending),
187
+ consumed: control?.consumed !== false,
188
+ running: Boolean(control?.running),
189
+ lastDecision: control?.lastDecision,
190
+ lastMaterialSignature: control?.lastMaterialSignature,
191
+ lastReason: control?.lastReason,
192
+ lastTrigger: control?.lastTrigger,
193
+ lastAppliedAt: control?.lastAppliedAt,
194
+ },
195
+ advisorPauseUntilTurn: Number.isFinite(pauseUntil) ? pauseUntil : undefined,
196
+ };
197
+ }
198
+
199
+ function saveState(s: SessionState) {
200
+ writeJson(STATE_PATH, s);
201
+ }
202
+
203
+ function loadCache(): Record<string, string> {
204
+ return readJson<Record<string, string>>(CACHE_PATH, {});
205
+ }
206
+
207
+ function saveCache(c: Record<string, string>) {
208
+ const entries = Object.entries(c);
209
+ if (entries.length > MAX_CACHE) {
210
+ entries.sort((a, b) => a[0].localeCompare(b[0]));
211
+ for (const [k] of entries.slice(0, entries.length - MAX_CACHE)) delete c[k];
212
+ }
213
+ writeJson(CACHE_PATH, c);
214
+ }
215
+
216
+ // ── Prompts ───────────────────────────────────────────────────────────────
217
+
218
+ const ADVISOR_SYSTEM = `You are a senior engineering advisor. Use the session brief only. Return terse, specific advice with concrete recommendations. 200 words max.`;
219
+
220
+ const REVIEW_SYSTEM = `You are a senior reviewer. An AI agent just completed work. Assess it. Return ONLY valid JSON:
221
+ {
222
+ "verdict": "on_track"|"course_correct"|"not_done",
223
+ "summary": "1-2 sentence assessment",
224
+ "actions": ["action1"],
225
+ "checklist": ["item"],
226
+ "notify": false
227
+ }`;
228
+
229
+ // ── Helpers ───────────────────────────────────────────────────────────────
230
+
231
+ function hash(...parts: string[]): string {
232
+ return createHash("sha256").update(parts.join("||")).digest("hex").slice(0, 16);
233
+ }
234
+
235
+ function brief(s: SessionState): string {
236
+ const lines: string[] = [];
237
+ if (s.lastTask) lines.push(`Task: ${truncate(s.lastTask, 200)}`);
238
+ if (s.turns) lines.push(`Turns: ${s.turns}`);
239
+ if (s.notes.length) { lines.push("Notes:"); s.notes.slice(-4).forEach(n => lines.push(`- ${truncate(n, 200)}`)); }
240
+ if (s.files.length) lines.push(`Files: ${s.files.slice(-4).join(", ")}`);
241
+ if (s.errors.length) lines.push(`Errors: ${s.errors.slice(-2).join(" | ")}`);
242
+ return lines.join("\n").slice(0, 1200);
243
+ }
244
+
245
+ function squish(t: unknown, max = 200): string {
246
+ const s = String(t ?? "").replace(/\s+/g, " ").trim();
247
+ return s.length <= max ? s : s.slice(0, max - 1).trimEnd() + "…";
248
+ }
249
+
250
+ function noteText(note: unknown): string {
251
+ const text = contentText(note);
252
+ if (/^\[object Object\](,\[object Object\])*$/.test(text)) return "";
253
+ if (text) return squish(text, 500);
254
+ if (note && typeof note === "object") return squish(JSON.stringify(note), 500);
255
+ return text;
256
+ }
257
+
258
+ function normalizeReviewSignals(materialSignals: string[] = []): string[] {
259
+ return [...new Set(materialSignals.filter(Boolean).map((signal) => squish(signal)))].sort();
260
+ }
261
+
262
+ function reviewMaterialSignature(state: SessionState, delta: string, meta: ReviewMaterialMeta): string {
263
+ const signals = normalizeReviewSignals(meta.materialSignals);
264
+ return hash(
265
+ "rev",
266
+ state.lastTask || "",
267
+ String(meta.isAgentEnd),
268
+ String(meta.fileChanged),
269
+ String(meta.failed),
270
+ delta || "(none)",
271
+ ...signals,
272
+ );
273
+ }
274
+
275
+ function shouldSkipReview(state: SessionState, signature: string): boolean {
276
+ return Boolean(signature && state.reviewControl.lastMaterialSignature === signature && !state.reviewControl.running);
277
+ }
278
+
279
+ function consumeReviewFollowUp(state: SessionState): void {
280
+ state.followUp = "";
281
+ state.reviewControl = {
282
+ ...state.reviewControl,
283
+ status: "consumed",
284
+ pending: false,
285
+ consumed: true,
286
+ running: false,
287
+ lastAppliedAt: new Date().toISOString(),
288
+ };
289
+ }
290
+
291
+ function markReviewSkipped(state: SessionState, signature: string, trigger: string): void {
292
+ state.reviewControl = {
293
+ ...state.reviewControl,
294
+ status: "consumed",
295
+ running: false,
296
+ consumed: true,
297
+ pending: false,
298
+ lastMaterialSignature: signature,
299
+ lastDecision: "defer",
300
+ lastTrigger: trigger,
301
+ lastReason: "repeated material snapshot",
302
+ lastAppliedAt: new Date().toISOString(),
303
+ };
304
+ }
305
+
306
+ function markReviewRunning(state: SessionState, signature: string, trigger: string): void {
307
+ state.reviewControl = {
308
+ ...state.reviewControl,
309
+ status: "running",
310
+ running: true,
311
+ pending: true,
312
+ consumed: false,
313
+ lastMaterialSignature: signature,
314
+ lastTrigger: trigger,
315
+ };
316
+ }
317
+
318
+ function markReviewApplied(state: SessionState, signature: string, trigger: string, decision: "continue" | "review" | "defer", reason: string, consumed: boolean): void {
319
+ state.reviewControl = {
320
+ ...state.reviewControl,
321
+ status: consumed ? "consumed" : "needed",
322
+ running: false,
323
+ pending: !consumed,
324
+ consumed,
325
+ lastMaterialSignature: signature,
326
+ lastDecision: decision,
327
+ lastTrigger: trigger,
328
+ lastReason: reason,
329
+ lastAppliedAt: new Date().toISOString(),
330
+ };
331
+ }
332
+
333
+ function persistReviewState(state: SessionState, includeReviewRoute: boolean): void {
334
+ const persisted = loadState();
335
+ persisted.reviewControl = state.reviewControl;
336
+ persisted.followUp = state.followUp;
337
+ persisted.advisorPauseUntilTurn = state.advisorPauseUntilTurn;
338
+ if (includeReviewRoute && state.router.review) {
339
+ persisted.router.review = state.router.review;
340
+ }
341
+ saveState(persisted);
342
+ }
343
+
344
+ function recoverReviewControl(state: SessionState): void {
345
+ if (!state.reviewControl.running) return;
346
+
347
+ const pending = Boolean(state.reviewControl.pending);
348
+ state.reviewControl = {
349
+ ...state.reviewControl,
350
+ running: false,
351
+ status: pending ? "needed" : state.reviewControl.status === "needed" ? "needed" : "idle",
352
+ consumed: !pending,
353
+ lastAppliedAt: new Date().toISOString(),
354
+ };
355
+ }
356
+
357
+ type AdvisorHintDetails = {
358
+ decision?: "continue" | "review" | "defer";
359
+ reason?: string;
360
+ summary?: string;
361
+ actions?: string[];
362
+ };
363
+
364
+ type ReviewControlState = {
365
+ status: "idle" | "needed" | "running" | "consumed";
366
+ pending: boolean;
367
+ consumed: boolean;
368
+ running: boolean;
369
+ lastDecision?: "continue" | "review" | "defer";
370
+ lastMaterialSignature?: string;
371
+ lastReason?: string;
372
+ lastTrigger?: string;
373
+ lastAppliedAt?: string;
374
+ };
375
+
376
+ type ReviewMaterialMeta = {
377
+ fileChanged: boolean;
378
+ failed: boolean;
379
+ isAgentEnd: boolean;
380
+ materialSignals?: string[];
381
+ };
382
+ function sendAdvisorHint(pi: ExtensionAPI, decision: "continue" | "review" | "defer", reason: string, summary: string, actions: string[] = []) {
383
+ pi.sendMessage(
384
+ {
385
+ customType: "advisor:llm",
386
+ content: reason,
387
+ display: true,
388
+ details: { decision, reason, summary, actions: actions.slice(0, 2) },
389
+ },
390
+ { deliverAs: "followUp" },
391
+ );
392
+ }
393
+
394
+ function renderAdvisorHint(message: any, options: { expanded?: boolean }, theme: any) {
395
+ const details = (message?.details ?? {}) as AdvisorHintDetails;
396
+ const customType = String(message?.customType ?? "advisor:rules");
397
+ const decision = details.decision ?? "defer";
398
+ const sourceColor = customType === "advisor:llm" ? "success" : customType === "advisor:model" ? "accent" : "muted";
399
+ const decisionColor = decision === "review" ? "accent" : decision === "continue" ? "muted" : "dim";
400
+ const source = theme.bold(theme.fg(sourceColor, `[${customType}]`));
401
+ const verdict = theme.bold(theme.fg(decisionColor, decision));
402
+ const glyph = decision === "review" ? "↗" : decision === "defer" ? "…" : "·";
403
+ const reason = squish(details.reason || contentText(message?.content) || "no extra detail", 180);
404
+
405
+ const box = new Box(1, 1, (s: string) => theme.bg("customMessageBg", s));
406
+ box.addChild(new Text(`${theme.bold(theme.fg(decisionColor, glyph))} ${source} ${verdict} · ${theme.fg("dim", "reason: ")}${reason}`, 0, 0));
407
+
408
+ if (options.expanded && details.summary) {
409
+ box.addChild(new Text(theme.fg("dim", `summary: ${squish(details.summary, 220)}`), 0, 0));
410
+ }
411
+ if (options.expanded && details.actions?.length) {
412
+ box.addChild(new Text(theme.fg("dim", `actions: ${details.actions.map((a) => squish(a, 80)).join(" • ")}`), 0, 0));
413
+ }
414
+
415
+ return box;
416
+ }
417
+
418
+ /** Extract readable text from message content (handles strings, blocks, and nested message payloads). */
419
+ export function contentText(content: unknown): string {
420
+ if (typeof content === "string") return content.trim();
421
+ if (content && typeof content === "object" && !Array.isArray(content)) {
422
+ const obj = content as Record<string, unknown>;
423
+ if (typeof obj.text === "string") return obj.text.trim();
424
+ if (obj.content !== undefined) return contentText(obj.content);
425
+ if (obj.message !== undefined) return contentText(obj.message);
426
+ return "";
427
+ }
428
+ if (!Array.isArray(content)) return String(content ?? "").trim();
429
+ const parts: string[] = [];
430
+ for (const item of content) {
431
+ if (!item) continue;
432
+ if (typeof item === "string") { parts.push(item); continue; }
433
+ const obj = item as Record<string, unknown>;
434
+ if (obj.type === "text" && typeof obj.text === "string") parts.push(obj.text);
435
+ else if (typeof obj.text === "string") parts.push(obj.text);
436
+ else if (obj.content !== undefined) {
437
+ const nested = contentText(obj.content);
438
+ if (nested) parts.push(nested);
439
+ }
440
+ else if (obj.message !== undefined) {
441
+ const nested = contentText(obj.message);
442
+ if (nested) parts.push(nested);
443
+ }
444
+ }
445
+ return parts.join("\n").replace(/\s+/g, " ").trim();
446
+ }
447
+
448
+ /** Check if a tool result or message indicates an actual execution failure */
449
+ function isActualFailure(tool: any): boolean {
450
+ if (tool?.isError === true) return true;
451
+ if (tool?.status === "error" || tool?.status === "failure") return true;
452
+ if (tool?.error && String(tool.error).length > 0) return true;
453
+ return false;
454
+ }
455
+
456
+ function responseText(resp: { content?: Array<{ type?: string; text?: string }> } | null | undefined): string {
457
+ return (resp?.content ?? []).filter((b: any) => b?.type === "text").map((b: any) => b.text).join("\n").trim();
458
+ }
459
+
460
+ function mergeRouteReview(configReview: AdvisorConfig["review"], route?: ReviewPolicy): ReviewPolicy {
461
+ if (configReview === "off") return "off";
462
+ if (!route) return configReview;
463
+ return mergeReviewPolicy(configReview, route);
464
+ }
465
+
466
+ function sessionKey(ctx: any): string {
467
+ const sessionFile = ctx?.sessionManager?.getSessionFile?.();
468
+ if (!sessionFile) return "session";
469
+ return basename(String(sessionFile)).replace(/\.[^.]+$/, "");
470
+ }
471
+
472
+ type OrchestrationSnapshot = {
473
+ goal: string;
474
+ loop: { enabled?: boolean; interval?: string; instruction?: string };
475
+ research: { instruction?: string; interval?: string; cycles?: number; lastResult?: string };
476
+ };
477
+
478
+ function readOrchestrationSnapshot(ctx: any): OrchestrationSnapshot {
479
+ const dir = join(ORCHESTRATION_DIR, sessionKey(ctx));
480
+ return {
481
+ goal: readText(join(dir, "goal.md")).trim(),
482
+ loop: readJson(join(dir, "loop.json"), {}),
483
+ research: readJson(join(dir, "autoresearch.json"), {}),
484
+ };
485
+ }
486
+
487
+ function orchestrationSnapshotText(ctx: any): string {
488
+ const snapshot = readOrchestrationSnapshot(ctx);
489
+ const goalActive = Boolean(snapshot.goal);
490
+ const loopActive = Boolean(snapshot.loop.enabled && snapshot.loop.instruction);
491
+ const researchActive = Boolean(snapshot.research.instruction);
492
+ const status = goalActive && !loopActive && !researchActive
493
+ ? "setup gap — goal exists but no active autoresearch/loop progression"
494
+ : goalActive
495
+ ? "progression configured"
496
+ : "no active goal";
497
+ return [
498
+ "Orchestration:",
499
+ `- Goal: ${goalActive ? `active — ${truncate(snapshot.goal, 360)}` : "off"}`,
500
+ `- Autoresearch: ${researchActive ? `active — ${truncate(snapshot.research.instruction || "", 240)}; cycles=${snapshot.research.cycles ?? 0}${snapshot.research.lastResult ? `, last=${snapshot.research.lastResult}` : ""}` : "off"}`,
501
+ `- Loop: ${loopActive ? `active every ${snapshot.loop.interval || "?"} — ${truncate(snapshot.loop.instruction || "", 260)}` : "off"}`,
502
+ `- Status: ${status}`,
503
+ ].join("\n");
504
+ }
505
+
506
+ export function buildAdvisorCheckinPrompt(source: string, orchestration: string, sessionBrief: string): string {
507
+ return [
508
+ `Mid-session check-in (${source})`,
509
+ "Role: alignment reviewer for the active work. Do not create a new task, research direction, benchmark, script, artifact, or model switch unless the active goal explicitly asks for it.",
510
+ "Stay anchored to the active goal/autoresearch/loop. If autoresearch is active, preserve its research question and judge whether the latest work is gathering evidence toward that question.",
511
+ "Bad nudge examples: research the existence of weaknesses instead of solving the named weakness; create a script/report about weaknesses when the goal is to fix advisor behavior; swap to a shallower research mode.",
512
+ "Return exactly two short lines:",
513
+ "Status: on_track|stuck|off_track - <why, tied to the active goal>",
514
+ "Nudge: <one concrete next action that continues the active goal>",
515
+ orchestration,
516
+ sessionBrief ? `Session brief:\n${sessionBrief}` : "",
517
+ ].filter(Boolean).join("\n\n");
518
+ }
519
+
520
+ function advisorPauseRemaining(state: SessionState, nowTurns = state.turns): number {
521
+ const until = state.advisorPauseUntilTurn;
522
+ if (until === undefined || Number.isNaN(until)) return 0;
523
+ return Math.max(0, until - nowTurns);
524
+ }
525
+
526
+ function isAdvisorPaused(state: SessionState, nowTurns = state.turns): boolean {
527
+ return advisorPauseRemaining(state, nowTurns) > 0;
528
+ }
529
+
530
+ function isAdvisorAutoRunSuppressed(state: SessionState, nowTurns = state.turns): boolean {
531
+ return isAdvisorPaused(state, nowTurns);
532
+ }
533
+
534
+ function isAdvisorAutoRunSuppressedForTurnContext(state: SessionState, nowTurns = state.turns): boolean {
535
+ return isAdvisorAutoRunSuppressed(state, nowTurns) || isAdvisorAutoRunSuppressed(state, nowTurns - 1);
536
+ }
537
+
538
+ function checkinDescription(config: AdvisorConfig): string {
539
+ if (config.checkins === "off") return "checkins off";
540
+ return `checkins ${config.checkinIntervalMinutes}m`;
541
+ }
542
+
543
+ function setPiRogueStatus(ctx: any, config = loadConfig(), state = loadState()): void {
544
+ const normalized = normalizeAdvisorConfig(config);
545
+ const checkin = checkinDescription(normalized);
546
+ const pause = advisorPauseRemaining(state, state.turns);
547
+ const pauseText = pause > 0 ? ` · pause ${pause} turn${pause === 1 ? "" : "s"}` : "";
548
+ const last = state.checkin.lastAt ? ` · last ${new Date(state.checkin.lastAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}` : "";
549
+ ctx.ui.setStatus("pi-rogue", `☠︎ advisor ${normalized.mode}/${normalized.review} · ${checkin}${pauseText}${last}`);
550
+ }
551
+
552
+ export function shouldRunCheckin(config: AdvisorConfig, state: SessionState, now = Date.now(), startedAt = now): string | null {
553
+ if (isAdvisorAutoRunSuppressed(state, state.turns)) return null;
554
+ const normalized = normalizeAdvisorConfig(config);
555
+ if (normalized.mode === "off" || normalized.mode === "manual") return null;
556
+ if (normalized.checkins === "off") return null;
557
+ if (state.checkin.queued) {
558
+ return state.checkin.queuedReason || "Queued mid-session check-in.";
559
+ }
560
+ if (!state.lastTask && state.notes.length === 0) return null;
561
+
562
+ const lastTurn = state.checkin.lastTurn ?? 0;
563
+ if (state.turns <= lastTurn) return null;
564
+
565
+ const lastAt = state.checkin.lastAt ? Date.parse(state.checkin.lastAt) : 0;
566
+ const intervalMs = normalized.checkinIntervalMinutes * 60_000;
567
+ const streamStartedAt = Number.isFinite(normalized.checkinStartedAt ?? NaN)
568
+ ? (normalized.checkinStartedAt as number)
569
+ : startedAt;
570
+ const since = Math.max(lastAt, streamStartedAt);
571
+ if (since && now - since < intervalMs) return null;
572
+ return `mid-hour check-in after ${state.turns - lastTurn} new turn(s)`;
573
+ }
574
+
575
+
576
+ function isAdvisorIdle(ctx: any): boolean {
577
+ try {
578
+ return typeof ctx?.isIdle === "function" ? ctx.isIdle() : true;
579
+ } catch {
580
+ return true;
581
+ }
582
+ }
583
+
584
+ export async function requestAdvisorLoopCheckin(pi: ExtensionAPI, ctx: any, source = "loop_tick"): Promise<boolean> {
585
+ return maybeAdvisorCheckin(pi, ctx, source);
586
+ }
587
+
588
+ async function maybeAdvisorCheckin(pi: ExtensionAPI, ctx: any, source: string): Promise<boolean> {
589
+ const key = sessionKey(ctx);
590
+ if (checkinLocks.has(key)) return false;
591
+
592
+ const config = loadConfig();
593
+ const state = loadState();
594
+ const reason = shouldRunCheckin(config, state, Date.now(), Date.now());
595
+ if (!reason) {
596
+ if (state.checkin.queued) {
597
+ state.checkin.queued = false;
598
+ saveState(state);
599
+ setPiRogueStatus(ctx, config, state);
600
+ }
601
+ return false;
602
+ }
603
+
604
+ if (!isAdvisorIdle(ctx)) {
605
+ if (!state.checkin.queued) {
606
+ state.checkin.queued = true;
607
+ state.checkin.queuedReason = reason;
608
+ saveState(state);
609
+ setPiRogueStatus(ctx, config, state);
610
+ }
611
+ return false;
612
+ }
613
+
614
+ checkinLocks.add(key);
615
+ try {
616
+ const prompt = buildAdvisorCheckinPrompt(source, orchestrationSnapshotText(ctx), brief(state));
617
+ const completed = await completeWithHigherAdvisorModel(
618
+ ctx,
619
+ config,
620
+ prompt,
621
+ [
622
+ {
623
+ role: "user",
624
+ content: prompt,
625
+ timestamp: new Date().toISOString(),
626
+ },
627
+ ],
628
+ { maxTokens: 260, reasoning: "low" as ThinkingLevel },
629
+ );
630
+ if (!completed) return false;
631
+
632
+ const next = loadState();
633
+ next.checkin = {
634
+ lastAt: new Date().toISOString(),
635
+ lastTurn: next.turns,
636
+ lastReason: reason,
637
+ queued: false,
638
+ };
639
+ saveState(next);
640
+ setPiRogueStatus(ctx, config, next);
641
+ sendAdvisorHint(pi, "review", "mid-hour check-in", completed.text, [completed.text]);
642
+ return true;
643
+ } finally {
644
+ checkinLocks.delete(key);
645
+ }
646
+ }
647
+
648
+ function piRogueCockpitText(config: AdvisorConfig, state: SessionState, currentNote: string, orchestration = ""): string {
649
+ const normalized = normalizeAdvisorConfig(config);
650
+ const pause = advisorPauseRemaining(state, state.turns);
651
+ return [
652
+ "☠︎ Pi-Rogue cockpit",
653
+ currentNote ? `Advisor: ${truncate(currentNote, 220)}` : "Advisor: no current note",
654
+ `Mode: ${normalized.mode} | Review: ${normalized.review} | Check-ins: ${checkinDescription(normalized)}`,
655
+ pause > 0 ? `Advisor pause: ${pause} turn${pause === 1 ? "" : "s"} remaining` : "Advisor pause: off",
656
+ `Turns: ${state.turns} | Advisor calls: ${state.advisorCalls} | Cache hits: ${state.cacheHits}`,
657
+ state.checkin.lastAt ? `Last check-in: ${new Date(state.checkin.lastAt).toLocaleString()} (${state.checkin.lastReason || "mid-hour"})` : "Last check-in: never",
658
+ state.checkin.queued ? `Queued check-in: ${state.checkin.queuedReason || "due"}` : "",
659
+ orchestration,
660
+ "",
661
+ "Commands: /advisor status · /goal · /loop status · /autoresearch status",
662
+ ].filter(Boolean).join("\n");
663
+ }
664
+
665
+ // ── Model resolution (higher/advanced first, then optional regular fallback) ──
666
+ type ResolvedAdvisorModel = { model: any; auth: any; label: string; fallback?: boolean };
667
+ type ModelResolutionOptions = { allowRegularFallback?: boolean };
668
+
669
+ export async function resolveModelCandidates(ctx: any, config: AdvisorConfig, options: ModelResolutionOptions = {}): Promise<ResolvedAdvisorModel[]> {
670
+ const { allowRegularFallback = true } = options;
671
+ const candidates: ResolvedAdvisorModel[] = [];
672
+ const seen = new Set<string>();
673
+ const add = async (found: any, label: string, fallback = false) => {
674
+ if (!found) return;
675
+ const key = String(found.id || label);
676
+ if (seen.has(key)) return;
677
+ const auth = await ctx.modelRegistry?.getApiKeyAndHeaders(found);
678
+ if (auth?.ok && auth.apiKey) {
679
+ seen.add(key);
680
+ candidates.push({ model: found, auth, label, fallback });
681
+ }
682
+ };
683
+
684
+ // Try configured higher/advanced advisor model first.
685
+ if (config.model && config.model.includes("/")) {
686
+ const [p, ...m] = config.model.split("/");
687
+ await add(ctx.modelRegistry?.find(p, m.join("/")), p + "/" + m.join("/"));
688
+ }
689
+
690
+ // Fall through SOTA chain.
691
+ for (const sota of SOTA_CHAIN) {
692
+ await add(ctx.modelRegistry?.find(sota.provider, sota.model), sota.label);
693
+ }
694
+
695
+ if (allowRegularFallback) {
696
+ // Final fallback: any configured text model, i.e. the regular session-capable model.
697
+ for (const m of (ctx.modelRegistry?.getAvailable() ?? []).filter((model: any) => model.input?.includes?.("text"))) {
698
+ await add(m, m.id || "regular model", true);
699
+ }
700
+ }
701
+
702
+ return candidates;
703
+ }
704
+
705
+ async function resolveModel(ctx: any, config: AdvisorConfig): Promise<ResolvedAdvisorModel | null> {
706
+ return (await resolveModelCandidates(ctx, config))[0] ?? null;
707
+ }
708
+
709
+ export async function completeWithModelFallback(ctx: any, config: AdvisorConfig, systemPrompt: string, messages: any[], options: { maxTokens: number; reasoning: ThinkingLevel }): Promise<{ text: string; model: string; fallback?: boolean } | null> {
710
+ let lastError = "";
711
+ for (const resolved of await resolveModelCandidates(ctx, config)) {
712
+ try {
713
+ const resp = await completeSimple(resolved.model, { systemPrompt, messages }, {
714
+ apiKey: resolved.auth.apiKey,
715
+ headers: resolved.auth.headers,
716
+ maxTokens: options.maxTokens,
717
+ reasoning: options.reasoning,
718
+ });
719
+ return { text: responseText(resp) || "(empty)", model: resolved.label, fallback: resolved.fallback };
720
+ } catch (error) {
721
+ lastError = error instanceof Error ? error.message : String(error);
722
+ }
723
+ }
724
+ return lastError ? { text: `No advisor/check-in model completed successfully (${lastError}).`, model: "none" } : null;
725
+ }
726
+
727
+ export async function completeWithHigherAdvisorModel(
728
+ ctx: any,
729
+ config: AdvisorConfig,
730
+ systemPrompt: string,
731
+ messages: any[],
732
+ options: { maxTokens: number; reasoning: ThinkingLevel; allowRegularFallback?: boolean },
733
+ ): Promise<{ text: string; model: string } | null> {
734
+ const { allowRegularFallback = true } = options;
735
+ for (const resolved of await resolveModelCandidates(ctx, config, { allowRegularFallback })) {
736
+ try {
737
+ const resp = await completeSimple(resolved.model, { systemPrompt, messages }, {
738
+ apiKey: resolved.auth.apiKey,
739
+ headers: resolved.auth.headers,
740
+ maxTokens: options.maxTokens,
741
+ reasoning: options.reasoning,
742
+ });
743
+ return { text: responseText(resp) || "(empty)", model: resolved.label };
744
+ } catch {
745
+ // keep trying remaining candidates
746
+ }
747
+ }
748
+ return null;
749
+ }
750
+
751
+ async function askAdvisor(pi: ExtensionAPI, ctx: any, question: string, scope: string, includeWork: boolean) {
752
+ const config = loadConfig();
753
+ const state = loadState();
754
+ if (!question.trim()) return { text: "Ask a question.", error: "empty" };
755
+
756
+ const ck = hash("adv", config.model ?? "auto", squish(question, 300), includeWork ? brief(state) : "");
757
+ const cache = loadCache();
758
+ if (cache[ck]) { state.cacheHits++; saveState(state); return { text: cache[ck], cached: true }; }
759
+
760
+ const msgs = [
761
+ { role: "user", content: [ `Question: ${question}`, scope ? `Scope: ${scope}` : "", includeWork && brief(state) ? `Session:\n${brief(state)}` : "" ].filter(Boolean).join("\n"), timestamp: new Date().toISOString() },
762
+ ] as any[];
763
+
764
+ const completed = await completeWithModelFallback(ctx, config, ADVISOR_SYSTEM, msgs, { maxTokens: 600, reasoning: "medium" as ThinkingLevel });
765
+ if (!completed) return { text: "No model available. Install one via pi config.", error: "no_model" };
766
+ const text = completed.text;
767
+ if (text && text !== "(empty)") { cache[ck] = text; saveCache(cache); }
768
+ state.advisorCalls++;
769
+ saveState(state);
770
+ return { text, model: completed.model, fallback: completed.fallback };
771
+ }
772
+
773
+ async function doReview(pi: ExtensionAPI, ctx: any, trigger: string, delta: string, meta: ReviewMaterialMeta) {
774
+ const config = loadConfig();
775
+ if (config.review === "off") return;
776
+ const state = loadState();
777
+
778
+ const signature = reviewMaterialSignature(state, delta, meta);
779
+ if (state.reviewControl.running) {
780
+ return;
781
+ }
782
+ if (shouldSkipReview(state, signature)) {
783
+ markReviewSkipped(state, signature, trigger);
784
+ persistReviewState(state, false);
785
+ return;
786
+ }
787
+
788
+ markReviewRunning(state, signature, trigger);
789
+ persistReviewState(state, false);
790
+
791
+ let finalized = false;
792
+ let finalDecision: "continue" | "review" | "defer" = "defer";
793
+ let finalReason = "pending review";
794
+
795
+ try {
796
+ const phase: AdvisorRouteInput["phase"] = meta.isAgentEnd ? "closeout" : "review";
797
+ const reviewInput: AdvisorRouteInput = {
798
+ phase,
799
+ text: delta || "(none)",
800
+ brief: brief(state),
801
+ fileChanged: meta.fileChanged,
802
+ failed: meta.failed,
803
+ };
804
+ const reviewHeuristic = heuristicRoute(reviewInput);
805
+ const gatePrediction = binaryGatePredict(reviewInput.text);
806
+ let reviewRoute = reviewHeuristic;
807
+ if (gatePrediction && gatePrediction.confidence >= 0.55 && !reviewHeuristic.safety) {
808
+ const gateContinues = gatePrediction.decision === "continue";
809
+ reviewRoute = {
810
+ ...reviewHeuristic,
811
+ label: gateContinues ? "abstain" : reviewHeuristic.label,
812
+ confidence: gatePrediction.confidence,
813
+ source: "model",
814
+ reason: gateContinues
815
+ ? "local gate predicts continue"
816
+ : "local gate predicts review",
817
+ review: gateContinues ? "off" as const : reviewHeuristic.review,
818
+ escalate: gateContinues ? false : reviewHeuristic.escalate,
819
+ };
820
+ }
821
+ appendRouteLog(reviewRoute);
822
+ state.router.review = reviewRoute;
823
+ persistReviewState(state, true);
824
+
825
+ if (gatePrediction && gatePrediction.confidence >= 0.55 && gatePrediction.decision === "continue" && !reviewHeuristic.safety) {
826
+ finalDecision = "continue";
827
+ finalReason = "local gate continue";
828
+ markReviewApplied(state, signature, trigger, finalDecision, finalReason, true);
829
+ persistReviewState(state, true);
830
+ finalized = true;
831
+ return;
832
+ }
833
+
834
+ const effectiveReview = mergeRouteReview(config.review, state.router.preflight?.review);
835
+ const finalReview = mergeReviewPolicy(effectiveReview, reviewRoute.review);
836
+ if (finalReview === "off") {
837
+ finalDecision = "continue";
838
+ finalReason = "review disabled";
839
+ markReviewApplied(state, signature, trigger, finalDecision, finalReason, true);
840
+ persistReviewState(state, true);
841
+ finalized = true;
842
+ return;
843
+ }
844
+
845
+ const shouldRun =
846
+ finalReview === "strict"
847
+ ? meta.isAgentEnd || meta.fileChanged || meta.failed || reviewRoute.label !== "abstain" || state.turns % 3 === 0
848
+ : meta.fileChanged || meta.failed;
849
+ if (!shouldRun) {
850
+ finalDecision = "defer";
851
+ finalReason = "no material signal";
852
+ markReviewApplied(state, signature, trigger, finalDecision, finalReason, true);
853
+ persistReviewState(state, true);
854
+ finalized = true;
855
+ return;
856
+ }
857
+
858
+ const b = brief(state);
859
+ if (!b) {
860
+ finalDecision = "defer";
861
+ finalReason = "missing brief context";
862
+ markReviewApplied(state, signature, trigger, finalDecision, finalReason, true);
863
+ persistReviewState(state, true);
864
+ finalized = true;
865
+ return;
866
+ }
867
+
868
+ const rk = hash("rev", trigger, b, delta, String(meta.fileChanged), String(meta.failed), String(meta.isAgentEnd), String(reviewRoute.label), signature);
869
+ const cache = loadCache();
870
+ if (cache[rk]) {
871
+ finalDecision = "defer";
872
+ finalReason = "cached verdict";
873
+ markReviewApplied(state, signature, trigger, finalDecision, finalReason, true);
874
+ persistReviewState(state, true);
875
+ finalized = true;
876
+ return;
877
+ }
878
+
879
+ const msgs = [
880
+ { role: "user", content: [
881
+ `Trigger: ${trigger}`,
882
+ `Task: ${state.lastTask || "(unknown)"}`,
883
+ `Delta: ${delta || "(none)"}`,
884
+ `Files: ${meta.fileChanged} Errors: ${meta.failed}`,
885
+ `Route: ${summarizeRoute(reviewRoute)}`,
886
+ `Brief:\n${b}`,
887
+ ].join("\n"), timestamp: new Date().toISOString() },
888
+ ] as any[];
889
+ const completed = await completeWithModelFallback(ctx, config, REVIEW_SYSTEM, msgs, { maxTokens: 400, reasoning: "low" as ThinkingLevel });
890
+ const raw = completed?.text;
891
+ if (!raw) {
892
+ finalDecision = "defer";
893
+ finalReason = "empty verdict";
894
+ markReviewApplied(state, signature, trigger, finalDecision, finalReason, true);
895
+ persistReviewState(state, true);
896
+ finalized = true;
897
+ return;
898
+ }
899
+
900
+ cache[rk] = raw;
901
+ saveCache(cache);
902
+
903
+ let json: any = null;
904
+ try { json = JSON.parse(raw.replace(/^```(?:json)?\n?/i, "").replace(/\n?```$/i, "")); } catch { /* ignore */ }
905
+ if (!json) {
906
+ finalDecision = "defer";
907
+ finalReason = "unparseable verdict";
908
+ markReviewApplied(state, signature, trigger, finalDecision, finalReason, true);
909
+ persistReviewState(state, true);
910
+ finalized = true;
911
+ return;
912
+ }
913
+
914
+ if (json.verdict === "skip") {
915
+ finalDecision = "defer";
916
+ finalReason = "explicit skip";
917
+ markReviewApplied(state, signature, trigger, finalDecision, finalReason, true);
918
+ persistReviewState(state, true);
919
+ finalized = true;
920
+ return;
921
+ }
922
+
923
+ if (json.verdict === "on_track") {
924
+ finalDecision = "continue";
925
+ finalReason = (json.reason || json.summary || "review result").slice(0, 120);
926
+ markReviewApplied(state, signature, trigger, finalDecision, finalReason, true);
927
+ persistReviewState(state, true);
928
+ finalized = true;
929
+ return;
930
+ }
931
+
932
+ const decision = json.verdict === "course_correct" ? "review"
933
+ : json.verdict === "not_done" ? "review"
934
+ : "defer";
935
+ finalDecision = decision;
936
+ finalReason = (json.reason || json.summary || "review result").slice(0, 120);
937
+
938
+ const display = formatAdvisorDisplay("advisor:llm", decision, finalReason);
939
+ writeText(CURRENT_PATH, `${display}\n`);
940
+ sendAdvisorHint(pi, decision, finalReason, json.summary || "", json.actions || []);
941
+
942
+ if (json.verdict !== "on_track") {
943
+ state.followUp = [json.summary, ...(json.actions?.slice(0, 2) || [])].filter(Boolean).join(" — ");
944
+ }
945
+
946
+ markReviewApplied(state, signature, trigger, finalDecision, finalReason, false);
947
+ persistReviewState(state, true);
948
+ finalized = true;
949
+ } finally {
950
+ if (!finalized) {
951
+ markReviewApplied(state, signature, trigger, finalDecision, finalReason, false);
952
+ persistReviewState(state, true);
953
+ }
954
+ }
955
+ }
956
+
957
+ // ── Extension entry point ──────────────────────────────────────────────────
958
+
959
+ export function registerAdvisor(pi: ExtensionAPI): void {
960
+ const p = pi as any;
961
+ if (p.__piRogueAdvisorRegistered) return;
962
+ p.__piRogueAdvisorRegistered = true;
963
+
964
+ for (const customType of ["advisor:model", "advisor:rules", "advisor:llm"] as const) {
965
+ pi.registerMessageRenderer(customType, renderAdvisorHint);
966
+ }
967
+
968
+ pi.on("session_start", (_event, ctx) => {
969
+ const key = sessionKey(ctx);
970
+ checkinLocks.delete(key);
971
+ const state = loadState();
972
+ recoverReviewControl(state);
973
+ saveState(state);
974
+ setPiRogueStatus(ctx, loadConfig(), state);
975
+ // No timer is owned by advisor itself anymore; check-ins are triggered
976
+ // from active goal/loop/autoresearch flow progression.
977
+ });
978
+
979
+ pi.on("session_shutdown", (_event, ctx) => {
980
+ const key = sessionKey(ctx);
981
+ checkinLocks.delete(key);
982
+ ctx.ui.setStatus("pi-rogue", undefined);
983
+ });
984
+
985
+ // ── Tool ───────────────────────────────────────────────────────────────
986
+ pi.registerTool({
987
+ name: "advisor",
988
+ label: "Advisor",
989
+ description: "Strategic advisor. Call before architecture/refactor/tradeoff decisions. Uses best available model (default gpt-5.5).",
990
+ parameters: Type.Object({
991
+ question: Type.String({ description: "1 concise question" }),
992
+ scope: Type.Optional(Type.String({ description: "architecture|implementation|debug|review|planning" })),
993
+ includeRecentWork: Type.Optional(Type.Boolean({ description: "default: true" })),
994
+ }),
995
+ async execute(_id, params, _signal, onUpdate, ctx) {
996
+ const r = await askAdvisor(pi, ctx, String(params.question || ""), String(params.scope || ""), params.includeRecentWork !== false);
997
+ onUpdate?.({ content: [{ type: "text", text: r.cached ? "(cached)" : r.model ? `Consulting ${r.model}…` : "" }], details: {} });
998
+ return { content: [{ type: "text", text: r.text }], details: { cached: r.cached, error: r.error } };
999
+ },
1000
+ });
1001
+
1002
+ // ── Preflight (heuristics only — no LLM call, <1ms) ──────────────────
1003
+ pi.on("before_agent_start", async (event: any, ctx: any) => {
1004
+ const cfg = loadConfig();
1005
+ const state = loadState();
1006
+ const hasFollowUp = Boolean(state.followUp);
1007
+ if ((isAdvisorAutoRunSuppressed(state, state.turns) && !hasFollowUp) || cfg.mode === "off" || cfg.mode === "manual") {
1008
+ return { systemPrompt: event.systemPrompt };
1009
+ }
1010
+ setPiRogueStatus(ctx, cfg, state);
1011
+ const prompt = typeof event.prompt === "string" && event.prompt.trim() ? squish(event.prompt, 1000) : "";
1012
+ if (prompt) state.lastTask = prompt;
1013
+ const briefText = brief(state);
1014
+ const intent = prompt ? classifyIntent(prompt) : "";
1015
+ const mode = prompt ? classifyMode(prompt) : "";
1016
+ const intentTag = intent ? `Intent: ${intent}` : "";
1017
+ const modeTag = mode ? `Mode: ${mode}` : "";
1018
+ // Enrich preflight text with session context so the binary gate has more signal
1019
+ const enrichedText = [prompt, event.systemPrompt || "", briefText ? `Brief: ${briefText}` : "", intentTag, modeTag].filter(Boolean).join(" ");
1020
+ const routeInput: AdvisorRouteInput = { phase: "preflight", text: enrichedText || prompt || event.systemPrompt || briefText || intentTag || modeTag || "", brief: briefText };
1021
+
1022
+ // Binary gate model — fast local classifier for continue/escalate decisions
1023
+ const gatePrediction = binaryGatePredict(routeInput.text);
1024
+ const heuristic = heuristicRoute(routeInput);
1025
+ let route: AdvisorRouteDecision;
1026
+ if (gatePrediction && gatePrediction.confidence >= 0.55) {
1027
+ const binLabel = gatePrediction.decision === "continue" ? "continue" as const : "escalate_to_advisor" as const;
1028
+ if (heuristic.safety) {
1029
+ route = heuristic;
1030
+ } else {
1031
+ route = {
1032
+ ...heuristic,
1033
+ label: binLabel,
1034
+ confidence: gatePrediction.confidence,
1035
+ reason: gatePrediction.decision === "continue"
1036
+ ? "local gate predicts continue"
1037
+ : "local gate predicts review",
1038
+ source: "model",
1039
+ preflight: binLabel === "continue" ? "off" as const : "full" as const,
1040
+ escalate: binLabel === "escalate_to_advisor",
1041
+ };
1042
+ }
1043
+ } else {
1044
+ route = heuristic;
1045
+ }
1046
+ appendRouteLog(route);
1047
+ state.router.preflight = route;
1048
+ const follow = state.followUp;
1049
+ if (follow) {
1050
+ consumeReviewFollowUp(state);
1051
+ }
1052
+ saveState(state);
1053
+
1054
+ const note = routeNote(route);
1055
+ const control = state.reviewControl;
1056
+ const controlTag = control.status === "needed" || control.status === "running" ? `Review-control: ${control.status}${control.lastDecision ? ` (${control.lastDecision})` : ""}` : "";
1057
+ writeText(CURRENT_PATH, `${note}\n`);
1058
+ return {
1059
+ systemPrompt: [
1060
+ event.systemPrompt,
1061
+ follow ? `Advisor follow-up:\n${follow}` : "",
1062
+ note,
1063
+ controlTag,
1064
+ briefText ? `Brief (cache-aware):\n${briefText}` : "",
1065
+ ].filter(Boolean).join("\n\n"),
1066
+ };
1067
+ });
1068
+
1069
+ // ── Post-review (turn_end) ─────────────────────────────────────────────
1070
+ pi.on("turn_end", async (event: any, ctx: any) => {
1071
+ const cfg = loadConfig();
1072
+ if (cfg.mode === "off") return;
1073
+ const state = loadState();
1074
+ const suppressedThisTurn = isAdvisorAutoRunSuppressedForTurnContext(state, state.turns);
1075
+ const tools = (event.toolResults || []).map((t: any) => String(t?.toolName || t?.name || "tool"));
1076
+ const fileChanged = tools.some((t: string) => /^(edit|write)$/i.test(t));
1077
+ const failed = (event.toolResults || []).some((t: any) => isActualFailure(t));
1078
+ const text = squish(contentText(event.message?.content));
1079
+ if (text && text !== state.notes[state.notes.length - 1]) state.notes.push(text);
1080
+ state.turns++;
1081
+ if (state.advisorPauseUntilTurn && isAdvisorPaused(state, state.turns) === false) {
1082
+ state.advisorPauseUntilTurn = undefined;
1083
+ }
1084
+ saveState(state);
1085
+ setPiRogueStatus(ctx, cfg, state);
1086
+ if (cfg.review !== "off" && !suppressedThisTurn) {
1087
+ await doReview(pi, ctx, `turn-${state.turns}`, text, {
1088
+ fileChanged,
1089
+ failed,
1090
+ isAgentEnd: false,
1091
+ materialSignals: tools,
1092
+ });
1093
+ }
1094
+
1095
+ const post = loadState();
1096
+ if (!isAdvisorAutoRunSuppressed(post, post.turns)) {
1097
+ void maybeAdvisorCheckin(pi, ctx, "turn_end");
1098
+ }
1099
+ });
1100
+
1101
+ // ── Post-review (agent_end) ────────────────────────────────────────────
1102
+ pi.on("agent_end", async (event: any, ctx: any) => {
1103
+ const cfg = loadConfig();
1104
+ if (cfg.mode === "off") return;
1105
+ const state = loadState();
1106
+ const suppressed = isAdvisorAutoRunSuppressedForTurnContext(state, state.turns);
1107
+ if (cfg.review === "off" || suppressed) {
1108
+ if (!suppressed) {
1109
+ void maybeAdvisorCheckin(pi, ctx, "agent_end");
1110
+ }
1111
+ return;
1112
+ }
1113
+
1114
+ const msgs = (event.messages || []).filter((m: any) => m.role === "assistant" || m.role === "toolResult");
1115
+ const last = msgs[msgs.length - 1];
1116
+ const delta = contentText(last?.content) || "(none)";
1117
+ const fileChanged = msgs.some((m: any) => /(?:write|edit)/i.test(JSON.stringify(m)));
1118
+ const failed = msgs.some((m: any) => isActualFailure(m));
1119
+ const signals = msgs.map((m: any) => {
1120
+ const sig = contentText(m?.content);
1121
+ return `${m?.role || "msg"}: ${sig ? squish(sig, 120) : "(empty)"}`;
1122
+ });
1123
+ await doReview(pi, ctx, "agent-end", delta, {
1124
+ fileChanged,
1125
+ failed,
1126
+ isAgentEnd: true,
1127
+ materialSignals: signals,
1128
+ });
1129
+
1130
+ const post = loadState();
1131
+ if (!isAdvisorAutoRunSuppressed(post, post.turns)) {
1132
+ void maybeAdvisorCheckin(pi, ctx, "agent_end");
1133
+ }
1134
+ });
1135
+
1136
+ // ── /pi-rogue cockpit ──────────────────────────────────────────────────
1137
+ pi.registerCommand("pi-rogue", {
1138
+ description: "Show Pi-Rogue cockpit: advisor and orchestration command pointers",
1139
+ getArgumentCompletions: (prefix: string) => piRogueArgumentCompletions(prefix),
1140
+ handler: async (args, ctx) => {
1141
+ const cfg = loadConfig();
1142
+ const state = loadState();
1143
+ const arg = String(args ?? "").trim().toLowerCase();
1144
+ setPiRogueStatus(ctx, cfg, state);
1145
+
1146
+ if (!arg || arg === "status" || arg === "help") {
1147
+ ctx.ui.notify(piRogueCockpitText(cfg, state, readText(CURRENT_PATH).trim(), orchestrationSnapshotText(ctx)), "info");
1148
+ return;
1149
+ }
1150
+
1151
+ if (arg.startsWith("advisor")) {
1152
+ ctx.ui.notify([
1153
+ "Advisor surface:",
1154
+ " /advisor status",
1155
+ " /advisor config",
1156
+ " /advisor <question>",
1157
+ "",
1158
+ "Check-ins are orchestration-managed: set /goal or /loop to activate them.",
1159
+ ].join("\n"), "info");
1160
+ return;
1161
+ }
1162
+
1163
+ if (arg.startsWith("orchestration")) {
1164
+ ctx.ui.notify([
1165
+ "Orchestration surface:",
1166
+ " /goal show|clear|list|set <text>",
1167
+ " /loop status|off|clear|stop|<interval> <instruction>",
1168
+ " /autoresearch status|clear|<instruction>",
1169
+ " /autoresearch-lab status|clear|<instruction>",
1170
+ ].join("\n"), "info");
1171
+ return;
1172
+ }
1173
+
1174
+ if (arg.startsWith("checkins")) {
1175
+ ctx.ui.notify([
1176
+ `Check-ins: ${checkinDescription(cfg)}`,
1177
+ "Managed by orchestration: /goal or /loop activates them; stopping or clearing either disables them.",
1178
+ orchestrationSnapshotText(ctx),
1179
+ ].join("\n"), "info");
1180
+ return;
1181
+ }
1182
+
1183
+ ctx.ui.notify(piRogueCockpitText(cfg, state, readText(CURRENT_PATH).trim(), orchestrationSnapshotText(ctx)), "info");
1184
+ },
1185
+ });
1186
+
1187
+ // ── /advisor command ───────────────────────────────────────────────────
1188
+ pi.registerCommand("advisor", {
1189
+ description: "Senior engineering advisor. Usage: /advisor [on|off|status|config|pause|unpause|question]",
1190
+ getArgumentCompletions: (prefix: string) => advisorArgumentCompletions(prefix),
1191
+ handler: async (args, ctx) => {
1192
+ const a = String(args ?? "").trim().toLowerCase();
1193
+ const [cmd, ...rest] = a.split(/\s+/);
1194
+ const cfg = loadConfig();
1195
+ const state = loadState();
1196
+
1197
+ if (!a || cmd === "status") {
1198
+ const note = readText(CURRENT_PATH).trim();
1199
+ const resolved = await resolveModel(ctx, cfg);
1200
+ const route = state.router.review ?? state.router.preflight;
1201
+ const pause = advisorPauseRemaining(state, state.turns);
1202
+ ctx.ui.notify([
1203
+ note ? `🧭 ${truncate(note, 200)}` : "",
1204
+ route ? `Router: ${summarizeRoute(route)}${route.safety ? " · safety" : ""}` : "",
1205
+ "",
1206
+ `Mode: ${cfg.mode} | Review: ${cfg.review} | Check-ins: ${checkinDescription(cfg)} (orchestration-managed) | Model: ${resolved?.label || cfg.model || "auto"}`,
1207
+ pause > 0 ? `Advisor pause: ${pause} turn${pause === 1 ? "" : "s"} remaining` : "Advisor pause: off",
1208
+ `Turns: ${state.turns} | Calls: ${state.advisorCalls} | Cache hits: ${state.cacheHits}`,
1209
+ state.checkin.lastAt ? `Last check-in: ${new Date(state.checkin.lastAt).toLocaleString()} (${state.checkin.lastReason || "mid-hour"})` : "Last check-in: never",
1210
+ state.checkin.queued ? `Queued check-in: ${state.checkin.queuedReason || "due"}` : "",
1211
+ orchestrationSnapshotText(ctx),
1212
+ "",
1213
+ "Commands: /advisor on|off | /advisor status | /advisor config | /advisor pause <n turns> | <question>",
1214
+ "Tip: SOTA models auto-detected. No config needed.",
1215
+ ].filter(Boolean).join("\n"), "info");
1216
+ return;
1217
+ }
1218
+
1219
+ if (cmd === "on" && cfg.mode === "off") {
1220
+ const next = { ...cfg, mode: "auto" as const };
1221
+ saveConfig(next);
1222
+ setPiRogueStatus(ctx, next, state);
1223
+ ctx.ui.notify("Advisor enabled (auto mode).", "info");
1224
+ return;
1225
+ }
1226
+ if (cmd === "off") {
1227
+ const next = { ...cfg, mode: "off" as const };
1228
+ saveConfig(next);
1229
+ setPiRogueStatus(ctx, next, state);
1230
+ ctx.ui.notify("Advisor disabled.", "info");
1231
+ return;
1232
+ }
1233
+ if (cmd === "mode") {
1234
+ const v = rest[0];
1235
+ if (v === "auto" || v === "manual") {
1236
+ const next: AdvisorConfig = { ...cfg, mode: v };
1237
+ saveConfig(next);
1238
+ setPiRogueStatus(ctx, next, state);
1239
+ ctx.ui.notify(`Mode set to ${v}.`, "info");
1240
+ return;
1241
+ }
1242
+ if (v === "off") {
1243
+ const next = { ...cfg, mode: "off" as const };
1244
+ saveConfig(next);
1245
+ setPiRogueStatus(ctx, next, state);
1246
+ ctx.ui.notify("Advisor disabled.", "info");
1247
+ return;
1248
+ }
1249
+ ctx.ui.notify("Usage: /advisor mode auto|manual|off", "error");
1250
+ return;
1251
+ }
1252
+ if (cmd === "model") {
1253
+ const v = rest.join("/").trim();
1254
+ if (!v || !v.includes("/")) {
1255
+ const resolved = await resolveModel(ctx, cfg);
1256
+ ctx.ui.notify([
1257
+ `Current: ${resolved?.label || "auto"}`,
1258
+ "",
1259
+ "Usage: /advisor model <provider>/<model>",
1260
+ '(e.g. "openai-codex/gpt-5.5" or "anthropic/claude-opus-4-6")',
1261
+ "Run /advisor status for SOTA options.",
1262
+ ].join("\n"), "info");
1263
+ return;
1264
+ }
1265
+ saveConfig({ ...cfg, model: v });
1266
+ ctx.ui.notify(`Model set to ${v}. Remove field to auto-detect.`, "info");
1267
+ return;
1268
+ }
1269
+ if (cmd === "config") {
1270
+ const pause = advisorPauseRemaining(state, state.turns);
1271
+ ctx.ui.notify([
1272
+ "Advisor config (check-ins are orchestration-managed):",
1273
+ ` mode: "${cfg.mode}" — auto (preflight+post+cache) | manual | off`,
1274
+ ` review: "${cfg.review}" — light (changes/errors) | strict (every 3) | off`,
1275
+ ` checkins: "${cfg.checkins}" — set by active /goal or /loop lifecycle`,
1276
+ ` checkinIntervalMinutes: ${cfg.checkinIntervalMinutes}`,
1277
+ pause > 0 ? ` advisorPauseUntilTurn: ${pause} turn${pause === 1 ? "" : "s"} remaining` : " advisorPauseUntilTurn: off",
1278
+ ` model: "${cfg.model || "auto"}" — optional override for higher/advanced advisor model`,
1279
+ "",
1280
+ "Router logs: evals/advisor-router.jsonl",
1281
+ "Run /advisor <question> for immediate advice.",
1282
+ ].join("\n"), "info");
1283
+ return;
1284
+ }
1285
+ if (cmd === "review") {
1286
+ const v = rest[0];
1287
+ 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; }
1288
+ ctx.ui.notify("Usage: /advisor review light|strict|off", "error");
1289
+ return;
1290
+ }
1291
+ if (cmd === "checkins" || cmd === "checkin") {
1292
+ ctx.ui.notify([
1293
+ "Advisor check-ins are orchestration-managed now.",
1294
+ `Current: ${checkinDescription(cfg)}`,
1295
+ "Create or resume /goal or /loop to activate scheduled higher-model check-ins; stop or clear either to disable them.",
1296
+ orchestrationSnapshotText(ctx),
1297
+ ].join("\n"), "info");
1298
+ return;
1299
+ }
1300
+
1301
+ if (cmd === "pause") {
1302
+ const value = rest[0];
1303
+ const turns = Number.parseInt(String(value || ""), 10);
1304
+ if (!Number.isFinite(turns) || turns <= 0) {
1305
+ if (value === "off" || value === "cancel" || value === "clear") {
1306
+ state.advisorPauseUntilTurn = undefined;
1307
+ saveState(state);
1308
+ ctx.ui.notify("Advisor pause cleared.", "info");
1309
+ return;
1310
+ }
1311
+ ctx.ui.notify("Usage: /advisor pause <turns> (or /advisor pause off)", "error");
1312
+ return;
1313
+ }
1314
+ state.advisorPauseUntilTurn = state.turns + turns;
1315
+ saveState(state);
1316
+ ctx.ui.notify(`Advisor pause enabled for next ${turns} turn${turns === 1 ? "" : "s"}.`, "info");
1317
+ return;
1318
+ }
1319
+
1320
+ if (cmd === "unpause") {
1321
+ state.advisorPauseUntilTurn = undefined;
1322
+ saveState(state);
1323
+ ctx.ui.notify("Advisor pause cleared.", "info");
1324
+ return;
1325
+ }
1326
+
1327
+ // Anything else: treat as a question to the advisor
1328
+ const r = await askAdvisor(pi, ctx, a, "slash", true);
1329
+ ctx.ui.notify(r.text, r.error ? "warning" : "info");
1330
+ },
1331
+ });
1332
+ }
1333
+
1334
+ export default function advisorExtension(pi: ExtensionAPI) { registerAdvisor(pi); }