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

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.
@@ -6,6 +6,7 @@ import {
6
6
  completeWithModelFallback,
7
7
  contentText,
8
8
  normalizeAdvisorConfig,
9
+ sanitizeAdvisorText,
9
10
  shouldRunCheckin,
10
11
  type AdvisorConfig,
11
12
  } from "./extension.js";
@@ -86,6 +87,17 @@ describe("advisor message extraction", () => {
86
87
  expect(contentText([{ type: "toolResult", content: [{ type: "text", text: "ok" }] }])).toBe("ok");
87
88
  expect(contentText({ arbitrary: "shape" })).toBe("");
88
89
  });
90
+
91
+ it("redacts transient clipboard image paths from advisor-facing text", () => {
92
+ const text = "see /var/folders/fm/rwczdnws5j58x7kbyn3vcx_h0000gn/T/clipboard-2026-06-04-012248-DEE3A154.png please";
93
+ expect(sanitizeAdvisorText(text)).toBe("see [clipboard image] please");
94
+ expect(contentText({ content: [{ type: "text", text }] })).toBe("see [clipboard image] please");
95
+ });
96
+
97
+ it("does not redact ordinary repo or temp file paths", () => {
98
+ const text = "inspect /Users/pavel/repos/fiale-plus/pi-rogue/packages/advisor/src/extension.ts and /tmp/benchmark-results.json";
99
+ expect(sanitizeAdvisorText(text)).toBe(text);
100
+ });
89
101
  });
90
102
 
