@juicesharp/rpiv-advisor 0.1.0

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 (4) hide show
  1. package/README.md +38 -0
  2. package/advisor.ts +616 -0
  3. package/index.ts +28 -0
  4. package/package.json +29 -0
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # rpiv-advisor
2
+
3
+ Pi extension that registers the `advisor` tool and `/advisor` slash command,
4
+ implementing the advisor-strategy pattern: the executor model can escalate
5
+ decisions to a stronger reviewer model (e.g. Opus), receive guidance, and
6
+ resume.
7
+
8
+ ## Installation
9
+
10
+ pi install npm:@juicesharp/rpiv-advisor
11
+
12
+ Then restart your Pi session.
13
+
14
+ ## Usage
15
+
16
+ Configure an advisor model with `/advisor` — the command opens a selector for
17
+ any model registered with Pi's model registry, plus a reasoning-effort picker
18
+ for reasoning-capable models. Selection persists across sessions at
19
+ `~/.config/rpiv-advisor/advisor.json` (chmod 0600).
20
+
21
+ The `advisor` tool is registered at load but excluded from active tools by
22
+ default; selecting a model via `/advisor` enables it. Choose "No advisor" to
23
+ disable.
24
+
25
+ `advisor` takes zero parameters — calling it forwards the full serialized
26
+ conversation branch to the advisor model, which returns guidance (plan,
27
+ correction, or stop signal) that the executor consumes.
28
+
29
+ ## Migration from rpiv-pi ≤ 0.3.0
30
+
31
+ If you had an advisor configured while rpiv-pi bundled this tool, your previous
32
+ selection lived at `~/.config/rpiv-pi/advisor.json`. The new plugin reads
33
+ `~/.config/rpiv-advisor/advisor.json` only — run `/advisor` once to re-select
34
+ your model.
35
+
36
+ ## License
37
+
38
+ MIT
package/advisor.ts ADDED
@@ -0,0 +1,616 @@
1
+ /**
2
+ * advisor tool + /advisor command — Advisor-strategy pattern.
3
+ *
4
+ * Lets the executor model consult a stronger advisor model (e.g. Opus) via an
5
+ * in-process completeSimple() call with the full serialized conversation branch
6
+ * as context. Advisor has no tools, never emits user-facing output, and returns
7
+ * guidance (plan, correction, or stop signal) that the executor resumes with.
8
+ *
9
+ * Default state is OFF — the tool is registered at load but a before_agent_start
10
+ * handler strips it from the active tool list each turn while no advisor model
11
+ * is selected. /advisor opens a selector panel (ctx.ui.custom) to pick an
12
+ * advisor model from ctx.modelRegistry.getAvailable() and toggles the tool in
13
+ * via pi.setActiveTools(). Selection is in-memory and resets each session.
14
+ */
15
+
16
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
17
+ import { dirname, join } from "node:path";
18
+ import { homedir } from "node:os";
19
+ import { completeSimple, supportsXhigh, type Message, type ThinkingLevel } from "@mariozechner/pi-ai";
20
+ import type { Api, Model, StopReason, Usage } from "@mariozechner/pi-ai";
21
+ import {
22
+ DynamicBorder,
23
+ convertToLlm,
24
+ serializeConversation,
25
+ type AgentToolResult,
26
+ type AgentToolUpdateCallback,
27
+ type ExtensionAPI,
28
+ type ExtensionContext,
29
+ type SessionEntry,
30
+ } from "@mariozechner/pi-coding-agent";
31
+ import {
32
+ Container,
33
+ SelectList,
34
+ Spacer,
35
+ Text,
36
+ type SelectItem,
37
+ } from "@mariozechner/pi-tui";
38
+ import { Type } from "@sinclair/typebox";
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Constants
42
+ // ---------------------------------------------------------------------------
43
+
44
+ export const ADVISOR_TOOL_NAME = "advisor";
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Config file persistence (cross-session)
48
+ // ---------------------------------------------------------------------------
49
+
50
+ interface AdvisorConfig {
51
+ modelKey?: string;
52
+ effort?: ThinkingLevel;
53
+ }
54
+
55
+ const ADVISOR_CONFIG_PATH = join(homedir(), ".config", "rpiv-advisor", "advisor.json");
56
+
57
+ function loadAdvisorConfig(): AdvisorConfig {
58
+ if (!existsSync(ADVISOR_CONFIG_PATH)) return {};
59
+ try {
60
+ return JSON.parse(readFileSync(ADVISOR_CONFIG_PATH, "utf-8")) as AdvisorConfig;
61
+ } catch {
62
+ return {};
63
+ }
64
+ }
65
+
66
+ function saveAdvisorConfig(key: string | undefined, effort: ThinkingLevel | undefined): void {
67
+ const config: AdvisorConfig = {};
68
+ if (key) config.modelKey = key;
69
+ if (effort) config.effort = effort;
70
+ try {
71
+ mkdirSync(dirname(ADVISOR_CONFIG_PATH), { recursive: true });
72
+ writeFileSync(ADVISOR_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf-8");
73
+ } catch {
74
+ // write may fail on disk-full or permission errors — best effort only
75
+ }
76
+ try {
77
+ chmodSync(ADVISOR_CONFIG_PATH, 0o600);
78
+ } catch {
79
+ // chmod may fail on some filesystems — best effort only
80
+ }
81
+ }
82
+
83
+ function parseModelKey(key: string): { provider: string; modelId: string } | undefined {
84
+ const idx = key.indexOf(":");
85
+ if (idx < 1) return undefined;
86
+ return { provider: key.slice(0, idx), modelId: key.slice(idx + 1) };
87
+ }
88
+
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
+ // ---------------------------------------------------------------------------
99
+ // Types
100
+ // ---------------------------------------------------------------------------
101
+
102
+ export interface AdvisorDetails {
103
+ advisorModel?: string;
104
+ effort?: ThinkingLevel;
105
+ usage?: Usage;
106
+ stopReason?: StopReason;
107
+ errorMessage?: string;
108
+ }
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // Module state — in-memory, resets each session
112
+ // ---------------------------------------------------------------------------
113
+
114
+ let selectedAdvisor: Model<Api> | undefined;
115
+ let selectedAdvisorEffort: ThinkingLevel | undefined;
116
+
117
+ export function getAdvisorModel(): Model<Api> | undefined {
118
+ return selectedAdvisor;
119
+ }
120
+
121
+ export function setAdvisorModel(model: Model<Api> | undefined): void {
122
+ selectedAdvisor = model;
123
+ }
124
+
125
+ export function getAdvisorEffort(): ThinkingLevel | undefined {
126
+ return selectedAdvisorEffort;
127
+ }
128
+
129
+ export function setAdvisorEffort(effort: ThinkingLevel | undefined): void {
130
+ selectedAdvisorEffort = effort;
131
+ }
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // Session restoration — called from index.ts session_start handler
135
+ // ---------------------------------------------------------------------------
136
+
137
+ export function restoreAdvisorState(ctx: ExtensionContext, pi: ExtensionAPI): void {
138
+ const config = loadAdvisorConfig();
139
+ if (!config.modelKey) return;
140
+
141
+ const parsed = parseModelKey(config.modelKey);
142
+ if (!parsed) return;
143
+
144
+ const model = ctx.modelRegistry.find(parsed.provider, parsed.modelId);
145
+ if (!model) {
146
+ if (ctx.hasUI) {
147
+ ctx.ui.notify(
148
+ `Previously configured advisor model ${config.modelKey} is no longer available`,
149
+ "warning",
150
+ );
151
+ }
152
+ return;
153
+ }
154
+
155
+ setAdvisorModel(model);
156
+ if (config.effort) {
157
+ setAdvisorEffort(config.effort);
158
+ }
159
+
160
+ const active = pi.getActiveTools();
161
+ if (!active.includes(ADVISOR_TOOL_NAME)) {
162
+ pi.setActiveTools([...active, ADVISOR_TOOL_NAME]);
163
+ }
164
+
165
+ if (ctx.hasUI) {
166
+ ctx.ui.notify(
167
+ `Advisor restored: ${model.provider}:${model.id}${config.effort ? `, ${config.effort}` : ""}`,
168
+ "info",
169
+ );
170
+ }
171
+ }
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // Core execute logic — curate context, call advisor, return structured result
175
+ // ---------------------------------------------------------------------------
176
+
177
+ function buildErrorResult(
178
+ advisorLabel: string | undefined,
179
+ userText: string,
180
+ errorMessage: string,
181
+ ): AgentToolResult<AdvisorDetails> {
182
+ const effort = getAdvisorEffort();
183
+ return {
184
+ content: [{ type: "text", text: userText }],
185
+ details: advisorLabel
186
+ ? { advisorModel: advisorLabel, effort, errorMessage }
187
+ : { effort, errorMessage },
188
+ };
189
+ }
190
+
191
+ async function executeAdvisor(
192
+ ctx: ExtensionContext,
193
+ signal: AbortSignal | undefined,
194
+ onUpdate: AgentToolUpdateCallback<AdvisorDetails> | undefined,
195
+ ): Promise<AgentToolResult<AdvisorDetails>> {
196
+ const advisor = getAdvisorModel();
197
+ 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
+ );
203
+ }
204
+ const advisorLabel = `${advisor.provider}:${advisor.id}`;
205
+ const effort = getAdvisorEffort();
206
+
207
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(advisor);
208
+ if (!auth.ok) {
209
+ return buildErrorResult(
210
+ advisorLabel,
211
+ `Advisor (${advisorLabel}) is misconfigured: ${auth.error}`,
212
+ auth.error,
213
+ );
214
+ }
215
+ 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
+ );
222
+ }
223
+
224
+ const branch = ctx.sessionManager.getBranch();
225
+ const agentMessages = branch
226
+ .filter((e): e is SessionEntry & { type: "message" } => e.type === "message")
227
+ .map((e) => e.message);
228
+ const conversationText = serializeConversation(convertToLlm(agentMessages));
229
+
230
+ const userMessage: Message = {
231
+ role: "user",
232
+ content: [
233
+ {
234
+ type: "text",
235
+ text: `## Conversation So Far\n\n${conversationText}`,
236
+ },
237
+ ],
238
+ timestamp: Date.now(),
239
+ };
240
+
241
+ onUpdate?.({
242
+ content: [{ type: "text", text: `Consulting advisor (${advisorLabel}${effort ? `, ${effort}` : ""})…` }],
243
+ details: { advisorModel: advisorLabel, effort },
244
+ });
245
+
246
+ try {
247
+ const response = await completeSimple(
248
+ advisor,
249
+ { systemPrompt: ADVISOR_SYSTEM_PROMPT, messages: [userMessage] },
250
+ { apiKey: auth.apiKey, headers: auth.headers, signal, reasoning: effort },
251
+ );
252
+
253
+ if (response.stopReason === "aborted") {
254
+ return {
255
+ content: [
256
+ { type: "text", text: "Advisor call was cancelled before it completed." },
257
+ ],
258
+ details: {
259
+ advisorModel: advisorLabel,
260
+ effort,
261
+ usage: response.usage,
262
+ stopReason: response.stopReason,
263
+ errorMessage: response.errorMessage ?? "aborted",
264
+ },
265
+ };
266
+ }
267
+
268
+ if (response.stopReason === "error") {
269
+ return {
270
+ content: [
271
+ {
272
+ type: "text",
273
+ text: `Advisor call failed: ${response.errorMessage ?? "unknown error"}`,
274
+ },
275
+ ],
276
+ details: {
277
+ advisorModel: advisorLabel,
278
+ effort,
279
+ usage: response.usage,
280
+ stopReason: response.stopReason,
281
+ errorMessage: response.errorMessage,
282
+ },
283
+ };
284
+ }
285
+
286
+ const advisorText = response.content
287
+ .filter((c): c is { type: "text"; text: string } => c.type === "text")
288
+ .map((c) => c.text)
289
+ .join("\n")
290
+ .trim();
291
+
292
+ if (!advisorText) {
293
+ return {
294
+ content: [{ type: "text", text: "Advisor returned no text content." }],
295
+ details: {
296
+ advisorModel: advisorLabel,
297
+ effort,
298
+ usage: response.usage,
299
+ stopReason: response.stopReason,
300
+ errorMessage: "empty response",
301
+ },
302
+ };
303
+ }
304
+
305
+ return {
306
+ content: [{ type: "text", text: advisorText }],
307
+ details: {
308
+ advisorModel: advisorLabel,
309
+ effort,
310
+ usage: response.usage,
311
+ stopReason: response.stopReason,
312
+ },
313
+ };
314
+ } catch (err) {
315
+ const message = err instanceof Error ? err.message : String(err);
316
+ return buildErrorResult(
317
+ advisorLabel,
318
+ `Advisor call threw: ${message}`,
319
+ message,
320
+ );
321
+ }
322
+ }
323
+
324
+ // ---------------------------------------------------------------------------
325
+ // Tool registration — zero-param schema, curated description/snippet/guidelines
326
+ // ---------------------------------------------------------------------------
327
+
328
+ const AdvisorParams = Type.Object({});
329
+
330
+ const ADVISOR_DESCRIPTION =
331
+ "Escalate to a stronger reviewer model for guidance. When you need " +
332
+ "stronger judgment — a complex decision, an ambiguous failure, a problem " +
333
+ "you're circling without progress — escalate to the advisor model for " +
334
+ "guidance, then resume. Takes NO parameters — when you call advisor(), " +
335
+ "your entire conversation history is automatically forwarded. The advisor " +
336
+ "sees the task, every tool call you've made, every result you've seen.";
337
+
338
+ const ADVISOR_PROMPT_SNIPPET =
339
+ "Escalate to a stronger reviewer model for guidance when stuck, before substantive work, or before declaring done";
340
+
341
+ const ADVISOR_PROMPT_GUIDELINES: string[] = [
342
+ "Call `advisor` BEFORE substantive work — before writing, before committing to an interpretation, before building on an assumption. Orientation (finding files, fetching a source, seeing what's there) is not substantive work; writing, editing, and declaring an answer are.",
343
+ "Also call `advisor` when you believe the task is complete. BEFORE this call, make your deliverable durable: write the file, save the result, commit the change. The advisor call takes time; if the session ends during it, a durable result persists and an unwritten one doesn't.",
344
+ "Also call `advisor` when stuck — errors recurring, approach not converging, results that don't fit — or when considering a change of approach.",
345
+ "On tasks longer than a few steps, call `advisor` at least once before committing to an approach and once before declaring done. On short reactive tasks where the next action is dictated by tool output you just read, you don't need to keep calling — the advisor adds most of its value on the first call, before the approach crystallizes.",
346
+ "Give the advisor's advice serious weight. If you follow a step and it fails empirically, or you have primary-source evidence that contradicts a specific claim, adapt — a passing self-test is not evidence the advice is wrong, it's evidence your test doesn't check what the advice is checking.",
347
+ "If you've already retrieved data pointing one way and the advisor points another, don't silently switch — surface the conflict in one more `advisor` call (\"I found X, you suggest Y, which constraint breaks the tie?\"). A reconcile call is cheaper than committing to the wrong branch.",
348
+ ];
349
+
350
+ export function registerAdvisorTool(pi: ExtensionAPI): void {
351
+ pi.registerTool({
352
+ name: ADVISOR_TOOL_NAME,
353
+ label: "Advisor",
354
+ description: ADVISOR_DESCRIPTION,
355
+ promptSnippet: ADVISOR_PROMPT_SNIPPET,
356
+ promptGuidelines: ADVISOR_PROMPT_GUIDELINES,
357
+ parameters: AdvisorParams,
358
+
359
+ async execute(_toolCallId, _params, signal, onUpdate, ctx) {
360
+ return executeAdvisor(ctx, signal, onUpdate);
361
+ },
362
+ });
363
+ }
364
+
365
+ // ---------------------------------------------------------------------------
366
+ // before_agent_start handler — strip advisor from active tools when disabled
367
+ // ---------------------------------------------------------------------------
368
+
369
+ export function registerAdvisorBeforeAgentStart(pi: ExtensionAPI): void {
370
+ pi.on("before_agent_start", async () => {
371
+ if (!getAdvisorModel()) {
372
+ const active = pi.getActiveTools();
373
+ if (active.includes(ADVISOR_TOOL_NAME)) {
374
+ pi.setActiveTools(active.filter((n) => n !== ADVISOR_TOOL_NAME));
375
+ }
376
+ }
377
+ });
378
+ }
379
+
380
+ // ---------------------------------------------------------------------------
381
+ // /advisor slash command — opens selector panel for picking the advisor model
382
+ // ---------------------------------------------------------------------------
383
+
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
+ function modelKey(m: { provider: string; id: string }): string {
406
+ return `${m.provider}:${m.id}`;
407
+ }
408
+
409
+ export function registerAdvisorCommand(pi: ExtensionAPI): void {
410
+ pi.registerCommand("advisor", {
411
+ description: "Configure the advisor model for the advisor-strategy pattern",
412
+ handler: async (_args, ctx) => {
413
+ if (!ctx.hasUI) {
414
+ ctx.ui.notify("/advisor requires interactive mode", "error");
415
+ return;
416
+ }
417
+
418
+ const availableModels = ctx.modelRegistry.getAvailable();
419
+ const current = getAdvisorModel();
420
+ const currentKey = current ? modelKey(current) : undefined;
421
+
422
+ const items: SelectItem[] = availableModels.map((m) => {
423
+ const key = modelKey(m);
424
+ const check = key === currentKey ? " ✓" : "";
425
+ return { value: key, label: `${m.name} (${m.provider})${check}` };
426
+ });
427
+ items.push({
428
+ value: NO_ADVISOR_VALUE,
429
+ label: currentKey === undefined ? "No advisor ✓" : "No advisor",
430
+ });
431
+
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
+
492
+ if (!choice) {
493
+ return;
494
+ }
495
+
496
+ const activeTools = pi.getActiveTools();
497
+ const activeHas = activeTools.includes(ADVISOR_TOOL_NAME);
498
+
499
+ if (choice === NO_ADVISOR_VALUE) {
500
+ setAdvisorModel(undefined);
501
+ setAdvisorEffort(undefined);
502
+ saveAdvisorConfig(undefined, undefined);
503
+ if (activeHas) {
504
+ pi.setActiveTools(
505
+ activeTools.filter((n) => n !== ADVISOR_TOOL_NAME),
506
+ );
507
+ }
508
+ ctx.ui.notify("Advisor disabled", "info");
509
+ return;
510
+ }
511
+
512
+ const picked = availableModels.find((m) => modelKey(m) === choice);
513
+ if (!picked) {
514
+ ctx.ui.notify(`Advisor selection not found: ${choice}`, "error");
515
+ return;
516
+ }
517
+
518
+ // Effort picker — only for reasoning-capable models
519
+ let effortChoice: ThinkingLevel | undefined;
520
+ 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;
526
+
527
+ const effortItems: SelectItem[] = [
528
+ { value: OFF_VALUE, label: "off" },
529
+ ...levels.map((level) => ({
530
+ value: level,
531
+ label: level === "high" ? `${level} (recommended)` : level,
532
+ })),
533
+ ];
534
+
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
+
598
+ if (!effortResult) {
599
+ return;
600
+ }
601
+ effortChoice = effortResult === OFF_VALUE ? undefined : effortResult as ThinkingLevel;
602
+ }
603
+
604
+ setAdvisorEffort(effortChoice);
605
+ setAdvisorModel(picked);
606
+ saveAdvisorConfig(modelKey(picked), effortChoice);
607
+ if (!activeHas) {
608
+ pi.setActiveTools([...activeTools, ADVISOR_TOOL_NAME]);
609
+ }
610
+ ctx.ui.notify(
611
+ `Advisor: ${picked.provider}:${picked.id}${effortChoice ? `, ${effortChoice}` : ""}`,
612
+ "info",
613
+ );
614
+ },
615
+ });
616
+ }
package/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * rpiv-advisor — Pi extension
3
+ *
4
+ * Registers the `advisor` tool, `/advisor` command, and the two lifecycle
5
+ * hooks (session_start restore, before_agent_start strip) that together
6
+ * implement the advisor-strategy pattern.
7
+ *
8
+ * Config persists at ~/.config/rpiv-advisor/advisor.json. Tool name
9
+ * preserved verbatim from rpiv-pi@7525a5d.
10
+ */
11
+
12
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
13
+ import {
14
+ registerAdvisorTool,
15
+ registerAdvisorCommand,
16
+ registerAdvisorBeforeAgentStart,
17
+ restoreAdvisorState,
18
+ } from "./advisor.js";
19
+
20
+ export default function (pi: ExtensionAPI) {
21
+ registerAdvisorTool(pi);
22
+ registerAdvisorCommand(pi);
23
+ registerAdvisorBeforeAgentStart(pi);
24
+
25
+ pi.on("session_start", async (_event, ctx) => {
26
+ restoreAdvisorState(ctx, pi);
27
+ });
28
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@juicesharp/rpiv-advisor",
3
+ "version": "0.1.0",
4
+ "description": "Pi extension: advisor-strategy pattern — escalate to a stronger reviewer model",
5
+ "keywords": ["pi-package", "pi-extension", "rpiv", "advisor"],
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "author": "juicesharp",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/juicesharp/rpiv-advisor.git"
12
+ },
13
+ "homepage": "https://github.com/juicesharp/rpiv-advisor#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/juicesharp/rpiv-advisor/issues"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "pi": {
21
+ "extensions": ["./index.ts"]
22
+ },
23
+ "peerDependencies": {
24
+ "@mariozechner/pi-ai": "*",
25
+ "@mariozechner/pi-coding-agent": "*",
26
+ "@mariozechner/pi-tui": "*",
27
+ "@sinclair/typebox": "*"
28
+ }
29
+ }