@juicesharp/rpiv-advisor 0.1.2 → 0.6.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 juicesharp
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -5,7 +5,7 @@ implementing the advisor-strategy pattern: the executor model can escalate
5
5
  decisions to a stronger reviewer model (e.g. Opus), receive guidance, and
6
6
  resume.
7
7
 
8
- ![Advisor model selector](https://raw.githubusercontent.com/juicesharp/rpiv-advisor/main/docs/advisor.jpg)
8
+ ![Advisor model selector](https://raw.githubusercontent.com/juicesharp/rpiv-mono/main/packages/rpiv-advisor/docs/advisor.jpg)
9
9
 
10
10
  ## Installation
11
11
 
package/advisor-ui.ts ADDED
@@ -0,0 +1,112 @@
1
+ /**
2
+ * advisor-ui — bordered select-panel builders for the /advisor command.
3
+ *
4
+ * Two public functions (showAdvisorPicker, showEffortPicker) share a private
5
+ * buildSelectPanel helper that owns the bordered-container layout and the
6
+ * SelectList theme wiring.
7
+ */
8
+
9
+ import type { ThinkingLevel } from "@mariozechner/pi-ai";
10
+ import { DynamicBorder, type ExtensionContext, type Theme } from "@mariozechner/pi-coding-agent";
11
+ import { Container, type SelectItem, SelectList, Spacer, Text } from "@mariozechner/pi-tui";
12
+
13
+ const MAX_VISIBLE_ROWS = 10;
14
+ const NAV_HINT = "↑↓ navigate • enter select • esc cancel";
15
+
16
+ const ADVISOR_HEADER_TITLE = "Advisor Tool";
17
+ const ADVISOR_HEADER_PROSE_1 =
18
+ "When the active model needs stronger judgment — a complex decision, an ambiguous " +
19
+ "failure, a problem it's circling without progress — it escalates to the " +
20
+ "advisor model for guidance, then resumes. The advisor runs server-side " +
21
+ "and uses additional tokens.";
22
+ const ADVISOR_HEADER_PROSE_2 =
23
+ "For certain workloads, pairing a faster model as the main model with a " +
24
+ "more capable one as the advisor gives near-top-tier performance with " +
25
+ "reduced token usage.";
26
+
27
+ const EFFORT_HEADER_TITLE = "Reasoning Level";
28
+ const EFFORT_HEADER_PROSE =
29
+ "Choose the reasoning effort level for the advisor. " +
30
+ "Higher levels produce stronger judgment but use more tokens.";
31
+
32
+ function selectListTheme(theme: Theme) {
33
+ return {
34
+ selectedPrefix: (t: string) => theme.bg("selectedBg", theme.fg("accent", t)),
35
+ selectedText: (t: string) => theme.bg("selectedBg", theme.bold(t)),
36
+ description: (t: string) => theme.fg("muted", t),
37
+ scrollInfo: (t: string) => theme.fg("dim", t),
38
+ noMatch: (t: string) => theme.fg("warning", t),
39
+ };
40
+ }
41
+
42
+ function buildSelectPanel(theme: Theme, title: string, proseLines: string[], selectList: SelectList): Container {
43
+ const container = new Container();
44
+ const border = () => new DynamicBorder((s: string) => theme.fg("accent", s));
45
+
46
+ container.addChild(border());
47
+ container.addChild(new Spacer(1));
48
+ container.addChild(new Text(theme.fg("accent", theme.bold(title)), 1, 0));
49
+ container.addChild(new Spacer(1));
50
+ for (const line of proseLines) {
51
+ container.addChild(new Text(line, 1, 0));
52
+ container.addChild(new Spacer(1));
53
+ }
54
+ container.addChild(selectList);
55
+ container.addChild(new Spacer(1));
56
+ container.addChild(new Text(theme.fg("dim", NAV_HINT), 1, 0));
57
+ container.addChild(new Spacer(1));
58
+ container.addChild(border());
59
+ return container;
60
+ }
61
+
62
+ export async function showAdvisorPicker(ctx: ExtensionContext, items: SelectItem[]): Promise<string | null> {
63
+ return ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
64
+ const selectList = new SelectList(items, Math.min(items.length, MAX_VISIBLE_ROWS), selectListTheme(theme));
65
+ selectList.onSelect = (item) => done(item.value);
66
+ selectList.onCancel = () => done(null);
67
+
68
+ const container = buildSelectPanel(
69
+ theme,
70
+ ADVISOR_HEADER_TITLE,
71
+ [ADVISOR_HEADER_PROSE_1, ADVISOR_HEADER_PROSE_2],
72
+ selectList,
73
+ );
74
+
75
+ return {
76
+ render: (w) => container.render(w),
77
+ invalidate: () => container.invalidate(),
78
+ handleInput: (data) => {
79
+ selectList.handleInput(data);
80
+ tui.requestRender();
81
+ },
82
+ };
83
+ });
84
+ }
85
+
86
+ export async function showEffortPicker(
87
+ ctx: ExtensionContext,
88
+ items: SelectItem[],
89
+ currentEffort: ThinkingLevel | undefined,
90
+ defaultEffort: ThinkingLevel,
91
+ ): Promise<string | null> {
92
+ return ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
93
+ const selectList = new SelectList(items, Math.min(items.length, MAX_VISIBLE_ROWS), selectListTheme(theme));
94
+ const preferredIdx = currentEffort ? items.findIndex((item) => item.value === currentEffort) : -1;
95
+ selectList.setSelectedIndex(
96
+ preferredIdx >= 0 ? preferredIdx : items.findIndex((item) => item.value === defaultEffort),
97
+ );
98
+ selectList.onSelect = (item) => done(item.value);
99
+ selectList.onCancel = () => done(null);
100
+
101
+ const container = buildSelectPanel(theme, EFFORT_HEADER_TITLE, [EFFORT_HEADER_PROSE], selectList);
102
+
103
+ return {
104
+ render: (w) => container.render(w),
105
+ invalidate: () => container.invalidate(),
106
+ handleInput: (data) => {
107
+ selectList.handleInput(data);
108
+ tui.requestRender();
109
+ },
110
+ };
111
+ });
112
+ }
package/advisor.ts CHANGED
@@ -14,34 +14,77 @@
14
14
  */