91
103
  describe("mid-hour check-ins", () => {
@@ -234,16 +234,22 @@ function hash(...parts: string[]): string {
234
234
 
235
235
  function brief(s: SessionState): string {
236
236
  const lines: string[] = [];
237
- if (s.lastTask) lines.push(`Task: ${truncate(s.lastTask, 200)}`);
237
+ if (s.lastTask) lines.push(`Task: ${truncate(sanitizeAdvisorText(s.lastTask), 200)}`);
238
238
  if (s.turns) lines.push(`Turns: ${s.turns}`);
239
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(" | ")}`);
240
+ if (s.files.length) lines.push(`Files: ${sanitizeAdvisorText(s.files.slice(-4).join(", "))}`);
241
+ if (s.errors.length) lines.push(`Errors: ${sanitizeAdvisorText(s.errors.slice(-2).join(" | "))}`);
242
242
  return lines.join("\n").slice(0, 1200);
243
243
  }
244
244
 
245
+ const CLIPBOARD_IMAGE_PATH_RE = /(?:\/(?:private\/)?var\/folders\/[^\s"'`<>]+\/T|\/(?:tmp|var\/tmp))\/clipboard-\d{4}-\d{2}-\d{2}-[A-Za-z0-9-]+\.(?:png|jpe?g|gif|webp)\b/g;
246
+
247
+ export function sanitizeAdvisorText(text: unknown): string {
248
+ return String(text ?? "").replace(CLIPBOARD_IMAGE_PATH_RE, "[clipboard image]");
249
+ }
250
+
245
251
  function squish(t: unknown, max = 200): string {
246
- const s = String(t ?? "").replace(/\s+/g, " ").trim();
252
+ const s = sanitizeAdvisorText(t).replace(/\s+/g, " ").trim();
247
253
  return s.length <= max ? s : s.slice(0, max - 1).trimEnd() + "…";
248
254
  }
249
255
 
@@ -386,35 +392,64 @@ function normalizeAdvisorActions(actions: unknown): string[] {
386
392
  return raw.map((action) => squish(action, 200)).filter(Boolean).slice(0, 2);
387
393
  }
388
394
 
395
+ function comparableAdvisorText(text: string): string {
396
+ return sanitizeAdvisorText(text).toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
397
+ }
398
+
399
+ function isRedundantAdvisorSummary(reason: string, summary: string): boolean {
400
+ const r = comparableAdvisorText(reason);
401
+ const s = comparableAdvisorText(summary);
402
+ if (!s) return true;
403
+ if (!r) return false;
404
+ if (r === s) return true;
405
+ if (Math.min(r.length, s.length) >= 60 && (r.includes(s) || s.includes(r))) return true;
406
+
407
+ const rTokens = new Set(r.split(" ").filter((token) => token.length > 2));
408
+ const sTokens = new Set(s.split(" ").filter((token) => token.length > 2));
409
+ if (rTokens.size < 8 || sTokens.size < 8) return false;
410
+ const overlap = [...sTokens].filter((token) => rTokens.has(token)).length;
411
+ return overlap / Math.max(rTokens.size, sTokens.size) >= 0.86;
412
+ }
413
+
414
+ function distinctAdvisorSummary(reason: string, summary: string): string {
415
+ const cleanSummary = sanitizeAdvisorText(summary).trim();
416
+ return isRedundantAdvisorSummary(reason, cleanSummary) ? "" : cleanSummary;
417
+ }
418
+
389
419
  function advisorHandoffText(decision: "continue" | "review" | "defer", reason: string, summary: string, actions: unknown = []): string {
390
420
  const limitedActions = normalizeAdvisorActions(actions);
421
+ const cleanReason = sanitizeAdvisorText(reason);
422
+ const cleanSummary = distinctAdvisorSummary(cleanReason, summary);
391
423
  return [
392
424
  `Advisor verdict: ${decision}.`,
393
- reason ? `Reason: ${reason}` : "",
394
- summary ? `Summary: ${summary}` : "",
425
+ cleanReason ? `Reason: ${cleanReason}` : "",
426
+ cleanSummary ? `Summary: ${cleanSummary}` : "",
395
427
  limitedActions.length ? `Actions: ${limitedActions.join("; ")}` : "",
396
428
  ].filter(Boolean).join("\n");
397
429
  }
398
430
 
399
431
  function sendAdvisorHint(pi: ExtensionAPI, decision: "continue" | "review" | "defer", reason: string, summary: string, actions: unknown = []) {
432
+ const cleanReason = sanitizeAdvisorText(reason);
433
+ const cleanSummary = distinctAdvisorSummary(cleanReason, summary);
400
434
  const limitedActions = normalizeAdvisorActions(actions);
401
435
  pi.sendMessage(
402
436
  {
403
437
  customType: "advisor:llm",
404
- content: advisorHandoffText(decision, reason, summary, limitedActions),
438
+ content: advisorHandoffText(decision, cleanReason, cleanSummary, limitedActions),
405
439
  display: true,
406
- details: { decision, reason, summary, actions: limitedActions },
440
+ details: { kind: "handoff", decision, reason: cleanReason, summary: cleanSummary, actions: limitedActions },
407
441
  },
408
442
  { deliverAs: "followUp" },
409
443
  );
410
444
  }
411
445
 
412
446
  function sendAdvisorAnswer(pi: ExtensionAPI, text: string) {
447
+ const cleanText = sanitizeAdvisorText(text);
413
448
  pi.sendMessage({
414
449
  customType: "advisor:llm",
415
- content: text,
450
+ content: cleanText,
416
451
  display: true,
417
- details: { kind: "answer", summary: text },
452
+ details: { kind: "answer", summary: cleanText },
418
453
  });
419
454
  }
420
455
 
@@ -425,7 +460,7 @@ function renderAdvisorHint(message: any, options: { expanded?: boolean }, theme:
425
460
  const source = theme.bold(theme.fg(sourceColor, `[${customType}]`));
426
461
 
427
462
  if (details.kind === "answer") {
428
- const body = contentText(message?.content) || details.summary || "No advisor response.";
463
+ const body = sanitizeAdvisorText(contentText(message?.content) || details.summary || "No advisor response.");
429
464
  const box = new Box(1, 1, (s: string) => theme.bg("customMessageBg", s));
430
465
  box.addChild(new Text(`${theme.bold(theme.fg("success", "↗"))} ${source} ${theme.bold(theme.fg("success", "answer"))}`, 0, 0));
431
466
  box.addChild(new Text(theme.fg("dim", body), 0, 0));
@@ -438,19 +473,30 @@ function renderAdvisorHint(message: any, options: { expanded?: boolean }, theme:
438
473
  const glyph = decision === "review" ? "↗" : decision === "defer" ? "…" : "·";
439
474
  const reason = squish(details.reason || contentText(message?.content) || "no extra detail", 180);
440
475
  const actions = normalizeAdvisorActions(details.actions);
476
+ const fullHandoff = sanitizeAdvisorText(
477
+ (details.reason || details.summary || actions.length)
478
+ ? advisorHandoffText(decision, details.reason || "", details.summary || "", actions)
479
+ : contentText(message?.content),
480
+ );
441
481
 
442
482
  const box = new Box(1, 1, (s: string) => theme.bg("customMessageBg", s));
443
483
  box.addChild(new Text(`${theme.bold(theme.fg(decisionColor, glyph))} ${source} ${verdict}`, 0, 0));
444
484
  box.addChild(new Text(theme.fg("dim", `reason: ${reason}`), 0, 0));
445
485
 
446
- if (details.summary) {
447
- box.addChild(new Text(theme.fg("dim", `summary: ${squish(details.summary, 220)}`), 0, 0));
448
- }
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));
486
+ if (options.expanded) {
487
+ box.addChild(new Text(theme.fg("dim", "full handoff:"), 0, 0));
488
+ box.addChild(new Text(theme.fg("dim", fullHandoff), 0, 0));
489
+ } else {
490
+ const summary = distinctAdvisorSummary(details.reason || "", details.summary || "");
491
+ if (summary) {
492
+ box.addChild(new Text(theme.fg("dim", `summary: ${squish(summary, 220)}`), 0, 0));
493
+ }
494
+ if (actions.length) {
495
+ box.addChild(new Text(theme.fg("dim", `actions: ${actions.map((a) => squish(a, 80)).join(" • ")}`), 0, 0));
496
+ }
497
+ if (fullHandoff.split("\n").length > 3) {
498
+ box.addChild(new Text(theme.fg("dim", "Ctrl+O full advisor handoff"), 0, 0));
499
+ }
454
500
  }
455
501
 
456
502
  return box;
@@ -458,15 +504,15 @@ function renderAdvisorHint(message: any, options: { expanded?: boolean }, theme:
458
504
 
459
505
  /** Extract readable text from message content (handles strings, blocks, and nested message payloads). */
460
506
  export function contentText(content: unknown): string {
461
- if (typeof content === "string") return content.trim();
507
+ if (typeof content === "string") return sanitizeAdvisorText(content).trim();
462
508
  if (content && typeof content === "object" && !Array.isArray(content)) {
463
509
  const obj = content as Record<string, unknown>;
464
- if (typeof obj.text === "string") return obj.text.trim();
510
+ if (typeof obj.text === "string") return sanitizeAdvisorText(obj.text).trim();
465
511
  if (obj.content !== undefined) return contentText(obj.content);
466
512
  if (obj.message !== undefined) return contentText(obj.message);
467
513
  return "";
468
514
  }
469
- if (!Array.isArray(content)) return String(content ?? "").trim();
515
+ if (!Array.isArray(content)) return sanitizeAdvisorText(content).trim();
470
516
  const parts: string[] = [];
471
517
  for (const item of content) {
472
518
  if (!item) continue;
@@ -483,7 +529,7 @@ export function contentText(content: unknown): string {
483
529
  if (nested) parts.push(nested);
484
530
  }
485
531
  }
486
- return parts.join("\n").replace(/\s+/g, " ").trim();
532
+ return sanitizeAdvisorText(parts.join("\n")).replace(/\s+/g, " ").trim();
487
533
  }
488
534
 
489
535
  /** Check if a tool result or message indicates an actual execution failure */
@@ -974,7 +1020,7 @@ async function doReview(pi: ExtensionAPI, ctx: any, trigger: string, delta: stri
974
1020
  : json.verdict === "not_done" ? "review"
975
1021
  : "defer";
976
1022
  finalDecision = decision;
977
- const rawReason = json.reason || json.summary || "review result";
1023
+ const rawReason = sanitizeAdvisorText(json.reason || json.summary || "review result");
978
1024
  finalReason = rawReason.slice(0, 120);
979
1025
 
980
1026
  const display = formatAdvisorDisplay("advisor:llm", decision, finalReason);
@@ -982,7 +1028,7 @@ async function doReview(pi: ExtensionAPI, ctx: any, trigger: string, delta: stri
982
1028
  sendAdvisorHint(pi, decision, rawReason, json.summary || "", json.actions || []);
983
1029
 
984
1030
  if (json.verdict !== "on_track") {
985
- state.followUp = [json.summary, ...normalizeAdvisorActions(json.actions)].filter(Boolean).join(" — ");
1031
+ state.followUp = [sanitizeAdvisorText(json.summary), ...normalizeAdvisorActions(json.actions)].filter(Boolean).join(" — ");
986
1032
  }
987
1033
 
988
1034
  markReviewApplied(state, signature, trigger, finalDecision, finalReason, false);
@@ -18,10 +18,12 @@ type Handler = (event: any, ctx: any) => any;
18
18
 
19
19
  type HandlerMap = Record<string, Handler[]>;
20
20
  type CommandMap = Record<string, { handler: (args: string, ctx: any) => any }>;
21
+ type MessageRendererMap = Record<string, (message: any, options: { expanded?: boolean }, theme: any) => any>;
21
22
 
22
23
  function makeHandlers() {
23
24
  const handlers: HandlerMap = {};
24
25
  const commands: CommandMap = {};
26
+ const messageRenderers: MessageRendererMap = {};
25
27
  const sendMessage = vi.fn();
26
28
 
27
29
  const pi = {
@@ -29,7 +31,9 @@ function makeHandlers() {
29
31
  handlers[event] ??= [];
30
32
  handlers[event].push(handler);
31
33
  },
32
- registerMessageRenderer: () => undefined,
34
+ registerMessageRenderer: (customType: string, renderer: MessageRendererMap[string]) => {
35
+ messageRenderers[customType] = renderer;
36
+ },
33
37
  registerCommand: (name: string, command: { handler: (args: string, ctx: any) => any }) => {
34
38
  commands[name] = command;
35
39
  },
@@ -42,7 +46,7 @@ function makeHandlers() {
42
46
  },
43
47
  };
44
48
 
45
- return { handlers, commands, pi: pi as any, sendMessage };
49
+ return { handlers, commands, messageRenderers, pi: pi as any, sendMessage };
46
50
  }
47
51
 
48
52
  const ADVISOR_STATE_DIR = join(homedir(), ".pi", "agent", "pi-rogue", "advisor");
@@ -79,6 +83,7 @@ describe("advisor two-agent convergence", () => {
79
83
  let ctx: any;
80
84
  let handlers: HandlerMap;
81
85
  let commands: CommandMap;
86
+ let messageRenderers: MessageRendererMap;
82
87
  let sendMessageMock: ReturnType<typeof vi.fn>;
83
88
  let completeSimpleMock: ReturnType<typeof vi.fn>;
84
89
  let priorState: string | null = null;
@@ -93,6 +98,7 @@ describe("advisor two-agent convergence", () => {
93
98
  const setup = makeHandlers();
94
99
  handlers = setup.handlers;
95
100
  commands = setup.commands;
101
+ messageRenderers = setup.messageRenderers;
96
102
  sendMessageMock = setup.sendMessage;
97
103
 
98
104
  mkdirSync(dirname(ADVISOR_STATE_PATH), { recursive: true });
@@ -250,6 +256,92 @@ describe("advisor two-agent convergence", () => {
250
256
  );
251
257
  });
252
258
 
259
+ it("redacts transient clipboard image paths from emitted advisor handoffs", async () => {
260
+ const preflight = handlers.before_agent_start;
261
+ const turnEnd = handlers.turn_end;
262
+ const clipboardPath = "/var/folders/fm/rwczdnws5j58x7kbyn3vcx_h0000gn/T/clipboard-2026-06-04-012248-DEE3A154.png";
263
+
264
+ completeSimpleMock.mockResolvedValue({
265
+ content: [{
266
+ type: "text",
267
+ text: JSON.stringify({
268
+ verdict: "not_done",
269
+ summary: `The visible handoff should not include ${clipboardPath}`,
270
+ reason: `Expanded Ctrl+O output leaks ${clipboardPath}`,
271
+ actions: [`redact ${clipboardPath}`],
272
+ checklist: [],
273
+ notify: true,
274
+ }),
275
+ }],
276
+ });
277
+
278
+ await handlers.session_start?.[0]?.({}, ctx);
279
+ await preflight![0]({ systemPrompt: "SYS", prompt: `Continue the current goal ${clipboardPath}` }, ctx);
280
+ await turnEnd![0]({
281
+ toolResults: [{ toolName: "edit" }],
282
+ message: { role: "assistant", content: "Repo-side autoresearch is verified closed. Only optional external rollout/CI smoke remains." },
283
+ }, ctx);
284
+
285
+ expect(sendMessageMock).toHaveBeenCalled();
286
+ const sent = sendMessageMock.mock.calls[0]?.[0];
287
+ expect(JSON.stringify(sent)).not.toContain(clipboardPath);
288
+ expect(sent.content).toContain("[clipboard image]");
289
+ expect(readAdvisorState().followUp).toContain("[clipboard image]");
290
+
291
+ const theme = {
292
+ fg: (_name: string, text: string) => text,
293
+ bg: (_name: string, text: string) => text,
294
+ bold: (text: string) => text,
295
+ };
296
+ const expanded = messageRenderers["advisor:llm"](sent, { expanded: true }, theme).render(120).join("\n");
297
+ expect(expanded).toContain("full handoff:");
298
+ expect(expanded).toContain("Advisor verdict: review.");
299
+ expect(expanded).toContain("[clipboard image]");
300
+ expect(expanded).not.toContain(clipboardPath);
301
+ });
302
+
303
+ it("suppresses duplicate reason and summary in advisor handoffs", async () => {
304
+ const preflight = handlers.before_agent_start;
305
+ const turnEnd = handlers.turn_end;
306
+ const duplicate = "The agent made a safe attempt, but it did not demonstrate that the advisor post-turn review was induced.";
307
+
308
+ completeSimpleMock.mockResolvedValue({
309
+ content: [{
310
+ type: "text",
311
+ text: JSON.stringify({
312
+ verdict: "not_done",
313
+ reason: duplicate,
314
+ summary: duplicate,
315
+ actions: ["Invoke the real review hook if available."],
316
+ checklist: [],
317
+ notify: true,
318
+ }),
319
+ }],
320
+ });
321
+
322
+ await handlers.session_start?.[0]?.({}, ctx);
323
+ await preflight![0]({ systemPrompt: "SYS", prompt: "Continue the current goal" }, ctx);
324
+ await turnEnd![0]({
325
+ toolResults: [{ toolName: "edit" }],
326
+ message: { role: "assistant", content: "Repo-side autoresearch is verified closed. Only optional external rollout/CI smoke remains." },
327
+ }, ctx);
328
+
329
+ const sent = sendMessageMock.mock.calls[0]?.[0];
330
+ expect(sent.content).toContain(`Reason: ${duplicate}`);
331
+ expect(sent.content).not.toContain("Summary:");
332
+ expect(sent.details.summary).toBe("");
333
+
334
+ const theme = {
335
+ fg: (_name: string, text: string) => text,
336
+ bg: (_name: string, text: string) => text,
337
+ bold: (text: string) => text,
338
+ };
339
+ const collapsed = messageRenderers["advisor:llm"](sent, { expanded: false }, theme).render(120).join("\n");
340
+ const expanded = messageRenderers["advisor:llm"](sent, { expanded: true }, theme).render(120).join("\n");
341
+ expect(collapsed).not.toContain("summary:");
342
+ expect(expanded).not.toContain("Summary:");
343
+ });
344
+
253
345
  it("renders manual advisor answers as advisor custom messages", async () => {
254
346
  expect(commands.advisor).toBeTruthy();
255
347
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiale-plus/pi-rogue-bundle",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
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",