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

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.
@@ -355,10 +355,11 @@ function recoverReviewControl(state: SessionState): void {
355
355
  }
356
356
 
357
357
  type AdvisorHintDetails = {
358
+ kind?: "handoff" | "answer";
358
359
  decision?: "continue" | "review" | "defer";
359
360
  reason?: string;
360
361
  summary?: string;
361
- actions?: string[];
362
+ actions?: unknown;
362
363
  };
363
364
 
364
365
  type ReviewControlState = {
@@ -379,37 +380,77 @@ type ReviewMaterialMeta = {
379
380
  isAgentEnd: boolean;
380
381
  materialSignals?: string[];
381
382
  };
382
- function sendAdvisorHint(pi: ExtensionAPI, decision: "continue" | "review" | "defer", reason: string, summary: string, actions: string[] = []) {
383
+
384
+ function normalizeAdvisorActions(actions: unknown): string[] {
385
+ const raw = Array.isArray(actions) ? actions : typeof actions === "string" ? [actions] : [];
386
+ return raw.map((action) => squish(action, 200)).filter(Boolean).slice(0, 2);
387
+ }
388
+
389
+ function advisorHandoffText(decision: "continue" | "review" | "defer", reason: string, summary: string, actions: unknown = []): string {
390
+ const limitedActions = normalizeAdvisorActions(actions);
391
+ return [
392
+ `Advisor verdict: ${decision}.`,
393
+ reason ? `Reason: ${reason}` : "",
394
+ summary ? `Summary: ${summary}` : "",
395
+ limitedActions.length ? `Actions: ${limitedActions.join("; ")}` : "",
396
+ ].filter(Boolean).join("\n");
397
+ }
398
+
399
+ function sendAdvisorHint(pi: ExtensionAPI, decision: "continue" | "review" | "defer", reason: string, summary: string, actions: unknown = []) {
400
+ const limitedActions = normalizeAdvisorActions(actions);
383
401
  pi.sendMessage(
384
402
  {
385
403
  customType: "advisor:llm",
386
- content: reason,
404
+ content: advisorHandoffText(decision, reason, summary, limitedActions),
387
405
  display: true,
388
- details: { decision, reason, summary, actions: actions.slice(0, 2) },
406
+ details: { decision, reason, summary, actions: limitedActions },
389
407
  },
390
408
  { deliverAs: "followUp" },
391
409
  );
392
410
  }
393
411
 
412
+ function sendAdvisorAnswer(pi: ExtensionAPI, text: string) {
413
+ pi.sendMessage({
414
+ customType: "advisor:llm",
415
+ content: text,
416
+ display: true,
417
+ details: { kind: "answer", summary: text },
418
+ });
419
+ }
420
+
394
421
  function renderAdvisorHint(message: any, options: { expanded?: boolean }, theme: any) {
395
422
  const details = (message?.details ?? {}) as AdvisorHintDetails;
396
423
  const customType = String(message?.customType ?? "advisor:rules");
397
- const decision = details.decision ?? "defer";
398
424
  const sourceColor = customType === "advisor:llm" ? "success" : customType === "advisor:model" ? "accent" : "muted";
399
- const decisionColor = decision === "review" ? "accent" : decision === "continue" ? "muted" : "dim";
400
425
  const source = theme.bold(theme.fg(sourceColor, `[${customType}]`));
426
+
427
+ if (details.kind === "answer") {
428
+ const body = contentText(message?.content) || details.summary || "No advisor response.";
429
+ const box = new Box(1, 1, (s: string) => theme.bg("customMessageBg", s));
430
+ box.addChild(new Text(`${theme.bold(theme.fg("success", "↗"))} ${source} ${theme.bold(theme.fg("success", "answer"))}`, 0, 0));
431
+ box.addChild(new Text(theme.fg("dim", body), 0, 0));
432
+ return box;
433
+ }
434
+
435
+ const decision = details.decision ?? "defer";
436
+ const decisionColor = decision === "review" ? "accent" : decision === "continue" ? "muted" : "dim";
401
437
  const verdict = theme.bold(theme.fg(decisionColor, decision));
402
438
  const glyph = decision === "review" ? "↗" : decision === "defer" ? "…" : "·";
403
439
  const reason = squish(details.reason || contentText(message?.content) || "no extra detail", 180);
440
+ const actions = normalizeAdvisorActions(details.actions);
404
441
 
405
442
  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));
443
+ box.addChild(new Text(`${theme.bold(theme.fg(decisionColor, glyph))} ${source} ${verdict}`, 0, 0));
444
+ box.addChild(new Text(theme.fg("dim", `reason: ${reason}`), 0, 0));
407
445
 
408
- if (options.expanded && details.summary) {
446
+ if (details.summary) {
409
447
  box.addChild(new Text(theme.fg("dim", `summary: ${squish(details.summary, 220)}`), 0, 0));
410
448
  }
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));
449
+ if (actions.length) {
450
+ box.addChild(new Text(theme.fg("dim", `actions: ${actions.map((a) => squish(a, 80)).join(" • ")}`), 0, 0));
451
+ }
452
+ if (!options.expanded && contentText(message?.content).split("\n").length > 3) {
453
+ box.addChild(new Text(theme.fg("dim", "Ctrl+O full advisor handoff"), 0, 0));
413
454
  }