15
15
 
16
16
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
17
- import { dirname, join } from "node:path";
18
17
  import { homedir } from "node:os";
19
- import { completeSimple, supportsXhigh, type Message, type ThinkingLevel } from "@mariozechner/pi-ai";
18
+ import { dirname, join } from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
20
  import type { Api, Model, StopReason, Usage } from "@mariozechner/pi-ai";
21
+ import { completeSimple, type Message, supportsXhigh, type ThinkingLevel } from "@mariozechner/pi-ai";
21
22
  import {
22
- DynamicBorder,
23
- convertToLlm,
24
- serializeConversation,
25
23
  type AgentToolResult,
26
24
  type AgentToolUpdateCallback,
25
+ convertToLlm,
27
26
  type ExtensionAPI,
28
27
  type ExtensionContext,
29
28
  type SessionEntry,
29
+ serializeConversation,
30
30
  } from "@mariozechner/pi-coding-agent";
31
- import {
32
- Container,
33
- SelectList,
34
- Spacer,
35
- Text,
36
- type SelectItem,
37
- } from "@mariozechner/pi-tui";
31
+ import type { SelectItem } from "@mariozechner/pi-tui";
38
32
  import { Type } from "@sinclair/typebox";
33
+ import { showAdvisorPicker, showEffortPicker } from "./advisor-ui.js";
39
34
 
40
35
  // ---------------------------------------------------------------------------
41
- // Constants
36
+ // Constants — grouped by concern, flat named consts (no namespaced objects)
42
37
  // ---------------------------------------------------------------------------
43
38
 
39
+ // Tool identity
44
40
  export const ADVISOR_TOOL_NAME = "advisor";
