@fiale-plus/pi-rogue-bundle 0.1.10 → 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 =
|
|
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
|
|
|
@@ -355,10 +361,11 @@ function recoverReviewControl(state: SessionState): void {
|
|
|
355
361
|
}
|
|
356
362
|
|
|
357
363
|
type AdvisorHintDetails = {
|
|
364
|
+
kind?: "handoff" | "answer";
|
|
358
365
|
decision?: "continue" | "review" | "defer";
|
|
359
366
|
reason?: string;
|
|
360
367
|
summary?: string;
|
|
361
|
-
actions?:
|
|
368
|
+
actions?: unknown;
|
|
362
369
|
};
|
|
363
370
|
|
|
364
371
|
type ReviewControlState = {
|
|
@@ -379,37 +386,117 @@ type ReviewMaterialMeta = {
|
|
|
379
386
|
isAgentEnd: boolean;
|
|
380
387
|
materialSignals?: string[];
|
|
381
388
|
};
|
|
382
|
-
|
|
389
|
+
|
|
390
|
+
function normalizeAdvisorActions(actions: unknown): string[] {
|
|
391
|
+
const raw = Array.isArray(actions) ? actions : typeof actions === "string" ? [actions] : [];
|
|
392
|
+
return raw.map((action) => squish(action, 200)).filter(Boolean).slice(0, 2);
|
|
393
|
+
}
|
|
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
|
+
|
|
419
|
+
function advisorHandoffText(decision: "continue" | "review" | "defer", reason: string, summary: string, actions: unknown = []): string {
|
|
420
|
+
const limitedActions = normalizeAdvisorActions(actions);
|
|
421
|
+
const cleanReason = sanitizeAdvisorText(reason);
|
|
422
|
+
const cleanSummary = distinctAdvisorSummary(cleanReason, summary);
|
|
423
|
+
return [
|
|
424
|
+
`Advisor verdict: ${decision}.`,
|
|
425
|
+
cleanReason ? `Reason: ${cleanReason}` : "",
|
|
426
|
+
cleanSummary ? `Summary: ${cleanSummary}` : "",
|
|
427
|
+
limitedActions.length ? `Actions: ${limitedActions.join("; ")}` : "",
|
|
428
|
+
].filter(Boolean).join("\n");
|
|
429
|
+
}
|
|
430
|
+
|
|
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);
|
|
434
|
+
const limitedActions = normalizeAdvisorActions(actions);
|
|
383
435
|
pi.sendMessage(
|
|
384
436
|
{
|
|
385
437
|
customType: "advisor:llm",
|
|
386
|
-
content:
|
|
438
|
+
content: advisorHandoffText(decision, cleanReason, cleanSummary, limitedActions),
|
|
387
439
|
display: true,
|
|
388
|
-
details: { decision, reason, summary, actions:
|
|
440
|
+
details: { kind: "handoff", decision, reason: cleanReason, summary: cleanSummary, actions: limitedActions },
|
|
389
441
|
},
|
|
390
442
|
{ deliverAs: "followUp" },
|
|
391
443
|
);
|
|
392
444
|
}
|
|
393
445
|
|
|
446
|
+
function sendAdvisorAnswer(pi: ExtensionAPI, text: string) {
|
|
447
|
+
const cleanText = sanitizeAdvisorText(text);
|
|
448
|
+
pi.sendMessage({
|
|
449
|
+
customType: "advisor:llm",
|
|
450
|
+
content: cleanText,
|
|
451
|
+
display: true,
|
|
452
|
+
details: { kind: "answer", summary: cleanText },
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
394
456
|
function renderAdvisorHint(message: any, options: { expanded?: boolean }, theme: any) {
|
|
395
457
|
const details = (message?.details ?? {}) as AdvisorHintDetails;
|
|
396
458
|
const customType = String(message?.customType ?? "advisor:rules");
|
|
397
|
-
const decision = details.decision ?? "defer";
|
|
398
459
|
const sourceColor = customType === "advisor:llm" ? "success" : customType === "advisor:model" ? "accent" : "muted";
|
|
399
|
-
const decisionColor = decision === "review" ? "accent" : decision === "continue" ? "muted" : "dim";
|
|
400
460
|
const source = theme.bold(theme.fg(sourceColor, `[${customType}]`));
|
|
461
|
+
|
|
462
|
+
if (details.kind === "answer") {
|
|
463
|
+
const body = sanitizeAdvisorText(contentText(message?.content) || details.summary || "No advisor response.");
|
|
464
|
+
const box = new Box(1, 1, (s: string) => theme.bg("customMessageBg", s));
|
|
465
|
+
box.addChild(new Text(`${theme.bold(theme.fg("success", "↗"))} ${source} ${theme.bold(theme.fg("success", "answer"))}`, 0, 0));
|
|
466
|
+
box.addChild(new Text(theme.fg("dim", body), 0, 0));
|
|
467
|
+
return box;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const decision = details.decision ?? "defer";
|
|
471
|
+
const decisionColor = decision === "review" ? "accent" : decision === "continue" ? "muted" : "dim";
|
|
401
472
|
const verdict = theme.bold(theme.fg(decisionColor, decision));
|
|
402
473
|
const glyph = decision === "review" ? "↗" : decision === "defer" ? "…" : "·";
|
|
403
474
|
const reason = squish(details.reason || contentText(message?.content) || "no extra detail", 180);
|
|
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
|
+
);
|
|
404
481
|
|
|
405
482
|
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}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
483
|
+
box.addChild(new Text(`${theme.bold(theme.fg(decisionColor, glyph))} ${source} ${verdict}`, 0, 0));
|
|
484
|
+
box.addChild(new Text(theme.fg("dim", `reason: ${reason}`), 0, 0));
|
|
485
|
+
|
|
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
|
+
}
|
|
413
500
|
}
|
|
414
501
|
|
|
415
502
|
return box;
|
|
@@ -417,15 +504,15 @@ function renderAdvisorHint(message: any, options: { expanded?: boolean }, theme:
|
|
|
417
504
|
|
|
418
505
|
/** Extract readable text from message content (handles strings, blocks, and nested message payloads). */
|
|
419
506
|
export function contentText(content: unknown): string {
|
|
420
|
-
if (typeof content === "string") return content.trim();
|
|
507
|
+
if (typeof content === "string") return sanitizeAdvisorText(content).trim();
|
|
421
508
|
if (content && typeof content === "object" && !Array.isArray(content)) {
|
|
422
509
|
const obj = content as Record<string, unknown>;
|
|
423
|
-
if (typeof obj.text === "string") return obj.text.trim();
|
|
510
|
+
if (typeof obj.text === "string") return sanitizeAdvisorText(obj.text).trim();
|
|
424
511
|
if (obj.content !== undefined) return contentText(obj.content);
|
|
425
512
|
if (obj.message !== undefined) return contentText(obj.message);
|
|
426
513
|
return "";
|
|
427
514
|
}
|
|
428
|
-
if (!Array.isArray(content)) return
|
|
515
|
+
if (!Array.isArray(content)) return sanitizeAdvisorText(content).trim();
|
|
429
516
|
const parts: string[] = [];
|
|
430
517
|
for (const item of content) {
|
|
431
518
|
if (!item) continue;
|
|
@@ -442,7 +529,7 @@ export function contentText(content: unknown): string {
|
|
|
442
529
|
if (nested) parts.push(nested);
|
|
443
530
|
}
|
|
444
531
|
}
|
|
445
|
-
return parts.join("\n").replace(/\s+/g, " ").trim();
|
|
532
|
+
return sanitizeAdvisorText(parts.join("\n")).replace(/\s+/g, " ").trim();
|
|
446
533
|
}
|
|
447
534
|
|
|
448
535
|
/** Check if a tool result or message indicates an actual execution failure */
|
|
@@ -933,14 +1020,15 @@ async function doReview(pi: ExtensionAPI, ctx: any, trigger: string, delta: stri
|
|
|
933
1020
|
: json.verdict === "not_done" ? "review"
|
|
934
1021
|
: "defer";
|
|
935
1022
|
finalDecision = decision;
|
|
936
|
-
|
|
1023
|
+
const rawReason = sanitizeAdvisorText(json.reason || json.summary || "review result");
|
|
1024
|
+
finalReason = rawReason.slice(0, 120);
|
|
937
1025
|
|
|
938
1026
|
const display = formatAdvisorDisplay("advisor:llm", decision, finalReason);
|
|
939
1027
|
writeText(CURRENT_PATH, `${display}\n`);
|
|
940
|
-
sendAdvisorHint(pi, decision,
|
|
1028
|
+
sendAdvisorHint(pi, decision, rawReason, json.summary || "", json.actions || []);
|
|
941
1029
|
|
|
942
1030
|
if (json.verdict !== "on_track") {
|
|
943
|
-
state.followUp = [json.summary, ...(json.actions
|
|
1031
|
+
state.followUp = [sanitizeAdvisorText(json.summary), ...normalizeAdvisorActions(json.actions)].filter(Boolean).join(" — ");
|
|
944
1032
|
}
|
|
945
1033
|
|
|
946
1034
|
markReviewApplied(state, signature, trigger, finalDecision, finalReason, false);
|
|
@@ -1326,7 +1414,11 @@ export function registerAdvisor(pi: ExtensionAPI): void {
|
|
|
1326
1414
|
|
|
1327
1415
|
// Anything else: treat as a question to the advisor
|
|
1328
1416
|
const r = await askAdvisor(pi, ctx, a, "slash", true);
|
|
1329
|
-
|
|
1417
|
+
if (r.error) {
|
|
1418
|
+
ctx.ui.notify(r.text, "warning");
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
sendAdvisorAnswer(pi, r.text);
|
|
1330
1422
|
},
|
|
1331
1423
|
});
|
|
1332
1424
|
}
|
|
@@ -17,9 +17,13 @@ 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 }>;
|
|
21
|
+
type MessageRendererMap = Record<string, (message: any, options: { expanded?: boolean }, theme: any) => any>;
|
|
20
22
|
|
|
21
23
|
function makeHandlers() {
|
|
22
24
|
const handlers: HandlerMap = {};
|
|
25
|
+
const commands: CommandMap = {};
|
|
26
|
+
const messageRenderers: MessageRendererMap = {};
|
|
23
27
|
const sendMessage = vi.fn();
|
|
24
28
|
|
|
25
29
|
const pi = {
|
|
@@ -27,8 +31,12 @@ function makeHandlers() {
|
|
|
27
31
|
handlers[event] ??= [];
|
|
28
32
|
handlers[event].push(handler);
|
|
29
33
|
},
|
|
30
|
-
registerMessageRenderer: () =>
|
|
31
|
-
|
|
34
|
+
registerMessageRenderer: (customType: string, renderer: MessageRendererMap[string]) => {
|
|
35
|
+
messageRenderers[customType] = renderer;
|
|
36
|
+
},
|
|
37
|
+
registerCommand: (name: string, command: { handler: (args: string, ctx: any) => any }) => {
|
|
38
|
+
commands[name] = command;
|
|
39
|
+
},
|
|
32
40
|
registerTool: vi.fn(),
|
|
33
41
|
sendMessage,
|
|
34
42
|
sendUserMessage: () => undefined,
|
|
@@ -38,12 +46,13 @@ function makeHandlers() {
|
|
|
38
46
|
},
|
|
39
47
|
};
|
|
40
48
|
|
|
41
|
-
return { handlers, pi: pi as any, sendMessage };
|
|
49
|
+
return { handlers, commands, messageRenderers, pi: pi as any, sendMessage };
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
const ADVISOR_STATE_DIR = join(homedir(), ".pi", "agent", "pi-rogue", "advisor");
|
|
45
53
|
const ADVISOR_STATE_PATH = join(ADVISOR_STATE_DIR, "state.json");
|
|
46
54
|
const ADVISOR_CONFIG_PATH = join(ADVISOR_STATE_DIR, "config.json");
|
|
55
|
+
const ADVISOR_CACHE_PATH = join(ADVISOR_STATE_DIR, "cache.json");
|
|
47
56
|
|
|
48
57
|
function readAdvisorState(): any {
|
|
49
58
|
return JSON.parse(readFileSync(ADVISOR_STATE_PATH, "utf8"));
|
|
@@ -73,21 +82,28 @@ function mkCtx() {
|
|
|
73
82
|
describe("advisor two-agent convergence", () => {
|
|
74
83
|
let ctx: any;
|
|
75
84
|
let handlers: HandlerMap;
|
|
85
|
+
let commands: CommandMap;
|
|
86
|
+
let messageRenderers: MessageRendererMap;
|
|
76
87
|
let sendMessageMock: ReturnType<typeof vi.fn>;
|
|
77
88
|
let completeSimpleMock: ReturnType<typeof vi.fn>;
|
|
78
89
|
let priorState: string | null = null;
|
|
79
90
|
let priorConfig: string | null = null;
|
|
91
|
+
let priorCache: string | null = null;
|
|
80
92
|
|
|
81
93
|
beforeEach(() => {
|
|
82
94
|
priorState = existsSync(ADVISOR_STATE_PATH) ? readFileSync(ADVISOR_STATE_PATH, "utf8") : null;
|
|
83
95
|
priorConfig = existsSync(ADVISOR_CONFIG_PATH) ? readFileSync(ADVISOR_CONFIG_PATH, "utf8") : null;
|
|
96
|
+
priorCache = existsSync(ADVISOR_CACHE_PATH) ? readFileSync(ADVISOR_CACHE_PATH, "utf8") : null;
|
|
84
97
|
|
|
85
98
|
const setup = makeHandlers();
|
|
86
99
|
handlers = setup.handlers;
|
|
100
|
+
commands = setup.commands;
|
|
101
|
+
messageRenderers = setup.messageRenderers;
|
|
87
102
|
sendMessageMock = setup.sendMessage;
|
|
88
103
|
|
|
89
104
|
mkdirSync(dirname(ADVISOR_STATE_PATH), { recursive: true });
|
|
90
105
|
writeFileSync(ADVISOR_CONFIG_PATH, JSON.stringify({ mode: "auto", review: "light", checkins: "off", checkinIntervalMinutes: 30 }, null, 2), "utf8");
|
|
106
|
+
writeFileSync(ADVISOR_CACHE_PATH, "{}", "utf8");
|
|
91
107
|
writeFileSync(ADVISOR_STATE_PATH, JSON.stringify({
|
|
92
108
|
turns: 0,
|
|
93
109
|
lastTask: "",
|
|
@@ -136,6 +152,12 @@ describe("advisor two-agent convergence", () => {
|
|
|
136
152
|
} else {
|
|
137
153
|
writeFileSync(ADVISOR_CONFIG_PATH, priorConfig, "utf8");
|
|
138
154
|
}
|
|
155
|
+
|
|
156
|
+
if (priorCache === null) {
|
|
157
|
+
writeFileSync(ADVISOR_CACHE_PATH, "{}", "utf8");
|
|
158
|
+
} else {
|
|
159
|
+
writeFileSync(ADVISOR_CACHE_PATH, priorCache, "utf8");
|
|
160
|
+
}
|
|
139
161
|
});
|
|
140
162
|
|
|
141
163
|
it("does not re-run advisory review on repeated material snapshots", async () => {
|
|
@@ -160,6 +182,17 @@ describe("advisor two-agent convergence", () => {
|
|
|
160
182
|
const firstState = readAdvisorState();
|
|
161
183
|
expect(firstState.reviewControl.lastDecision).toBe("review");
|
|
162
184
|
expect(firstState.followUp).toContain("Closeout is incomplete");
|
|
185
|
+
expect(sendMessageMock).toHaveBeenCalledWith(
|
|
186
|
+
expect.objectContaining({
|
|
187
|
+
customType: "advisor:llm",
|
|
188
|
+
content: expect.stringContaining("Summary: Closeout is incomplete"),
|
|
189
|
+
}),
|
|
190
|
+
expect.anything(),
|
|
191
|
+
);
|
|
192
|
+
expect(sendMessageMock).toHaveBeenCalledWith(
|
|
193
|
+
expect.objectContaining({ content: expect.stringContaining("Actions: run focused check") }),
|
|
194
|
+
expect.anything(),
|
|
195
|
+
);
|
|
163
196
|
expect(completeSimpleMock).toHaveBeenCalledTimes(1);
|
|
164
197
|
|
|
165
198
|
const consumedPrompt = await preflight;
|
|
@@ -183,6 +216,157 @@ describe("advisor two-agent convergence", () => {
|
|
|
183
216
|
expect(String(withoutFollowUp?.systemPrompt)).not.toContain("Advisor follow-up");
|
|
184
217
|
});
|
|
185
218
|
|
|
219
|
+
it("normalizes string actions in advisor handoffs", async () => {
|
|
220
|
+
const preflight = handlers.before_agent_start;
|
|
221
|
+
const turnEnd = handlers.turn_end;
|
|
222
|
+
expect(preflight?.length).toBe(1);
|
|
223
|
+
expect(turnEnd?.length).toBe(1);
|
|
224
|
+
|
|
225
|
+
completeSimpleMock.mockResolvedValue({
|
|
226
|
+
content: [{
|
|
227
|
+
type: "text",
|
|
228
|
+
text: JSON.stringify({
|
|
229
|
+
verdict: "not_done",
|
|
230
|
+
summary: "Closeout is incomplete",
|
|
231
|
+
reason: "Verification is missing",
|
|
232
|
+
actions: "run focused check",
|
|
233
|
+
checklist: [],
|
|
234
|
+
notify: true,
|
|
235
|
+
}),
|
|
236
|
+
}],
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
await handlers.session_start?.[0]?.({}, ctx);
|
|
240
|
+
await preflight;
|
|
241
|
+
await turnEnd;
|
|
245
|
+
|
|
246
|
+
const state = readAdvisorState();
|
|
247
|
+
expect(completeSimpleMock).toHaveBeenCalledTimes(1);
|
|
248
|
+
expect(state.followUp).toBe("Closeout is incomplete — run focused check");
|
|
249
|
+
expect(sendMessageMock).toHaveBeenCalledWith(
|
|
250
|
+
expect.objectContaining({
|
|
251
|
+
customType: "advisor:llm",
|
|
252
|
+
content: expect.stringContaining("Actions: run focused check"),
|
|
253
|
+
details: expect.objectContaining({ actions: ["run focused check"] }),
|
|
254
|
+
}),
|
|
255
|
+
expect.anything(),
|
|
256
|
+
);
|
|
257
|
+
});
|
|
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;
|
|
280
|
+
await turnEnd;
|
|
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;
|
|
324
|
+
await turnEnd;
|
|
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
|
+
|
|
345
|
+
it("renders manual advisor answers as advisor custom messages", async () => {
|
|
346
|
+
expect(commands.advisor).toBeTruthy();
|
|
347
|
+
|
|
348
|
+
completeSimpleMock.mockResolvedValue({
|
|
349
|
+
content: [{
|
|
350
|
+
type: "text",
|
|
351
|
+
text: "Post-turn review: no merge blockers identified from the session brief.",
|
|
352
|
+
}],
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
await commands.advisor.handler("should we merge this pr?", ctx);
|
|
356
|
+
|
|
357
|
+
expect(sendMessageMock).toHaveBeenCalledWith(
|
|
358
|
+
expect.objectContaining({
|
|
359
|
+
customType: "advisor:llm",
|
|
360
|
+
content: "Post-turn review: no merge blockers identified from the session brief.",
|
|
361
|
+
display: true,
|
|
362
|
+
details: expect.objectContaining({
|
|
363
|
+
kind: "answer",
|
|
364
|
+
summary: "Post-turn review: no merge blockers identified from the session brief.",
|
|
365
|
+
}),
|
|
366
|
+
}),
|
|
367
|
+
);
|
|
368
|
+
});
|
|
369
|
+
|
|
186
370
|
it("does not re-run advisory review on repeated agent-end material snapshots", async () => {
|
|
187
371
|
const preflight = handlers.before_agent_start;
|
|
188
372
|
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.
|
|
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",
|