414
455
 
415
456
  return box;
@@ -933,14 +974,15 @@ async function doReview(pi: ExtensionAPI, ctx: any, trigger: string, delta: stri
933
974
  : json.verdict === "not_done" ? "review"
934
975
  : "defer";
935
976
  finalDecision = decision;
936
- finalReason = (json.reason || json.summary || "review result").slice(0, 120);
977
+ const rawReason = json.reason || json.summary || "review result";
978
+ finalReason = rawReason.slice(0, 120);
937
979
 
938
980
  const display = formatAdvisorDisplay("advisor:llm", decision, finalReason);
939
981
  writeText(CURRENT_PATH, `${display}\n`);
940
- sendAdvisorHint(pi, decision, finalReason, json.summary || "", json.actions || []);
982
+ sendAdvisorHint(pi, decision, rawReason, json.summary || "", json.actions || []);
941
983
 
942
984
  if (json.verdict !== "on_track") {
943
- state.followUp = [json.summary, ...(json.actions?.slice(0, 2) || [])].filter(Boolean).join(" — ");
985
+ state.followUp = [json.summary, ...normalizeAdvisorActions(json.actions)].filter(Boolean).join(" — ");
944
986
  }
945
987
 
946
988
  markReviewApplied(state, signature, trigger, finalDecision, finalReason, false);
@@ -1326,7 +1368,11 @@ export function registerAdvisor(pi: ExtensionAPI): void {
1326
1368
 
1327
1369
  // Anything else: treat as a question to the advisor
1328
1370
  const r = await askAdvisor(pi, ctx, a, "slash", true);
1329
- ctx.ui.notify(r.text, r.error ? "warning" : "info");
1371
+ if (r.error) {
1372
+ ctx.ui.notify(r.text, "warning");
1373
+ return;
1374
+ }
1375
+ sendAdvisorAnswer(pi, r.text);
1330
1376
  },
1331
1377
  });
1332
1378
  }