41
+ const TOOL_LABEL = "Advisor";
42
+
43
+ // Persistence
44
+ const CONFIG_DIR = join(homedir(), ".config", "rpiv-advisor");
45
+ const ADVISOR_CONFIG_PATH = join(CONFIG_DIR, "advisor.json");
46
+ const CONFIG_FILE_MODE = 0o600;
47
+
48
+ // Selector sentinels — double-underscore form is collision-proof against real provider:id keys
49
+ const NO_ADVISOR_VALUE = "__no_advisor__";
50
+ const OFF_VALUE = "__off__";
51
+
52
+ // Effort levels
53
+ const BASE_EFFORT_LEVELS: ThinkingLevel[] = ["minimal", "low", "medium", "high"];
54
+ const XHIGH_EFFORT_LEVEL: ThinkingLevel = "xhigh";
55
+ const DEFAULT_EFFORT: ThinkingLevel = "high";
56
+ const RECOMMENDED_EFFORT_SUFFIX = " (recommended)";
57
+
58
+ // UI — labels used by command flow; panel prose/titles live in advisor-ui.ts
59
+ const CHECKMARK = " ✓";
60
+
61
+ // Messages (static)
62
+ const MSG_ADVISOR_DISABLED = "Advisor disabled";
63
+ const MSG_REQUIRES_INTERACTIVE = "/advisor requires interactive mode";
64
+
65
+ // Errors (static)
66
+ const ERR_NO_MODEL = "No advisor model is configured. The user can enable one with the /advisor command.";
67
+ const ERR_CALL_ABORTED = "Advisor call was cancelled before it completed.";
68
+ const ERR_EMPTY_RESPONSE = "Advisor returned no text content.";
69
+ const ERR_NO_MODEL_SELECTED = "no advisor model selected";
70
+ const ERR_EMPTY_RESPONSE_DETAIL = "empty response";
71
+ const ERR_ABORTED_DETAIL = "aborted";
72
+ const ERR_UNKNOWN = "unknown error";
73
+
74
+ // Errors/messages (parameterized)
75
+ const errMisconfigured = (label: string, err: string) => `Advisor (${label}) is misconfigured: ${err}`;
76
+ const errNoApiKey = (label: string) => `Advisor (${label}) has no API key available.`;
77
+ const errNoApiKeyDetail = (provider: string) => `no API key for ${provider}`;
78
+ const errCallFailed = (err: string | undefined) => `Advisor call failed: ${err ?? ERR_UNKNOWN}`;
79
+ const errCallThrew = (msg: string) => `Advisor call threw: ${msg}`;
80
+ const errSelectionNotFound = (choice: string) => `Advisor selection not found: ${choice}`;
81
+ const errModelUnavailable = (key: string) => `Previously configured advisor model ${key} is no longer available`;
82
+ const msgAdvisorEnabled = (label: string, effort: ThinkingLevel | undefined) =>
83
+ `Advisor: ${label}${effort ? `, ${effort}` : ""}`;
84
+ const msgAdvisorRestored = (label: string, effort: ThinkingLevel | undefined) =>
85
+ `Advisor restored: ${label}${effort ? `, ${effort}` : ""}`;
86
+ const msgConsulting = (label: string, effort: ThinkingLevel | undefined) =>
87
+ `Consulting advisor (${label}${effort ? `, ${effort}` : ""})…`;
45
88
 
46
89
  // ---------------------------------------------------------------------------
47
90
  // Config file persistence (cross-session)
@@ -52,8 +95,6 @@ interface AdvisorConfig {
52
95
  effort?: ThinkingLevel;
53
96
  }
54
97
 
