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

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 (39) hide show
  1. package/README.md +24 -13
  2. package/node_modules/@fiale-plus/pi-rogue-advisor/README.md +59 -0
  3. package/node_modules/@fiale-plus/pi-rogue-advisor/advisor/index.ts +1 -0
  4. package/node_modules/@fiale-plus/pi-rogue-advisor/assets/binary-gate-model.json +24026 -0
  5. package/node_modules/@fiale-plus/pi-rogue-advisor/package.json +50 -0
  6. package/node_modules/@fiale-plus/pi-rogue-advisor/skills/advisor/SKILL.md +51 -0
  7. package/node_modules/@fiale-plus/pi-rogue-advisor/src/completions.test.ts +28 -0
  8. package/node_modules/@fiale-plus/pi-rogue-advisor/src/completions.ts +79 -0
  9. package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.test.ts +257 -0
  10. package/node_modules/@fiale-plus/pi-rogue-advisor/src/extension.ts +1334 -0
  11. package/node_modules/@fiale-plus/pi-rogue-advisor/src/index.ts +3 -0
  12. package/node_modules/@fiale-plus/pi-rogue-advisor/src/internal.ts +48 -0
  13. package/node_modules/@fiale-plus/pi-rogue-advisor/src/loop-convergence.test.ts +301 -0
  14. package/node_modules/@fiale-plus/pi-rogue-advisor/src/preflight-signals.test.ts +22 -0
  15. package/node_modules/@fiale-plus/pi-rogue-advisor/src/preflight-signals.ts +21 -0
  16. package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.test.ts +78 -0
  17. package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.ts +516 -0
  18. package/node_modules/@fiale-plus/pi-rogue-orchestration/README.md +56 -0
  19. package/node_modules/@fiale-plus/pi-rogue-orchestration/orchestration/index.ts +1 -0
  20. package/node_modules/@fiale-plus/pi-rogue-orchestration/package.json +44 -0
  21. package/node_modules/@fiale-plus/pi-rogue-orchestration/skills/orchestration/SKILL.md +44 -0
  22. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.test.ts +142 -0
  23. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/advisor-checkins.ts +96 -0
  24. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch-state.ts +70 -0
  25. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch.test.ts +143 -0
  26. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/autoresearch.ts +139 -0
  27. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/completions.test.ts +23 -0
  28. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/completions.ts +53 -0
  29. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/extension.ts +23 -0
  30. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal-resolution.ts +36 -0
  31. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.test.ts +182 -0
  32. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.ts +232 -0
  33. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/index.ts +1 -0
  34. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/internal.ts +98 -0
  35. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/loop.ts +274 -0
  36. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.test.ts +35 -0
  37. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.ts +145 -0
  38. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/state.ts +24 -0
  39. package/package.json +10 -2