@@ -17,9 +17,11 @@ vi.mock("@earendil-works/pi-ai", async () => {
17
17
  type Handler = (event: any, ctx: any) => any;
18
18
 
19
19
  type HandlerMap = Record<string, Handler[]>;
20
+ type CommandMap = Record<string, { handler: (args: string, ctx: any) => any }>;
20
21
 
21
22
  function makeHandlers() {
22
23
  const handlers: HandlerMap = {};
24
+ const commands: CommandMap = {};
23
25
  const sendMessage = vi.fn();
24
26
 
25
27
  const pi = {
@@ -28,7 +30,9 @@ function makeHandlers() {
28
30
  handlers[event].push(handler);
29
31
  },
30
32
  registerMessageRenderer: () => undefined,
31
- registerCommand: () => undefined,
33
+ registerCommand: (name: string, command: { handler: (args: string, ctx: any) => any }) => {
34
+ commands[name] = command;
35
+ },
32
36
  registerTool: vi.fn(),
33
37
  sendMessage,
34
38
  sendUserMessage: () => undefined,
@@ -38,12 +42,13 @@ function makeHandlers() {
38
42
  },
39
43
  };
40
44
 
41
- return { handlers, pi: pi as any, sendMessage };
45
+ return { handlers, commands, pi: pi as any, sendMessage };
42
46
  }
43
47
 
44
48
  const ADVISOR_STATE_DIR = join(homedir(), ".pi", "agent", "pi-rogue", "advisor");
45
49
  const ADVISOR_STATE_PATH = join(ADVISOR_STATE_DIR, "state.json");
46
50
  const ADVISOR_CONFIG_PATH = join(ADVISOR_STATE_DIR, "config.json");
51
+ const ADVISOR_CACHE_PATH = join(ADVISOR_STATE_DIR, "cache.json");
47
52
 
48
53
  function readAdvisorState(): any {
49
54
  return JSON.parse(readFileSync(ADVISOR_STATE_PATH, "utf8"));
@@ -73,21 +78,26 @@ function mkCtx() {
73
78
  describe("advisor two-agent convergence", () => {
74
79
  let ctx: any;
75
80
  let handlers: HandlerMap;
81
+ let commands: CommandMap;
76
82
  let sendMessageMock: ReturnType<typeof vi.fn>;
77
83
  let completeSimpleMock: ReturnType<typeof vi.fn>;
78
84
  let priorState: string | null = null;
79
85
  let priorConfig: string | null = null;
86
+ let priorCache: string | null = null;
80
87
 
81
88
  beforeEach(() => {
82
89
  priorState = existsSync(ADVISOR_STATE_PATH) ? readFileSync(ADVISOR_STATE_PATH, "utf8") : null;
83
90
  priorConfig = existsSync(ADVISOR_CONFIG_PATH) ? readFileSync(ADVISOR_CONFIG_PATH, "utf8") : null;
91
+ priorCache = existsSync(ADVISOR_CACHE_PATH) ? readFileSync(ADVISOR_CACHE_PATH, "utf8") : null;
84
92
 
85
93
  const setup = makeHandlers();
86
94
  handlers = setup.handlers;
95
+ commands = setup.commands;
87
96
  sendMessageMock = setup.sendMessage;
88
97
 
89
98
  mkdirSync(dirname(ADVISOR_STATE_PATH), { recursive: true });
90
99
  writeFileSync(ADVISOR_CONFIG_PATH, JSON.stringify({ mode: "auto", review: "light", checkins: "off", checkinIntervalMinutes: 30 }, null, 2), "utf8");
100
+ writeFileSync(ADVISOR_CACHE_PATH, "{}", "utf8");
91
101
  writeFileSync(ADVISOR_STATE_PATH, JSON.stringify({
92
102
  turns: 0,
93
103
  lastTask: "",
@@ -136,6 +146,12 @@ describe("advisor two-agent convergence", () => {
136
146
  } else {
137
147
  writeFileSync(ADVISOR_CONFIG_PATH, priorConfig, "utf8");
138
148
  }
149
+
150
+ if (priorCache === null) {
151
+ writeFileSync(ADVISOR_CACHE_PATH, "{}", "utf8");
152
+ } else {
153
+ writeFileSync(ADVISOR_CACHE_PATH, priorCache, "utf8");
154
+ }
139
155
  });
140
156
 
141
157
  it("does not re-run advisory review on repeated material snapshots", async () => {
@@ -160,6 +176,17 @@ describe("advisor two-agent convergence", () => {
160
176
  const firstState = readAdvisorState();
161
177
  expect(firstState.reviewControl.lastDecision).toBe("review");
162
178
  expect(firstState.followUp).toContain("Closeout is incomplete");
179
+ expect(sendMessageMock).toHaveBeenCalledWith(
180
+ expect.objectContaining({
181
+ customType: "advisor:llm",
182
+ content: expect.stringContaining("Summary: Closeout is incomplete"),
183
+ }),
184
+ expect.anything(),
185
+ );
186
+ expect(sendMessageMock).toHaveBeenCalledWith(
187
+ expect.objectContaining({ content: expect.stringContaining("Actions: run focused check") }),
188
+ expect.anything(),
189
+ );
163
190
  expect(completeSimpleMock).toHaveBeenCalledTimes(1);
164
191
 
165
192
  const consumedPrompt = await preflight![0]({ systemPrompt: "SYS", prompt: basePrompt }, ctx);
@@ -183,6 +210,71 @@ describe("advisor two-agent convergence", () => {
183
210
  expect(String(withoutFollowUp?.systemPrompt)).not.toContain("Advisor follow-up");
184
211
  });
185
212
 
213
+ it("normalizes string actions in advisor handoffs", async () => {
214
+ const preflight = handlers.before_agent_start;
215
+ const turnEnd = handlers.turn_end;
216
+ expect(preflight?.length).toBe(1);
217
+ expect(turnEnd?.length).toBe(1);
218
+
219
+ completeSimpleMock.mockResolvedValue({
220
+ content: [{
221
+ type: "text",
222
+ text: JSON.stringify({
223
+ verdict: "not_done",
224
+ summary: "Closeout is incomplete",
225
+ reason: "Verification is missing",
226
+ actions: "run focused check",
227
+ checklist: [],
228
+ notify: true,
229
+ }),
230
+ }],
231
+ });
232
+
233
+ await handlers.session_start?.[0]?.({}, ctx);
234
+ await preflight![0]({ systemPrompt: "SYS", prompt: "Continue the current goal" }, ctx);
235
+ await turnEnd![0]({
236
+ toolResults: [{ toolName: "edit" }],
237
+ message: { role: "assistant", content: "Repo-side autoresearch is verified closed. Only optional external rollout/CI smoke remains." },
238
+ }, ctx);
239
+
240
+ const state = readAdvisorState();
241
+ expect(completeSimpleMock).toHaveBeenCalledTimes(1);
242
+ expect(state.followUp).toBe("Closeout is incomplete — run focused check");
243
+ expect(sendMessageMock).toHaveBeenCalledWith(
244
+ expect.objectContaining({
245
+ customType: "advisor:llm",
246
+ content: expect.stringContaining("Actions: run focused check"),
247
+ details: expect.objectContaining({ actions: ["run focused check"] }),
248
+ }),
249
+ expect.anything(),
250
+ );
251
+ });
252
+
253
+ it("renders manual advisor answers as advisor custom messages", async () => {
254
+ expect(commands.advisor).toBeTruthy();
255
+
256
+ completeSimpleMock.mockResolvedValue({
257
+ content: [{
258
+ type: "text",
259
+ text: "Post-turn review: no merge blockers identified from the session brief.",
260
+ }],
261
+ });
262
+
263
+ await commands.advisor.handler("should we merge this pr?", ctx);
264
+
265
+ expect(sendMessageMock).toHaveBeenCalledWith(
266
+ expect.objectContaining({
267
+ customType: "advisor:llm",
268
+ content: "Post-turn review: no merge blockers identified from the session brief.",
269
+ display: true,
270
+ details: expect.objectContaining({
271
+ kind: "answer",
272
+ summary: "Post-turn review: no merge blockers identified from the session brief.",
273
+ }),
274
+ }),
275
+ );
276
+ });
277
+
186
278
  it("does not re-run advisory review on repeated agent-end material snapshots", async () => {
187
279
  const preflight = handlers.before_agent_start;
188
280
  const agentEnd = handlers.agent_end;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiale-plus/pi-rogue-bundle",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Public Pi-Rogue bundle for advisor and orchestration. Single consolidated artefact (advisor and orchestration releases paused; their packages are private and bundled here).",
5
5
  "type": "module",
6
6
  "license": "MIT",