55
- const ADVISOR_CONFIG_PATH = join(homedir(), ".config", "rpiv-advisor", "advisor.json");
56
-
57
98
  function loadAdvisorConfig(): AdvisorConfig {
58
99
  if (!existsSync(ADVISOR_CONFIG_PATH)) return {};
59
100
  try {
@@ -69,12 +110,12 @@ function saveAdvisorConfig(key: string | undefined, effort: ThinkingLevel | unde
69
110
  if (effort) config.effort = effort;
70
111
  try {
71
112
  mkdirSync(dirname(ADVISOR_CONFIG_PATH), { recursive: true });
72
- writeFileSync(ADVISOR_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf-8");
113
+ writeFileSync(ADVISOR_CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
73
114
  } catch {
74
115
  // write may fail on disk-full or permission errors — best effort only
75
116
  }
76
117
  try {
77
- chmodSync(ADVISOR_CONFIG_PATH, 0o600);
118
+ chmodSync(ADVISOR_CONFIG_PATH, CONFIG_FILE_MODE);
78
119
  } catch {
79
120
  // chmod may fail on some filesystems — best effort only
80
121
  }
@@ -86,26 +127,14 @@ function parseModelKey(key: string): { provider: string; modelId: string } | und
86
127
  return { provider: key.slice(0, idx), modelId: key.slice(idx + 1) };
87
128
  }
88
129
 
89
- export const ADVISOR_SYSTEM_PROMPT = `You are an advisor model in an advisor-strategy pattern. An executor model is running a task end-to-end — calling tools, reading results, iterating toward a solution. When the executor hits a decision it cannot reasonably solve alone, it consults you for guidance.
90
-
91
- You read the shared conversation context and return ONE of:
92
- - a plan (concrete next steps the executor should take),
93
- - a correction (the executor is going down a wrong path — redirect it),
94
- - a stop signal (the executor should halt and escalate to the user).
95
-
96
- You NEVER call tools. You NEVER produce user-facing output. Be concise, directive, and grounded in the shared context. Name files, functions, and line numbers where possible. No preamble, no apologies, no meta-commentary about being an advisor — just the guidance the executor needs.`;
97
-
98
130
  // ---------------------------------------------------------------------------
99
- // Types
131
+ // System prompt — loaded once at module init from prompts/advisor-system.txt
100
132
  // ---------------------------------------------------------------------------
101
133
 
102
- export interface AdvisorDetails {
103
- advisorModel?: string;
104
- effort?: ThinkingLevel;
105
- usage?: Usage;
106
- stopReason?: StopReason;
107
- errorMessage?: string;
108
- }
134
+ export const ADVISOR_SYSTEM_PROMPT = readFileSync(
135
+ fileURLToPath(new URL("./prompts/advisor-system.txt", import.meta.url)),
136
+ "utf-8",
137
+ ).trimEnd();
109
138
 
110
139
  // ---------------------------------------------------------------------------
111
140
  // Module state — in-memory, resets each session
@@ -144,10 +173,7 @@ export function restoreAdvisorState(ctx: ExtensionContext, pi: ExtensionAPI): vo
144
173
  const model = ctx.modelRegistry.find(parsed.provider, parsed.modelId);
145
174
  if (!model) {
146
175
  if (ctx.hasUI) {
147
- ctx.ui.notify(
148
- `Previously configured advisor model ${config.modelKey} is no longer available`,
149
- "warning",
150
- );
176
+ ctx.ui.notify(errModelUnavailable(config.modelKey), "warning");
151
177
  }
152
178
  return;
153
179
  }
@@ -163,10 +189,7 @@ export function restoreAdvisorState(ctx: ExtensionContext, pi: ExtensionAPI): vo
163
189
  }
164
190
 
165
191
  if (ctx.hasUI) {
166
- ctx.ui.notify(
167
- `Advisor restored: ${model.provider}:${model.id}${config.effort ? `, ${config.effort}` : ""}`,
168
- "info",
169
- );
192
+ ctx.ui.notify(msgAdvisorRestored(`${model.provider}:${model.id}`, config.effort), "info");
170
193
  }
171
194
  }
172
195
 
@@ -174,6 +197,14 @@ export function restoreAdvisorState(ctx: ExtensionContext, pi: ExtensionAPI): vo
174
197
  // Core execute logic — curate context, call advisor, return structured result
175
198
  // ---------------------------------------------------------------------------
176
199
 
200
+ export interface AdvisorDetails {
201
+ advisorModel?: string;
202
+ effort?: ThinkingLevel;
203
+ usage?: Usage;
204
+ stopReason?: StopReason;
205
+ errorMessage?: string;
206
+ }
207
+
177
208
  function buildErrorResult(
178
209
  advisorLabel: string | undefined,
179
210
  userText: string,
@@ -182,9 +213,7 @@ function buildErrorResult(
182
213
  const effort = getAdvisorEffort();
183
214
  return {
184
215
  content: [{ type: "text", text: userText }],
185
- details: advisorLabel
186
- ? { advisorModel: advisorLabel, effort, errorMessage }
187
- : { effort, errorMessage },
216
+ details: advisorLabel ? { advisorModel: advisorLabel, effort, errorMessage } : { effort, errorMessage },
188
217
  };
189
218
  }
190
219
 
@@ -195,30 +224,17 @@ async function executeAdvisor(
195
224
  ): Promise<AgentToolResult<AdvisorDetails>> {
196
225
  const advisor = getAdvisorModel();
197
226
  if (!advisor) {
198
- return buildErrorResult(
199
- undefined,
200
- "No advisor model is configured. The user can enable one with the /advisor command.",
201
- "no advisor model selected",
202
- );
227
+ return buildErrorResult(undefined, ERR_NO_MODEL, ERR_NO_MODEL_SELECTED);
203
228
  }
204
229
  const advisorLabel = `${advisor.provider}:${advisor.id}`;
205
230
  const effort = getAdvisorEffort();
206
231
 
207
232
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(advisor);
208
233
  if (!auth.ok) {
209
- return buildErrorResult(
210
- advisorLabel,
211
- `Advisor (${advisorLabel}) is misconfigured: ${auth.error}`,
212
- auth.error,
213
- );
234
+ return buildErrorResult(advisorLabel, errMisconfigured(advisorLabel, auth.error), auth.error);
214
235
  }
215
236
  if (!auth.apiKey) {
216
- const msg = `no API key for ${advisor.provider}`;
217
- return buildErrorResult(
218
- advisorLabel,
219
- `Advisor (${advisorLabel}) has no API key available.`,
220
- msg,
221
- );
237
+ return buildErrorResult(advisorLabel, errNoApiKey(advisorLabel), errNoApiKeyDetail(advisor.provider));
222
238
  }
223
239
 
224
240
  const branch = ctx.sessionManager.getBranch();
@@ -239,7 +255,7 @@ async function executeAdvisor(
239
255
  };
240
256
 
241
257
  onUpdate?.({
242
- content: [{ type: "text", text: `Consulting advisor (${advisorLabel}${effort ? `, ${effort}` : ""})…` }],
258
+ content: [{ type: "text", text: msgConsulting(advisorLabel, effort) }],
243
259
  details: { advisorModel: advisorLabel, effort },
244
260
  });
245
261
 
@@ -252,27 +268,20 @@ async function executeAdvisor(
252
268
 
253
269
  if (response.stopReason === "aborted") {
254
270
  return {
255
- content: [
256
- { type: "text", text: "Advisor call was cancelled before it completed." },
257
- ],
271
+ content: [{ type: "text", text: ERR_CALL_ABORTED }],
258
272
  details: {
259
273
  advisorModel: advisorLabel,
260
274
  effort,
261
275
  usage: response.usage,
262
276
  stopReason: response.stopReason,
263
- errorMessage: response.errorMessage ?? "aborted",
277
+ errorMessage: response.errorMessage ?? ERR_ABORTED_DETAIL,
264
278
  },
265
279
  };
266
280
  }
267
281
 
268
282
  if (response.stopReason === "error") {
269
283
  return {
270
- content: [
271
- {
272
- type: "text",
273
- text: `Advisor call failed: ${response.errorMessage ?? "unknown error"}`,
274
- },
275
- ],
284
+ content: [{ type: "text", text: errCallFailed(response.errorMessage) }],
276
285
  details: {
277
286
  advisorModel: advisorLabel,
278
287
  effort,
@@ -291,13 +300,13 @@ async function executeAdvisor(
291
300
 
292
301
  if (!advisorText) {
293
302
  return {
294
- content: [{ type: "text", text: "Advisor returned no text content." }],
303
+ content: [{ type: "text", text: ERR_EMPTY_RESPONSE }],
295
304
  details: {
296
305
  advisorModel: advisorLabel,
297
306
  effort,
298
307
  usage: response.usage,
299
308
  stopReason: response.stopReason,
300
- errorMessage: "empty response",
309
+ errorMessage: ERR_EMPTY_RESPONSE_DETAIL,
301
310
  },
302
311
  };
303
312
  }
@@ -313,11 +322,7 @@ async function executeAdvisor(
313
322
  };
314
323
  } catch (err) {
315
324
  const message = err instanceof Error ? err.message : String(err);
316
- return buildErrorResult(
317
- advisorLabel,
318
- `Advisor call threw: ${message}`,
319
- message,
320
- );
325
+ return buildErrorResult(advisorLabel, errCallThrew(message), message);
321
326
  }
322
327
  }
323
328
 
@@ -350,7 +355,7 @@ const ADVISOR_PROMPT_GUIDELINES: string[] = [
350
355
  export function registerAdvisorTool(pi: ExtensionAPI): void {
351
356
  pi.registerTool({
352
357
  name: ADVISOR_TOOL_NAME,
353
- label: "Advisor",
358
+ label: TOOL_LABEL,
354
359
  description: ADVISOR_DESCRIPTION,
355
360
  promptSnippet: ADVISOR_PROMPT_SNIPPET,
356
361
  promptGuidelines: ADVISOR_PROMPT_GUIDELINES,
@@ -381,27 +386,6 @@ export function registerAdvisorBeforeAgentStart(pi: ExtensionAPI): void {
381
386
  // /advisor slash command — opens selector panel for picking the advisor model
382
387
  // ---------------------------------------------------------------------------
383
388
 
384
- const ADVISOR_HEADER_TITLE = "Advisor Tool";
385
-
386
- const ADVISOR_HEADER_PROSE_1 =
387
- "When the active model needs stronger judgment — a complex decision, an ambiguous " +
388
- "failure, a problem it's circling without progress — it escalates to the " +
389
- "advisor model for guidance, then resumes. The advisor runs server-side " +
390
- "and uses additional tokens.";
391
-
392
- const ADVISOR_HEADER_PROSE_2 =
393
- "For certain workloads, pairing a faster model as the main model with a " +
394
- "more capable one as the advisor gives near-top-tier performance with " +
395
- "reduced token usage.";
396
-
397
- const NO_ADVISOR_VALUE = "__no_advisor__";
398
-
399
- const EFFORT_HEADER_TITLE = "Reasoning Level";
400
-
401
- const EFFORT_HEADER_PROSE =
402
- "Choose the reasoning effort level for the advisor. " +
403
- "Higher levels produce stronger judgment but use more tokens.";
404
-
405
389
  function modelKey(m: { provider: string; id: string }): string {
406
390
  return `${m.provider}:${m.id}`;
407
391
  }
@@ -411,7 +395,7 @@ export function registerAdvisorCommand(pi: ExtensionAPI): void {
411
395
  description: "Configure the advisor model for the advisor-strategy pattern",
412
396
  handler: async (_args, ctx) => {
413
397
  if (!ctx.hasUI) {
414
- ctx.ui.notify("/advisor requires interactive mode", "error");
398
+ ctx.ui.notify(MSG_REQUIRES_INTERACTIVE, "error");
415
399
  return;
416
400
  }
417
401
 
@@ -421,74 +405,15 @@ export function registerAdvisorCommand(pi: ExtensionAPI): void {
421
405
 
422
406
  const items: SelectItem[] = availableModels.map((m) => {
423
407
  const key = modelKey(m);
424
- const check = key === currentKey ? " ✓" : "";
408
+ const check = key === currentKey ? CHECKMARK : "";
425
409
  return { value: key, label: `${m.name} (${m.provider})${check}` };
426
410
  });
427
411
  items.push({
428
412
  value: NO_ADVISOR_VALUE,
429
- label: currentKey === undefined ? "No advisor ✓" : "No advisor",
413
+ label: currentKey === undefined ? `No advisor${CHECKMARK}` : "No advisor",
430
414
  });
431
415
 
432
- const choice = await ctx.ui.custom<string | null>(
433
- (tui, theme, _kb, done) => {
434
- const container = new Container();
435
-
436
- container.addChild(
437
- new DynamicBorder((s: string) => theme.fg("accent", s)),
438
- );
439
- container.addChild(new Spacer(1));
440
- container.addChild(
441
- new Text(
442
- theme.fg("accent", theme.bold(ADVISOR_HEADER_TITLE)),
443
- 1,
444
- 0,
445
- ),
446
- );
447
- container.addChild(new Spacer(1));
448
- container.addChild(new Text(ADVISOR_HEADER_PROSE_1, 1, 0));
449
- container.addChild(new Spacer(1));
450
- container.addChild(new Text(ADVISOR_HEADER_PROSE_2, 1, 0));
451
- container.addChild(new Spacer(1));
452
-
453
- const selectList = new SelectList(
454
- items,
455
- Math.min(items.length, 10),
456
- {
457
- selectedPrefix: (t) => theme.bg("selectedBg", theme.fg("accent", t)),
458
- selectedText: (t) => theme.bg("selectedBg", theme.bold(t)),
459
- description: (t) => theme.fg("muted", t),
460
- scrollInfo: (t) => theme.fg("dim", t),
461
- noMatch: (t) => theme.fg("warning", t),
462
- },
463
- );
464
- selectList.onSelect = (item) => done(item.value);
465
- selectList.onCancel = () => done(null);
466
- container.addChild(selectList);
467
-
468
- container.addChild(new Spacer(1));
469
- container.addChild(
470
- new Text(
471
- theme.fg("dim", "↑↓ navigate • enter select • esc cancel"),
472
- 1,
473
- 0,
474
- ),
475
- );
476
- container.addChild(new Spacer(1));
477
- container.addChild(
478
- new DynamicBorder((s: string) => theme.fg("accent", s)),
479
- );
480
-
481
- return {
482
- render: (w) => container.render(w),
483
- invalidate: () => container.invalidate(),
484
- handleInput: (data) => {
485
- selectList.handleInput(data);
486
- tui.requestRender();
487
- },
488
- };
489
- },
490
- );
491
-
416
+ const choice = await showAdvisorPicker(ctx, items);
492
417
  if (!choice) {
493
418
  return;
494
419
  }
@@ -501,104 +426,36 @@ export function registerAdvisorCommand(pi: ExtensionAPI): void {
501
426
  setAdvisorEffort(undefined);
502
427
  saveAdvisorConfig(undefined, undefined);
503
428
  if (activeHas) {
504
- pi.setActiveTools(
505
- activeTools.filter((n) => n !== ADVISOR_TOOL_NAME),
506
- );
429
+ pi.setActiveTools(activeTools.filter((n) => n !== ADVISOR_TOOL_NAME));
507
430
  }
508
- ctx.ui.notify("Advisor disabled", "info");
431
+ ctx.ui.notify(MSG_ADVISOR_DISABLED, "info");
509
432
  return;
510
433
  }
511
434
 
512
435
  const picked = availableModels.find((m) => modelKey(m) === choice);
513
436
  if (!picked) {
514
- ctx.ui.notify(`Advisor selection not found: ${choice}`, "error");
437
+ ctx.ui.notify(errSelectionNotFound(choice), "error");
515
438
  return;
516
439
  }
517
440
 
518
441
  // Effort picker — only for reasoning-capable models
519
442
  let effortChoice: ThinkingLevel | undefined;
520
443
  if (picked.reasoning) {
521
- const OFF_VALUE = "__off__";
522
- const baseLevels: ThinkingLevel[] = ["minimal", "low", "medium", "high"];
523
- const levels = supportsXhigh(picked)
524
- ? [...baseLevels, "xhigh" as ThinkingLevel]
525
- : baseLevels;
444
+ const levels = supportsXhigh(picked) ? [...BASE_EFFORT_LEVELS, XHIGH_EFFORT_LEVEL] : BASE_EFFORT_LEVELS;
526
445
 
527
446
  const effortItems: SelectItem[] = [
528
447
  { value: OFF_VALUE, label: "off" },
529
448
  ...levels.map((level) => ({
530
449
  value: level,
531
- label: level === "high" ? `${level} (recommended)` : level,
450
+ label: level === DEFAULT_EFFORT ? `${level}${RECOMMENDED_EFFORT_SUFFIX}` : level,
532
451
  })),
533
452
  ];
534
453
 
535
- const effortResult = await ctx.ui.custom<string | null>(
536
- (tui, theme, _kb, done) => {
537
- const container = new Container();
538
-
539
- container.addChild(
540
- new DynamicBorder((s: string) => theme.fg("accent", s)),
541
- );
542
- container.addChild(new Spacer(1));
543
- container.addChild(
544
- new Text(
545
- theme.fg("accent", theme.bold(EFFORT_HEADER_TITLE)),
546
- 1,
547
- 0,
548
- ),
549
- );
550
- container.addChild(new Spacer(1));
551
- container.addChild(new Text(EFFORT_HEADER_PROSE, 1, 0));
552
- container.addChild(new Spacer(1));
553
-
554
- const selectList = new SelectList(
555
- effortItems,
556
- Math.min(effortItems.length, 10),
557
- {
558
- selectedPrefix: (t) => theme.bg("selectedBg", theme.fg("accent", t)),
559
- selectedText: (t) => theme.bg("selectedBg", theme.bold(t)),
560
- description: (t) => theme.fg("muted", t),
561
- scrollInfo: (t) => theme.fg("dim", t),
562
- noMatch: (t) => theme.fg("warning", t),
563
- },
564
- );
565
- const currentEffort = getAdvisorEffort();
566
- const defaultIdx = currentEffort
567
- ? effortItems.findIndex((item) => item.value === currentEffort)
568
- : -1;
569
- selectList.setSelectedIndex(defaultIdx >= 0 ? defaultIdx : effortItems.findIndex((item) => item.value === "high"));
570
- selectList.onSelect = (item) => done(item.value);
571
- selectList.onCancel = () => done(null);
572
- container.addChild(selectList);
573
-
574
- container.addChild(new Spacer(1));
575
- container.addChild(
576
- new Text(
577
- theme.fg("dim", "↑↓ navigate • enter select • esc cancel"),
578
- 1,
579
- 0,
580
- ),
581
- );
582
- container.addChild(new Spacer(1));
583
- container.addChild(
584
- new DynamicBorder((s: string) => theme.fg("accent", s)),
585
- );
586
-
587
- return {
588
- render: (w) => container.render(w),
589
- invalidate: () => container.invalidate(),
590
- handleInput: (data) => {
591
- selectList.handleInput(data);
592
- tui.requestRender();
593
- },
594
- };
595
- },
596
- );
597
-
454
+ const effortResult = await showEffortPicker(ctx, effortItems, getAdvisorEffort(), DEFAULT_EFFORT);
598
455
  if (!effortResult) {
599
456
  return;
600
457
  }
601
- effortChoice = effortResult === OFF_VALUE ? undefined : effortResult as ThinkingLevel;
458
+ effortChoice = effortResult === OFF_VALUE ? undefined : (effortResult as ThinkingLevel);
602
459
  }
603
460
 
604
461
  setAdvisorEffort(effortChoice);
@@ -607,10 +464,7 @@ export function registerAdvisorCommand(pi: ExtensionAPI): void {
607
464
  if (!activeHas) {
608
465
  pi.setActiveTools([...activeTools, ADVISOR_TOOL_NAME]);
609
466
  }
610
- ctx.ui.notify(
611
- `Advisor: ${picked.provider}:${picked.id}${effortChoice ? `, ${effortChoice}` : ""}`,
612
- "info",
613
- );
467
+ ctx.ui.notify(msgAdvisorEnabled(modelKey(picked), effortChoice), "info");
614
468
  },
615
469
  });
616
470
  }
package/index.ts CHANGED
@@ -11,18 +11,18 @@
11
11
 
12
12
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
13
13
  import {
14
- registerAdvisorTool,
15
- registerAdvisorCommand,
16
- registerAdvisorBeforeAgentStart,
17
- restoreAdvisorState,
14
+ registerAdvisorBeforeAgentStart,
15
+ registerAdvisorCommand,
16
+ registerAdvisorTool,
17
+ restoreAdvisorState,
18
18
  } from "./advisor.js";
19
19
 
20
20
  export default function (pi: ExtensionAPI) {
21
- registerAdvisorTool(pi);
22
- registerAdvisorCommand(pi);
23
- registerAdvisorBeforeAgentStart(pi);
21
+ registerAdvisorTool(pi);
22
+ registerAdvisorCommand(pi);
23
+ registerAdvisorBeforeAgentStart(pi);
24
24
 
25
- pi.on("session_start", async (_event, ctx) => {
26
- restoreAdvisorState(ctx, pi);
27
- });
25
+ pi.on("session_start", async (_event, ctx) => {
26
+ restoreAdvisorState(ctx, pi);
27
+ });
28
28
  }
package/package.json CHANGED
@@ -1,24 +1,40 @@
1
1
  {
2
2
  "name": "@juicesharp/rpiv-advisor",
3
- "version": "0.1.2",
3
+ "version": "0.6.1",
4
4
  "description": "Pi extension: advisor-strategy pattern — escalate to a stronger reviewer model",
5
- "keywords": ["pi-package", "pi-extension", "rpiv", "advisor"],
5
+ "keywords": [
6
+ "pi-package",
7
+ "pi-extension",
8
+ "rpiv",
9
+ "advisor"
10
+ ],
6
11
  "type": "module",
7
12
  "license": "MIT",
8
13
  "author": "juicesharp",
9
14
  "repository": {
10
15
  "type": "git",
11
- "url": "git+https://github.com/juicesharp/rpiv-advisor.git"
16
+ "url": "git+https://github.com/juicesharp/rpiv-mono.git",
17
+ "directory": "packages/rpiv-advisor"
12
18
  },
13
- "homepage": "https://github.com/juicesharp/rpiv-advisor#readme",
19
+ "homepage": "https://github.com/juicesharp/rpiv-mono/tree/main/packages/rpiv-advisor#readme",
14
20
  "bugs": {
15
- "url": "https://github.com/juicesharp/rpiv-advisor/issues"
21
+ "url": "https://github.com/juicesharp/rpiv-mono/issues"
16
22
  },
17
23
  "publishConfig": {
18
24
  "access": "public"
19
25
  },
26
+ "files": [
27
+ "index.ts",
28
+ "advisor.ts",
29
+ "advisor-ui.ts",
30
+ "prompts/",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
20
34
  "pi": {
21
- "extensions": ["./index.ts"]
35
+ "extensions": [
36
+ "./index.ts"
37
+ ]
22
38
  },
23
39
  "peerDependencies": {
24
40
  "@mariozechner/pi-ai": "*",
@@ -0,0 +1,8 @@
1
+ You are an advisor model in an advisor-strategy pattern. An executor model is running a task end-to-end — calling tools, reading results, iterating toward a solution. When the executor hits a decision it cannot reasonably solve alone, it consults you for guidance.
2
+
3
+ You read the shared conversation context and return ONE of:
4
+ - a plan (concrete next steps the executor should take),
5
+ - a correction (the executor is going down a wrong path — redirect it),
6
+ - a stop signal (the executor should halt and escalate to the user).
7
+
8
+ You NEVER call tools. You NEVER produce user-facing output. Be concise, directive, and grounded in the shared context. Name files, functions, and line numbers where possible. No preamble, no apologies, no meta-commentary about being an advisor — just the guidance the executor needs.
package/docs/advisor.jpg DELETED
Binary file