@@ -0,0 +1,3 @@
1
+ export { default, registerAdvisor } from "./extension.js";
2
+ export { requestAdvisorLoopCheckin } from "./extension.js";
3
+ export * from "./router.js";
@@ -0,0 +1,48 @@
1
+ import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { homedir } from "node:os";
4
+
5
+ const ROOT_DIR = join(homedir(), ".pi", "agent", "pi-rogue");
6
+
7
+ export function appDir(): string {
8
+ mkdirSync(ROOT_DIR, { recursive: true });
9
+ return ROOT_DIR;
10
+ }
11
+
12
+ export function featureDir(feature: string): string {
13
+ const dir = join(appDir(), feature);
14
+ mkdirSync(dir, { recursive: true });
15
+ return dir;
16
+ }
17
+
18
+ export function featureFile(feature: string, filename: string): string {
19
+ return join(featureDir(feature), filename);
20
+ }
21
+
22
+ export function truncate(text: string, max: number): string {
23
+ if (text.length <= max) return text;
24
+ return `${text.slice(0, Math.max(0, max - 1))}…`;
25
+ }
26
+
27
+ export function readText(filePath: string, fallback = ""): string {
28
+ try {
29
+ return readFileSync(filePath, "utf8");
30
+ } catch {
31
+ return fallback;
32
+ }
33
+ }
34
+
35
+ function ensureParent(filePath: string): string {
36
+ mkdirSync(dirname(filePath), { recursive: true });
37
+ return filePath;
38
+ }
39
+
40
+ export function writeText(filePath: string, text: string): void {
41
+ ensureParent(filePath);
42
+ writeFileSync(filePath, text, "utf8");
43
+ }
44
+
45
+ export function appendText(filePath: string, text: string): void {
46
+ ensureParent(filePath);
47
+ appendFileSync(filePath, text, "utf8");
48
+ }
@@ -0,0 +1,301 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { completeSimple } from "@earendil-works/pi-ai";
7
+ import { registerAdvisor } from "./extension.js";
8
+
9
+ vi.mock("@earendil-works/pi-ai", async () => {
10
+ const actual = await vi.importActual<typeof import("@earendil-works/pi-ai")>("@earendil-works/pi-ai");
11
+ return {
12
+ ...actual,
13
+ completeSimple: vi.fn(),
14
+ };
15
+ });
16
+
17
+ type Handler = (event: any, ctx: any) => any;
18
+
19
+ type HandlerMap = Record<string, Handler[]>;
20
+
21
+ function makeHandlers() {
22
+ const handlers: HandlerMap = {};
23
+ const sendMessage = vi.fn();
24
+
25
+ const pi = {
26
+ on: (event: string, handler: Handler) => {
27
+ handlers[event] ??= [];
28
+ handlers[event].push(handler);
29
+ },
30
+ registerMessageRenderer: () => undefined,
31
+ registerCommand: () => undefined,
32
+ registerTool: vi.fn(),
33
+ sendMessage,
34
+ sendUserMessage: () => undefined,
35
+ ui: {
36
+ setStatus: () => undefined,
37
+ notify: () => undefined,
38
+ },
39
+ };
40
+
41
+ return { handlers, pi: pi as any, sendMessage };
42
+ }
43
+
44
+ const ADVISOR_STATE_DIR = join(homedir(), ".pi", "agent", "pi-rogue", "advisor");
45
+ const ADVISOR_STATE_PATH = join(ADVISOR_STATE_DIR, "state.json");
46
+ const ADVISOR_CONFIG_PATH = join(ADVISOR_STATE_DIR, "config.json");
47
+
48
+ function readAdvisorState(): any {
49
+ return JSON.parse(readFileSync(ADVISOR_STATE_PATH, "utf8"));
50
+ }
51
+
52
+ function mkCtx() {
53
+ return {
54
+ sessionManager: {
55
+ getSessionFile: () => join(homedir(), ".pi", "agent", "pi-rogue", "advisor", "session.jsonl"),
56
+ },
57
+ isIdle: () => true,
58
+ modelRegistry: {
59
+ find: (provider: string, model: string) => {
60
+ if (provider === "openai-codex") return { id: `${provider}/${model}`, provider, input: ["text"] };
61
+ return null;
62
+ },
63
+ getAvailable: () => [{ id: "provider/text-light", provider: "provider", input: ["text"] }],
64
+ getApiKeyAndHeaders: async () => ({ ok: true, apiKey: "k", headers: {} }),
65
+ },
66
+ ui: {
67
+ setStatus: () => undefined,
68
+ notify: () => undefined,
69
+ },
70
+ } as any;
71
+ }
72
+
73
+ describe("advisor two-agent convergence", () => {
74
+ let ctx: any;
75
+ let handlers: HandlerMap;
76
+ let sendMessageMock: ReturnType<typeof vi.fn>;
77
+ let completeSimpleMock: ReturnType<typeof vi.fn>;
78
+ let priorState: string | null = null;
79
+ let priorConfig: string | null = null;
80
+
81
+ beforeEach(() => {
82
+ priorState = existsSync(ADVISOR_STATE_PATH) ? readFileSync(ADVISOR_STATE_PATH, "utf8") : null;
83
+ priorConfig = existsSync(ADVISOR_CONFIG_PATH) ? readFileSync(ADVISOR_CONFIG_PATH, "utf8") : null;
84
+
85
+ const setup = makeHandlers();
86
+ handlers = setup.handlers;
87
+ sendMessageMock = setup.sendMessage;
88
+
89
+ mkdirSync(dirname(ADVISOR_STATE_PATH), { recursive: true });
90
+ writeFileSync(ADVISOR_CONFIG_PATH, JSON.stringify({ mode: "auto", review: "light", checkins: "off", checkinIntervalMinutes: 30 }, null, 2), "utf8");
91
+ writeFileSync(ADVISOR_STATE_PATH, JSON.stringify({
92
+ turns: 0,
93
+ lastTask: "",
94
+ notes: [],
95
+ files: [],
96
+ errors: [],
97
+ advisorCalls: 0,
98
+ cacheHits: 0,
99
+ followUp: "",
100
+ router: {},
101
+ checkin: { queued: false },
102
+ reviewControl: {
103
+ status: "idle",
104
+ pending: false,
105
+ consumed: true,
106
+ running: false,
107
+ },
108
+ }, null, 2), "utf8");
109
+
110
+ registerAdvisor(setup.pi);
111
+
112
+ ctx = mkCtx();
113
+ completeSimpleMock = vi.mocked(completeSimple as any);
114
+ completeSimpleMock.mockReset();
115
+
116
+ const verdict = {
117
+ verdict: "not_done",
118
+ summary: "Closeout is incomplete",
119
+ reason: "Please run one concrete check and report the result",
120
+ actions: ["run focused check"],
121
+ checklist: [],
122
+ notify: true,
123
+ };
124
+ completeSimpleMock.mockResolvedValue({ content: [{ type: "text", text: JSON.stringify(verdict) }] });
125
+ });
126
+
127
+ afterEach(() => {
128
+ if (priorState === null) {
129
+ writeFileSync(ADVISOR_STATE_PATH, "{}", "utf8");
130
+ } else {
131
+ writeFileSync(ADVISOR_STATE_PATH, priorState, "utf8");
132
+ }
133
+
134
+ if (priorConfig === null) {
135
+ writeFileSync(ADVISOR_CONFIG_PATH, "{}", "utf8");
136
+ } else {
137
+ writeFileSync(ADVISOR_CONFIG_PATH, priorConfig, "utf8");
138
+ }
139
+ });
140
+
141
+ it("does not re-run advisory review on repeated material snapshots", async () => {
142
+ const preflight = handlers.before_agent_start;
143
+ const turnEnd = handlers.turn_end;
144
+ expect(preflight?.length).toBe(1);
145
+ expect(turnEnd?.length).toBe(1);
146
+
147
+ await handlers.session_start?.[0]?.({}, ctx);
148
+
149
+ const basePrompt = "Continue the current goal";
150
+ const statusText = "Repo-side autoresearch is verified closed. Only optional external rollout/CI smoke remains.";
151
+
152
+ const firstPrompt = await preflight![0]({ systemPrompt: "SYS", prompt: basePrompt }, ctx);
153
+ expect(typeof firstPrompt).toBe("object");
154
+
155
+ await turnEnd![0]({
156
+ toolResults: [{ toolName: "edit" }],
157
+ message: { role: "assistant", content: statusText },
158
+ }, ctx);
159
+
160
+ const firstState = readAdvisorState();
161
+ expect(firstState.reviewControl.lastDecision).toBe("review");
162
+ expect(firstState.followUp).toContain("Closeout is incomplete");
163
+ expect(completeSimpleMock).toHaveBeenCalledTimes(1);
164
+
165
+ const consumedPrompt = await preflight![0]({ systemPrompt: "SYS", prompt: basePrompt }, ctx);
166
+ expect(String(consumedPrompt?.systemPrompt)).toContain("Advisor follow-up");
167
+
168
+ const consumedState = readAdvisorState();
169
+ expect(consumedState.reviewControl.status).toBe("consumed");
170
+ expect(consumedState.followUp).toBe("");
171
+
172
+ await turnEnd![0]({
173
+ toolResults: [{ toolName: "edit" }],
174
+ message: { role: "assistant", content: statusText },
175
+ }, ctx);
176
+
177
+ const secondState = readAdvisorState();
178
+ expect(completeSimpleMock).toHaveBeenCalledTimes(1);
179
+ expect(secondState.reviewControl.status).toBe("consumed");
180
+ expect(["repeated material snapshot", firstState.reviewControl.lastReason]).toContain(secondState.reviewControl.lastReason);
181
+
182
+ const withoutFollowUp = await preflight![0]({ systemPrompt: "SYS", prompt: basePrompt }, ctx);
183
+ expect(String(withoutFollowUp?.systemPrompt)).not.toContain("Advisor follow-up");
184
+ });
185
+
186
+ it("does not re-run advisory review on repeated agent-end material snapshots", async () => {
187
+ const preflight = handlers.before_agent_start;
188
+ const agentEnd = handlers.agent_end;
189
+ expect(preflight?.length).toBe(1);
190
+ expect(agentEnd?.length).toBe(1);
191
+
192
+ await handlers.session_start?.[0]?.({}, ctx);
193
+
194
+ const basePrompt = "Continue the current goal";
195
+ const statusText = "Repo-side autoresearch is verified closed. Only optional external rollout/CI smoke remains.";
196
+
197
+ const firstPrompt = await preflight![0]({ systemPrompt: "SYS", prompt: basePrompt }, ctx);
198
+ expect(typeof firstPrompt).toBe("object");
199
+
200
+ await agentEnd![0]({
201
+ messages: [
202
+ { role: "assistant", content: statusText },
203
+ { role: "toolResult", content: "edit tool changed file" },
204
+ ],
205
+ }, ctx);
206
+
207
+ const firstState = readAdvisorState();
208
+ expect(firstState.reviewControl).toBeTruthy();
209
+ const callsBeforeSecond = completeSimpleMock.mock.calls.length;
210
+
211
+ const consumedPrompt = await preflight![0]({ systemPrompt: "SYS", prompt: basePrompt }, ctx);
212
+ if (firstState.followUp) {
213
+ expect(String(consumedPrompt?.systemPrompt)).toContain("Advisor follow-up");
214
+ } else {
215
+ expect(String(consumedPrompt?.systemPrompt)).not.toContain("Advisor follow-up");
216
+ }
217
+
218
+ const consumedState = readAdvisorState();
219
+ expect(consumedState.reviewControl.status).toBe("consumed");
220
+ expect(consumedState.followUp).toBe("");
221
+
222
+ await agentEnd![0]({
223
+ messages: [
224
+ { role: "assistant", content: statusText },
225
+ { role: "toolResult", content: "edit tool changed file" },
226
+ ],
227
+ }, ctx);
228
+
229
+ const secondState = readAdvisorState();
230
+ expect(completeSimpleMock).toHaveBeenCalledTimes(callsBeforeSecond);
231
+ expect(secondState.reviewControl.status).toBe("consumed");
232
+ expect(secondState.reviewControl.lastReason).toBe("repeated material snapshot");
233
+
234
+ const withoutFollowUp = await preflight![0]({ systemPrompt: "SYS", prompt: basePrompt }, ctx);
235
+ expect(String(withoutFollowUp?.systemPrompt)).not.toContain("Advisor follow-up");
236
+ });
237
+
238
+ it("records on-track reviews silently instead of emitting repetitive continue hints", async () => {
239
+ const preflight = handlers.before_agent_start;
240
+ const turnEnd = handlers.turn_end;
241
+ expect(preflight?.length).toBe(1);
242
+ expect(turnEnd?.length).toBe(1);
243
+
244
+ completeSimpleMock.mockResolvedValue({
245
+ content: [{
246
+ type: "text",
247
+ text: JSON.stringify({
248
+ verdict: "on_track",
249
+ summary: "Implementation aligns with the requested advisor check-in behavior",
250
+ actions: [],
251
+ checklist: [],
252
+ notify: true,
253
+ }),
254
+ }],
255
+ });
256
+
257
+ await handlers.session_start?.[0]?.({}, ctx);
258
+ await preflight![0]({ systemPrompt: "SYS", prompt: "Continue the current goal" }, ctx);
259
+ await turnEnd![0]({
260
+ toolResults: [{ toolName: "edit" }],
261
+ message: { role: "assistant", content: "Secret token handling was reviewed and the safety fix is complete." },
262
+ }, ctx);
263
+
264
+ const state = readAdvisorState();
265
+ expect(completeSimpleMock).toHaveBeenCalledTimes(1);
266
+ expect(state.reviewControl.lastDecision).toBe("continue");
267
+ expect(sendMessageMock).not.toHaveBeenCalledWith(
268
+ expect.objectContaining({ customType: "advisor:llm" }),
269
+ expect.anything(),
270
+ );
271
+ });
272
+
273
+ it("recovers running review control state on session start", async () => {
274
+ const preflight = handlers.before_agent_start;
275
+ const sessionStart = handlers.session_start;
276
+ expect(sessionStart?.length).toBe(1);
277
+ expect(preflight?.length).toBe(1);
278
+
279
+ const state = readAdvisorState();
280
+ state.reviewControl = {
281
+ status: "running",
282
+ pending: true,
283
+ consumed: false,
284
+ running: true,
285
+ lastMaterialSignature: "stale",
286
+ lastTrigger: "turn-1",
287
+ };
288
+ writeFileSync(ADVISOR_STATE_PATH, JSON.stringify(state, null, 2), "utf8");
289
+
290
+ await sessionStart![0]({}, ctx);
291
+
292
+ const recovered = readAdvisorState();
293
+ expect(recovered.reviewControl.running).toBe(false);
294
+ expect(recovered.reviewControl.status).toBe("needed");
295
+ expect(recovered.reviewControl.pending).toBe(true);
296
+ expect(recovered.reviewControl.consumed).toBe(false);
297
+
298
+ const status = await preflight![0]({ systemPrompt: "SYS", prompt: "Continue the current goal" }, ctx);
299
+ expect(status?.systemPrompt).toContain("Review-control: needed");
300
+ });
301
+ });
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { classifyIntent, classifyMode } from "./preflight-signals.js";
3
+
4
+ describe("preflight signal classifiers", () => {
5
+ it("classifies planning prompts", () => {
6
+ expect(classifyIntent("what should we do next, design the architecture")).toBe("plan");
7
+ });
8
+
9
+ it("classifies implementation prompts", () => {
10
+ expect(classifyIntent("implement the auth flow and add tests")).toBe("implement");
11
+ });
12
+
13
+ it("classifies review prompts", () => {
14
+ expect(classifyIntent("please review this PR and check the diff")).toBe("review");
15
+ });
16
+
17
+ it("classifies questions vs commands", () => {
18
+ expect(classifyMode("what should we do next?")).toBe("question");
19
+ expect(classifyMode("run the tests and fix the bug")).toBe("command");
20
+ expect(classifyMode("hello there")).toBe("neutral");
21
+ });
22
+ });
@@ -0,0 +1,21 @@
1
+ /** Lightweight intent classifier for preflight signal enrichment */
2
+ export function classifyIntent(text: string): string {
3
+ const t = ` ${String(text ?? "").toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ")} `;
4
+ if (/\b(plan|design|architecture|scope|next step|strategy|proposal|should we|what should|tradeoff|decision|path forward|how to approach)\b/.test(t)) return "plan";
5
+ if (/\b(debug|bug|error|fail|broken|crash|stack|traceback|investigate|why (is|was|does|did|are) )/.test(t)) return "debug";
6
+ if (/\b(review|check |verify|validate|look at|diff|pr |pull request|feedback)\b/i.test(t)) return "review";
7
+ if (/\b(research|compare|difference|which (one|model|lib|is better)|how does|documentation|read (about|the)|what is)\b/i.test(t)) return "research";
8
+ if (/\b(implement|build|write|create|add|make|refactor|rename|extract|migrate|fix|patch)\b/i.test(t)) return "implement";
9
+ if (/\b(install|config|setup|run|build|deploy|ssh|status|stats|logs?|theme|terminal|shell|brew|npm|git)\b/i.test(t)) return "ops";
10
+ if (/\b(continue|resume|compact|summarize|after compact|move on)\b/i.test(t)) return "handoff";
11
+ return "";
12
+ }
13
+
14
+ /** Classify prompt as question/command/neutral for signal enrichment */
15
+ export function classifyMode(text: string): string {
16
+ const t = String(text ?? "").trim();
17
+ if (!t) return "";
18
+ if (/^(create|add|make|change|write|fix|update|remove|delete|run|install|set[\s-]?up|build|deploy|check|investigate|debug|review|test|refactor|merge|close|open|start|stop|restart|continue|show|list|compact|setup)\b/i.test(t)) return "command";
19
+ if (t.includes("?") || /^(what|why|how|when|where|who|which|is there|can we|should|does|did|are we|is it|do you|would you|could we)\b/i.test(t)) return "question";
20
+ return "neutral";
21
+ }
@@ -0,0 +1,78 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { formatAdvisorDisplay, heuristicRoute, routeNote, shouldQueryClassifier, summarizeRoute, type AdvisorRouteInput } from "./router.js";
3
+
4
+ describe("advisor router heuristics", () => {
5
+ it("keeps tiny edits in continue mode", () => {
6
+ const input: AdvisorRouteInput = { phase: "preflight", text: "fix a typo in README" };
7
+ const route = heuristicRoute(input);
8
+
9
+ expect(route.label).toBe("continue");
10
+ expect(route.preflight).toBe("off");
11
+ expect(route.review).toBe("off");
12
+ expect(route.escalate).toBe(false);
13
+ expect(route.safety).toBe(false);
14
+ expect(shouldQueryClassifier(route)).toBe(false);
15
+ expect(routeNote(route)).toMatch(/^\[advisor:rules: continue, reason: [a-z0-9 ,.'-]+\]$/);
16
+ });
17
+
18
+ it("escalates complex architecture work", () => {
19
+ const input: AdvisorRouteInput = { phase: "preflight", text: "need to refactor the architecture and tradeoffs" };
20
+ const route = heuristicRoute(input);
21
+
22
+ expect(route.label).toBe("escalate_to_advisor");
23
+ expect(route.preflight).toBe("full");
24
+ expect(route.review).toBe("light");
25
+ expect(route.escalate).toBe(true);
26
+ expect(summarizeRoute(route)).toContain("preflight:escalate_to_advisor");
27
+ expect(routeNote(route)).toMatch(/^\[advisor:rules: review, reason: [a-z0-9 ,.'-]+\]$/);
28
+ });
29
+
30
+ it("escalates strategy and decision prompts", () => {
31
+ const input: AdvisorRouteInput = { phase: "preflight", text: "does it make sense to buy 3x usage 2x higher sustained speed? what would you choose as a strategy" };
32
+ const route = heuristicRoute(input);
33
+
34
+ expect(route.label).toBe("escalate_to_advisor");
35
+ expect(route.escalate).toBe(true);
36
+ expect(routeNote(route)).toMatch(/^\[advisor:rules: review, reason: [a-z0-9 ,.'-]+\]$/);
37
+ });
38
+
39
+ it("flags safety-sensitive prompts", () => {
40
+ const input: AdvisorRouteInput = { phase: "preflight", text: "run rm -rf on prod" };
41
+ const route = heuristicRoute(input);
42
+
43
+ expect(route.safety).toBe(true);
44
+ expect(route.label).toBe("escalate_to_advisor");
45
+ expect(routeNote(route)).toMatch(/^\[advisor:rules: review, reason: [a-z0-9 ,.'-]+\]$/);
46
+ });
47
+
48
+ it("reviews incomplete work as not done", () => {
49
+ const input: AdvisorRouteInput = { phase: "review", text: "still incomplete, tests fail", failed: true };
50
+ const route = heuristicRoute(input);
51
+
52
+ expect(route.label).toBe("not_done");
53
+ expect(route.review).toBe("strict");
54
+ expect(route.escalate).toBe(true);
55
+ expect(routeNote(route)).toMatch(/^\[advisor:rules: review, reason: [a-z0-9 ,.'-]+\]$/);
56
+ });
57
+
58
+ it("abstains when review signal is weak", () => {
59
+ const input: AdvisorRouteInput = { phase: "review", text: "looks okay" };
60
+ const route = heuristicRoute(input);
61
+
62
+ expect(route.label).toBe("abstain");
63
+ expect(route.review).toBe("off");
64
+ expect(shouldQueryClassifier(route)).toBe(true);
65
+ expect(routeNote(route)).toMatch(/^\[advisor:rules: defer, reason: [a-z0-9 ,.'-]+\]$/);
66
+ });
67
+
68
+ it("tags model-routed notes explicitly", () => {
69
+ const input: AdvisorRouteInput = { phase: "preflight", text: "what would you choose as a strategy for this decision" };
70
+ const route = { ...heuristicRoute(input), source: "model" as const };
71
+
72
+ expect(routeNote(route)).toMatch(/^\[advisor:model: review, reason: [a-z0-9 ,.'-]+\]$/);
73
+ });
74
+
75
+ it("formats llm advisor messages with the llm tag", () => {
76
+ expect(formatAdvisorDisplay("advisor:llm", "review", "All set and reviewed")).toBe("[advisor:llm: review, reason: all set and reviewed]");
77
+ });
78
+ });