@fiale-plus/pi-rogue 0.2.3 → 0.3.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 (33) hide show
  1. package/README.md +17 -1
  2. package/node_modules/@fiale-plus/pi-rogue-advisor/README.md +1 -0
  3. package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate-features.test.ts +8 -0
  4. package/node_modules/@fiale-plus/pi-rogue-advisor/src/binary-gate-features.ts +7 -0
  5. package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.test.ts +26 -0
  6. package/node_modules/@fiale-plus/pi-rogue-advisor/src/router.ts +10 -1
  7. package/node_modules/@fiale-plus/pi-rogue-orchestration/README.md +3 -3
  8. package/node_modules/@fiale-plus/pi-rogue-orchestration/package.json +3 -0
  9. package/node_modules/@fiale-plus/pi-rogue-orchestration/skills/orchestration/SKILL.md +3 -2
  10. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.test.ts +65 -2
  11. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/goal.ts +84 -4
  12. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/loop.ts +3 -0
  13. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.test.ts +43 -0
  14. package/node_modules/@fiale-plus/pi-rogue-orchestration/src/novelty-guard.ts +96 -11
  15. package/node_modules/@fiale-plus/pi-rogue-router/README.md +45 -6
  16. package/node_modules/@fiale-plus/pi-rogue-router/src/binary-gate.test.ts +88 -0
  17. package/node_modules/@fiale-plus/pi-rogue-router/src/binary-gate.ts +232 -0
  18. package/node_modules/@fiale-plus/pi-rogue-router/src/cli.ts +123 -9
  19. package/node_modules/@fiale-plus/pi-rogue-router/src/completions.ts +39 -16
  20. package/node_modules/@fiale-plus/pi-rogue-router/src/config-extension.test.ts +111 -4
  21. package/node_modules/@fiale-plus/pi-rogue-router/src/config.ts +17 -2
  22. package/node_modules/@fiale-plus/pi-rogue-router/src/extension.ts +67 -7
  23. package/node_modules/@fiale-plus/pi-rogue-router/src/index.ts +4 -0
  24. package/node_modules/@fiale-plus/pi-rogue-router/src/observe.ts +76 -5
  25. package/node_modules/@fiale-plus/pi-rogue-router/src/outcomes.ts +130 -6
  26. package/node_modules/@fiale-plus/pi-rogue-router/src/reports.test.ts +92 -0
  27. package/node_modules/@fiale-plus/pi-rogue-router/src/reports.ts +116 -0
  28. package/node_modules/@fiale-plus/pi-rogue-router/src/sharpening.test.ts +223 -0
  29. package/node_modules/@fiale-plus/pi-rogue-router/src/sharpening.ts +344 -0
  30. package/node_modules/@fiale-plus/pi-rogue-router/src/teacher-runner.test.ts +126 -0
  31. package/node_modules/@fiale-plus/pi-rogue-router/src/teacher-runner.ts +238 -0
  32. package/node_modules/@fiale-plus/pi-rogue-router/src/v1-telemetry.test.ts +54 -1
  33. package/package.json +1 -1
@@ -2,11 +2,15 @@
2
2
  import { existsSync, readdirSync, statSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
4
  import { decideRoute, readCheckpointJsonl, selectCheckpoint } from "./decision.js";
5
+ import { writeBinaryGateTraining } from "./binary-gate.js";
5
6
  import { writeSessionCheckpointsJsonl } from "./checkpoints.js";
6
7
  import { appendRouteEvent, buildRouteEvent } from "./ledger.js";
7
8
  import { writeCapabilityCards, writeShadowEval, writeTeacherPromptRequests, writeTeacherReflection } from "./learning.js";
8
9
  import { writeTrainingRows } from "./dataset.js";
9
- import { writeInferredOutcomes } from "./outcomes.js";
10
+ import { writeEnrichedOutcomes, writeInferredOutcomes } from "./outcomes.js";
11
+ import { writeRouterReport } from "./reports.js";
12
+ import { writeSharpeningHints } from "./sharpening.js";
13
+ import { runTeacherLabeling } from "./teacher-runner.js";
10
14
 
11
15
  interface Args {
12
16
  command?: string;
@@ -19,12 +23,22 @@ interface Args {
19
23
  events?: string;
20
24
  labels?: string;
21
25
  reflection?: string;
26
+ artifact?: string;
27
+ report?: string;
28
+ dataset?: string;
29
+ evalDataset?: string;
30
+ markdown?: string;
31
+ gateReport?: string;
22
32
  teacher?: string;
23
33
  teacherOutput?: string;
24
34
  teacherPrompts?: string;
35
+ requests?: string;
36
+ decisionsOutput?: string;
25
37
  outcomes?: string;
38
+ cards?: string;
26
39
  includeLocalRuleLabels?: boolean;
27
40
  workspaceDiff?: boolean;
41
+ dryRun?: boolean;
28
42
  pretty: boolean;
29
43
  }
30
44
 
@@ -35,9 +49,14 @@ function usage(): never {
35
49
  npm run router:decide -- --checkpoint-file <checkpoints.jsonl> [--checkpoint-id <id>] [--ledger <events.jsonl>] [--pretty]
36
50
  npm run router:cards -- --events <events.jsonl> --output <model-cards.jsonl> [--outcomes <outcomes.jsonl>] [--pretty]
37
51
  npm run router:outcomes -- --checkpoint-file <checkpoints.jsonl> --events <events.jsonl> --output <outcomes.jsonl> [--pretty]
52
+ npm run router:outcome-enrich -- --outcomes <outcomes.jsonl> --output <enriched-outcomes.jsonl> [--checkpoint-file <checkpoints.jsonl>] [--events <events.jsonl>] [--pretty]
38
53
  npm run router:teacher-requests -- --checkpoint-file <checkpoints.jsonl> --output <requests.jsonl> [--teacher openai-codex/gpt-5.5] [--pretty]
54
+ npm run router:teacher-label -- --requests <requests.jsonl> --teacher-output <decisions.jsonl> --labels <labels.jsonl> [--teacher openai-codex/gpt-5.5] [--dry-run] [--pretty]
39
55
  npm run router:reflect -- --checkpoint-file <checkpoints.jsonl> --labels <labels.jsonl> --reflection <reflection.md> [--teacher local-rule] [--teacher-output <decisions.jsonl>] [--teacher-prompts <requests.jsonl>] [--pretty]
40
56
  npm run router:dataset -- --checkpoint-file <checkpoints.jsonl> --output <training.jsonl> [--events <events.jsonl>] [--outcomes <outcomes.jsonl>] [--labels <labels.jsonl>] [--include-local-rule-labels] [--pretty]
57
+ npm run router:gate-train -- --dataset <training.jsonl> --eval-dataset <eval.jsonl> --artifact <gate.json> --report <gate-report.json> [--pretty]
58
+ npm run router:report -- --output <report.json> [--markdown <report.md>] [--events <events.jsonl>] [--outcomes <outcomes.jsonl>] [--dataset <training.jsonl>] [--gate-report <gate-report.json>] [--pretty]
59
+ npm run router:sharpen -- --events <events.jsonl> --output <hints.json> [--outcomes <outcomes.jsonl>] [--cards <model-cards.jsonl>] [--pretty]
41
60
  npm run router:shadow -- --checkpoint-file <checkpoints.jsonl> --output <report.json> [--ledger <events.jsonl>] [--pretty]
42
61
 
43
62
  Commands:
@@ -45,9 +64,14 @@ Commands:
45
64
  decide Emit a strict JSON route decision for a checkpoint and optionally append a route event.
46
65
  cards Generate local observed model capability cards from route events and optional outcomes.
47
66
  outcomes Infer conservative outcome skeletons that can be manually enriched.
67
+ outcome-enrich Enrich outcome records from checkpoints and route events.
48
68
  teacher-requests Generate local JSONL prompt requests for explicit teacher labeling.
69
+ teacher-label Run explicit teacher model labeling over request JSONL.
49
70
  reflect Generate command-triggered soft routing labels and a reflection artifact.
50
71
  dataset Export trainable rows for a conservative continue-vs-intervene gate.
72
+ gate-train Train/evaluate a local binary continue-vs-intervene gate artifact.
73
+ report Summarize route events, outcomes, labels, and gate eval metrics.
74
+ sharpen Generate local, provenance-backed router sharpening hints without mutating policy.
51
75
  shadow Shadow-evaluate the current rule policy over historical checkpoints.
52
76
  `);
53
77
  process.exit(2);
@@ -104,6 +128,36 @@ function parseArgs(argv: string[]): Args {
104
128
  index++;
105
129
  continue;
106
130
  }
131
+ if (arg === "--artifact" && next) {
132
+ args.artifact = next;
133
+ index++;
134
+ continue;
135
+ }
136
+ if (arg === "--report" && next) {
137
+ args.report = next;
138
+ index++;
139
+ continue;
140
+ }
141
+ if (arg === "--dataset" && next) {
142
+ args.dataset = next;
143
+ index++;
144
+ continue;
145
+ }
146
+ if (arg === "--eval-dataset" && next) {
147
+ args.evalDataset = next;
148
+ index++;
149
+ continue;
150
+ }
151
+ if (arg === "--markdown" && next) {
152
+ args.markdown = next;
153
+ index++;
154
+ continue;
155
+ }
156
+ if (arg === "--gate-report" && next) {
157
+ args.gateReport = next;
158
+ index++;
159
+ continue;
160
+ }
107
161
  if (arg === "--teacher" && next) {
108
162
  args.teacher = next;
109
163
  index++;
@@ -119,11 +173,26 @@ function parseArgs(argv: string[]): Args {
119
173
  index++;
120
174
  continue;
121
175
  }
176
+ if (arg === "--requests" && next) {
177
+ args.requests = next;
178
+ index++;
179
+ continue;
180
+ }
181
+ if (arg === "--decisions-output" && next) {
182
+ args.decisionsOutput = next;
183
+ index++;
184
+ continue;
185
+ }
122
186
  if (arg === "--outcomes" && next) {
123
187
  args.outcomes = next;
124
188
  index++;
125
189
  continue;
126
190
  }
191
+ if (arg === "--cards" && next) {
192
+ args.cards = next;
193
+ index++;
194
+ continue;
195
+ }
127
196
  if (arg === "--include-local-rule-labels") {
128
197
  args.includeLocalRuleLabels = true;
129
198
  continue;
@@ -132,6 +201,10 @@ function parseArgs(argv: string[]): Args {
132
201
  args.workspaceDiff = true;
133
202
  continue;
134
203
  }
204
+ if (arg === "--dry-run") {
205
+ args.dryRun = true;
206
+ continue;
207
+ }
135
208
  if (arg === "--pretty") {
136
209
  args.pretty = true;
137
210
  continue;
@@ -207,12 +280,28 @@ function outcomes(args: Args): unknown {
207
280
  return writeInferredOutcomes({ checkpointPath: args.checkpointFile, eventsPath: args.events, outputPath: args.output });
208
281
  }
209
282
 
283
+ function outcomeEnrich(args: Args): unknown {
284
+ if (!args.outcomes || !args.output) usage();
285
+ return writeEnrichedOutcomes({ outcomesPath: args.outcomes, outputPath: args.output, checkpointPath: args.checkpointFile, eventsPath: args.events });
286
+ }
287
+
210
288
  function teacherRequests(args: Args): unknown {
211
289
  if (!args.checkpointFile || !args.output) usage();
212
290
  const requests = writeTeacherPromptRequests(args.checkpointFile, args.output, args.teacher ?? "openai-codex/gpt-5.5");
213
291
  return { schema: "pi-router.teacher-requests-summary.v1", output: resolve(args.output), requests: requests.length, teacher: args.teacher ?? "openai-codex/gpt-5.5" };
214
292
  }
215
293
 
294
+ async function teacherLabel(args: Args): Promise<unknown> {
295
+ if (!args.requests || !args.teacherOutput || !args.labels) usage();
296
+ return runTeacherLabeling({
297
+ requestsPath: args.requests,
298
+ decisionsOutputPath: args.teacherOutput,
299
+ labelsOutputPath: args.labels,
300
+ teacher: args.teacher,
301
+ dryRun: args.dryRun,
302
+ });
303
+ }
304
+
216
305
  function reflect(args: Args): unknown {
217
306
  if (!args.checkpointFile || !args.labels || !args.reflection) usage();
218
307
  const result = writeTeacherReflection({
@@ -244,6 +333,21 @@ function dataset(args: Args): unknown {
244
333
  });
245
334
  }
246
335
 
336
+ function gateTrain(args: Args): unknown {
337
+ if (!args.dataset || !args.evalDataset || !args.artifact || !args.report) usage();
338
+ return writeBinaryGateTraining({ trainingRowsPath: args.dataset, evalRowsPath: args.evalDataset, artifactPath: args.artifact, reportPath: args.report });
339
+ }
340
+
341
+ function report(args: Args): unknown {
342
+ if (!args.output) usage();
343
+ return writeRouterReport({ outputPath: args.output, markdownPath: args.markdown, eventsPath: args.events, outcomesPath: args.outcomes, trainingRowsPath: args.dataset, gateReportPath: args.gateReport });
344
+ }
345
+
346
+ function sharpen(args: Args): unknown {
347
+ if (!args.events || !args.output) usage();
348
+ return writeSharpeningHints({ eventsPath: args.events, outputPath: args.output, outcomesPath: args.outcomes, cardsPath: args.cards });
349
+ }
350
+
247
351
  function shadow(args: Args): unknown {
248
352
  if (!args.checkpointFile || !args.output) usage();
249
353
  return writeShadowEval(args.checkpointFile, args.output, args.ledger);
@@ -259,15 +363,25 @@ async function main(): Promise<void> {
259
363
  ? cards(args)
260
364
  : args.command === "outcomes"
261
365
  ? outcomes(args)
262
- : args.command === "teacher-requests"
366
+ : args.command === "outcome-enrich"
367
+ ? outcomeEnrich(args)
368
+ : args.command === "teacher-requests"
263
369
  ? teacherRequests(args)
264
- : args.command === "reflect"
265
- ? reflect(args)
266
- : args.command === "dataset"
267
- ? dataset(args)
268
- : args.command === "shadow"
269
- ? shadow(args)
270
- : usage();
370
+ : args.command === "teacher-label"
371
+ ? await teacherLabel(args)
372
+ : args.command === "reflect"
373
+ ? reflect(args)
374
+ : args.command === "dataset"
375
+ ? dataset(args)
376
+ : args.command === "gate-train"
377
+ ? gateTrain(args)
378
+ : args.command === "report"
379
+ ? report(args)
380
+ : args.command === "sharpen"
381
+ ? sharpen(args)
382
+ : args.command === "shadow"
383
+ ? shadow(args)
384
+ : usage();
271
385
  console.log(args.pretty ? JSON.stringify(result, null, 2) : JSON.stringify(result));
272
386
  }
273
387
 
@@ -1,34 +1,57 @@
1
- import { DEFAULT_ROUTER_CONFIG, loadRouterConfig } from "./config.js";
1
+ import { DEFAULT_ROUTER_CONFIG, formatProfile, loadRouterConfig } from "./config.js";
2
2
 
3
3
  type CompletionItem = { value: string; label: string; description?: string };
4
4
 
5
- function item(value: string, description?: string): CompletionItem {
6
- return { value, label: value, ...(description ? { description } : {}) };
5
+ function item(value: string, description?: string, label = value.trimEnd()): CompletionItem {
6
+ return { value, label, ...(description ? { description } : {}) };
7
7
  }
8
8
 
9
9
  function filter(items: CompletionItem[], prefix: string): CompletionItem[] | null {
10
10
  const q = prefix.trimStart().toLowerCase();
11
- const out = q ? items.filter((entry) => entry.value.toLowerCase().startsWith(q)) : items;
11
+ const out = q ? items.filter((entry) => entry.value.toLowerCase().startsWith(q) || entry.label.toLowerCase().startsWith(q)) : items;
12
12
  return out.length ? out : null;
13
13
  }
14
14
 
15
+ function topLevelItems(): CompletionItem[] {
16
+ return [
17
+ item("status", "show current router state"),
18
+ item("help", "show command tree and safety notes"),
19
+ item("on", "enable router using the current explicit mode"),
20
+ item("off", "disable router"),
21
+ item("mode ", "choose observe or auto_model", "mode …"),
22
+ item("profile ", "choose active router profile", "profile …"),
23
+ item("print ", "choose router notification verbosity", "print …"),
24
+ item("models", "show active role → model mapping"),
25
+ item("profiles", "list all configured profiles"),
26
+ item("cycle", "cycle to the next router profile"),
27
+ item("configure", "create/show config and suggested next commands"),
28
+ ];
29
+ }
30
+
15
31
  export function routerArgumentCompletions(prefix: string, ctx?: any): CompletionItem[] | null {
16
32
  const trimmed = prefix.trimStart();
17
33
  const [cmd, rest = ""] = trimmed.split(/\s+/, 2);
18
- const top = [
19
- item("on", "enable observe-only router summaries"),
20
- item("off", "disable router summaries"),
21
- item("status", "show router state and active profile"),
22
- item("profile", "show or set active router profile"),
23
- item("profiles", "list router profiles"),
24
- item("models", "show active role to model mapping"),
25
- item("configure", "write default local config if missing"),
26
- item("cycle", "cycle to the next router profile"),
27
- ];
28
- if (!cmd || !trimmed.includes(" ")) return filter(top, trimmed);
34
+ if (!cmd || !trimmed.includes(" ")) return filter(topLevelItems(), trimmed);
35
+
29
36
  if (cmd === "profile") {
30
37
  const config = ctx ? loadRouterConfig(ctx) : DEFAULT_ROUTER_CONFIG;
31
- return filter(config.profileOrder.map((name) => item(`profile ${name}`, config.profiles[name]?.worker)), `profile ${rest}`);
38
+ return filter(config.profileOrder.map((name) => {
39
+ const marker = name === config.activeProfile ? "active" : "profile";
40
+ return item(`profile ${name}`, `${marker}: ${formatProfile(name, config.profiles[name])}`, name);
41
+ }), `profile ${rest}`);
42
+ }
43
+ if (cmd === "mode") {
44
+ return filter([
45
+ item("mode observe", "recommendations only", "observe"),
46
+ item("mode auto_model", "apply model switches only", "auto_model"),
47
+ ], `mode ${rest}`);
48
+ }
49
+ if (cmd === "print") {
50
+ return filter([
51
+ item("print mismatch_only", "notify only route/model mismatches", "mismatch_only"),
52
+ item("print all", "notify every router decision", "all"),
53
+ item("print off", "suppress observe notifications", "off"),
54
+ ], `print ${rest}`);
32
55
  }
33
56
  return null;
34
57
  }
@@ -3,10 +3,10 @@ import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { describe, expect, it } from "vitest";
5
5
  import { routerArgumentCompletions } from "./completions.js";
6
- import { activeProfile, cycleRouterProfile, ensureRouterConfig, loadRouterConfig, routerConfigPath, routerEventsPath, routerSessionDir, routerStatePath, saveRouterConfig, setRouterProfile } from "./config.js";
6
+ import { activeProfile, cycleRouterProfile, ensureRouterConfig, loadRouterConfig, routerConfigPath, routerEventsPath, routerSessionDir, routerStatePath, saveRouterConfig, setRouterMode, setRouterPrint, setRouterProfile } from "./config.js";
7
7
  import { registerRouter } from "./extension.js";
8
8
  import { decideRoute } from "./decision.js";
9
- import { observeRouterTurn, summarizeRouterDecision } from "./observe.js";
9
+ import { applyModelRouting, modelsMatch, observeRouterTurn, summarizeRouterDecision } from "./observe.js";
10
10
  import type { RouterCheckpoint } from "./types.js";
11
11
 
12
12
  function ctxMock(sessionPath?: string) {
@@ -37,7 +37,10 @@ function piMock() {
37
37
  const commands = new Map<string, any>();
38
38
  const shortcuts = new Map<string, any>();
39
39
  const handlers = new Map<string, any[]>();
40
+ const selectedModels: any[] = [];
40
41
  const pi: any = {
42
+ selectedModels,
43
+ async setModel(model: any) { selectedModels.push(model); return true; },
41
44
  registerCommand(name: string, options: any) { commands.set(name, options); },
42
45
  registerShortcut(key: string, options: any) { shortcuts.set(key, options); },
43
46
  on(name: string, handler: any) { handlers.set(name, [...(handlers.get(name) ?? []), handler]); },
@@ -91,6 +94,7 @@ describe("router config profiles", () => {
91
94
 
92
95
  expect(config.activeProfile).toBe("all-smart");
93
96
  expect(config.profileOrder).toEqual(["all-smart", "spark-smart", "local-smart"]);
97
+ expect(config.mode).toBe("observe");
94
98
  expect(activeProfile(config).worker).toBe("openai-codex/gpt-5.5");
95
99
  expect(readFileSync(routerConfigPath(ctx), "utf8")).toContain("spark-smart");
96
100
  });
@@ -102,11 +106,21 @@ describe("router config profiles", () => {
102
106
  expect(spark?.activeProfile).toBe("spark-smart");
103
107
  expect(cycleRouterProfile(spark!, 1).activeProfile).toBe("local-smart");
104
108
  expect(setRouterProfile(config, "missing")).toBeNull();
109
+ expect(setRouterMode(config, "auto")?.mode).toBe("auto_model");
110
+ expect(setRouterMode(config, "auto_model")?.mode).toBe("auto_model");
111
+ expect(setRouterMode(config, "agent-auto")).toBeNull();
112
+ expect(setRouterPrint(config, "all")?.print).toBe("all");
113
+ expect(setRouterPrint(config, "noisy")).toBeNull();
105
114
  });
106
115
 
107
- it("completes router commands and profile names", () => {
108
- expect(routerArgumentCompletions("")?.map((item) => item.value)).toEqual(expect.arrayContaining(["on", "off", "status", "profile"]));
116
+ it("completes router commands as a nested slash-menu tree", () => {
117
+ const top = routerArgumentCompletions("") ?? [];
118
+
119
+ expect(top.map((item) => item.value)).toEqual(["status", "help", "on", "off", "mode ", "profile ", "print ", "models", "profiles", "cycle", "configure"]);
120
+ expect(top.find((item) => item.value === "mode ")?.label).toBe("mode …");
109
121
  expect(routerArgumentCompletions("profile s")?.map((item) => item.value)).toEqual(["profile spark-smart"]);
122
+ expect(routerArgumentCompletions("mode a")?.map((item) => item.value)).toEqual(["mode auto_model"]);
123
+ expect(routerArgumentCompletions("print ")?.map((item) => item.value)).toEqual(["print mismatch_only", "print all", "print off"]);
110
124
  });
111
125
 
112
126
  it("keeps config repo-global while state and live events are session-scoped", async () => {
@@ -149,6 +163,18 @@ describe("router extension", () => {
149
163
  await commands.get("router").handler("profile spark-smart", ctx);
150
164
  expect(loadRouterConfig(ctx).activeProfile).toBe("spark-smart");
151
165
 
166
+ await commands.get("router").handler("mode auto_model", ctx);
167
+ expect(loadRouterConfig(ctx).mode).toBe("auto_model");
168
+ await commands.get("router").handler("print all", ctx);
169
+ expect(loadRouterConfig(ctx).print).toBe("all");
170
+ await commands.get("router").handler("help", ctx);
171
+ expect(ctx.notifications.at(-1)?.text).toContain("router command tree:");
172
+ await commands.get("router").handler("off", ctx);
173
+ await commands.get("router").handler("on", ctx);
174
+ expect(ctx.notifications.at(-1)?.text).toContain("auto_model applies model switches only");
175
+ await commands.get("router").handler("status", ctx);
176
+ expect(ctx.notifications.at(-1)?.text).toContain("model routing: auto_model");
177
+
152
178
  await shortcuts.get("ctrl+alt+p").handler(ctx);
153
179
  expect(loadRouterConfig(ctx).activeProfile).toBe("local-smart");
154
180
  });
@@ -162,4 +188,85 @@ describe("router extension", () => {
162
188
  expect(summary.text).toContain("smart(openai-codex/gpt-5.5)");
163
189
  expect(summary.text).toContain("current=gpt-5.3-codex-spark");
164
190
  });
191
+
192
+ it("auto_model applies only model switches for explicit target mismatches", async () => {
193
+ const { pi } = piMock();
194
+ const ctx = {
195
+ ...ctxMock(),
196
+ modelRegistry: {
197
+ find: (provider: string, id: string) => provider === "openai-codex" && id === "gpt-5.5" ? { provider, id } : undefined,
198
+ },
199
+ };
200
+ const config = { ...loadRouterConfig(ctx), enabled: true, mode: "auto_model" as const, activeProfile: "spark-smart" };
201
+ const item = checkpoint();
202
+ const summary = summarizeRouterDecision(item, decideRoute(item), config);
203
+
204
+ const applied = await applyModelRouting(pi, ctx, summary);
205
+
206
+ expect(applied).toMatchObject({ applied: true, fromModel: "gpt-5.3-codex-spark", toModel: "openai-codex/gpt-5.5" });
207
+ expect(pi.selectedModels).toEqual([{ provider: "openai-codex", id: "gpt-5.5" }]);
208
+
209
+ const none = await applyModelRouting(pi, ctx, { ...summary, role: "none", targetModel: undefined, match: null });
210
+ expect(none.applied).toBe(false);
211
+ expect(pi.selectedModels).toHaveLength(1);
212
+ });
213
+
214
+ it("does not treat provider-qualified target as matched when only leaf model id matches", async () => {
215
+ const { pi } = piMock();
216
+ const ctx = {
217
+ ...ctxMock(),
218
+ modelRegistry: {
219
+ find: (provider: string, id: string) => provider === "openai-codex" && id === "gpt-5.5" ? { provider, id } : undefined,
220
+ },
221
+ };
222
+ const config = { ...loadRouterConfig(ctx), enabled: true, mode: "auto_model" as const, activeProfile: "spark-smart" };
223
+ const item = checkpoint({ activeModel: "gpt-5.5", provider: "custom" });
224
+ const summary = summarizeRouterDecision(item, decideRoute(item), config);
225
+ const qualifiedWithoutProvider = summarizeRouterDecision(checkpoint({ activeModel: "custom/gpt-5.5", provider: undefined }), decideRoute(item), config);
226
+ const leafWithoutProvider = summarizeRouterDecision(checkpoint({ activeModel: "gpt-5.5", provider: undefined }), decideRoute(item), config);
227
+
228
+ expect(summary.match).toBe(false);
229
+ expect(qualifiedWithoutProvider.match).toBe(false);
230
+ expect(leafWithoutProvider.match).toBe(false);
231
+ expect(modelsMatch("zai/kimi-k2.6", "openrouter/moonshotai/kimi-k2.6", "openrouter")).toBe(false);
232
+ expect(modelsMatch("moonshotai/kimi-k2.6", "openrouter/moonshotai/kimi-k2.6", "openrouter")).toBe(true);
233
+ const applied = await applyModelRouting(pi, ctx, summary);
234
+
235
+ expect(applied.applied).toBe(true);
236
+ expect(pi.selectedModels).toEqual([{ provider: "openai-codex", id: "gpt-5.5" }]);
237
+ });
238
+
239
+ it("resolves bare slash-containing model ids from the registry", async () => {
240
+ const { pi } = piMock();
241
+ const ctx = {
242
+ ...ctxMock(),
243
+ modelRegistry: {
244
+ getAll: () => [{ provider: "openrouter", id: "moonshotai/kimi-k2.6" }],
245
+ find: (provider: string, id: string) => ({ provider, id }),
246
+ },
247
+ };
248
+
249
+ const applied = await applyModelRouting(pi, ctx, { checkpointId: "c", action: "ask_micro_hint", role: "smart", currentModel: "qwen", currentProvider: "openrouter", targetModel: "moonshotai/kimi-k2.6", match: false, confidence: 0.8, reason: "test", text: "test" });
250
+ const skipped = await applyModelRouting(pi, ctx, { checkpointId: "c", action: "ask_micro_hint", role: "smart", currentModel: "moonshotai/kimi-k2.6", currentProvider: "openrouter", targetModel: "moonshotai/kimi-k2.6", match: false, confidence: 0.8, reason: "test", text: "test" });
251
+ const duplicateProviderCtx = {
252
+ ...ctxMock(),
253
+ modelRegistry: {
254
+ getAll: () => [{ provider: "first", id: "same-model" }, { provider: "current", id: "same-model" }],
255
+ },
256
+ };
257
+ const duplicateSkipped = await applyModelRouting(pi, duplicateProviderCtx, { checkpointId: "c", action: "ask_micro_hint", role: "smart", currentModel: "same-model", currentProvider: "current", targetModel: "same-model", match: true, confidence: 0.8, reason: "test", text: "test" });
258
+
259
+ const ambiguousCtx = {
260
+ ...ctxMock(),
261
+ modelRegistry: { getAll: () => [{ provider: "first", id: "ambiguous" }, { provider: "second", id: "ambiguous" }] },
262
+ };
263
+ const ambiguous = await applyModelRouting(pi, ambiguousCtx, { checkpointId: "c", action: "ask_micro_hint", role: "smart", currentModel: "other", targetModel: "ambiguous", match: false, confidence: 0.8, reason: "test", text: "test" });
264
+
265
+ expect(applied.applied).toBe(true);
266
+ expect(skipped.applied).toBe(false);
267
+ expect(duplicateSkipped.applied).toBe(false);
268
+ expect(ambiguous.applied).toBe(false);
269
+ expect(ambiguous.reason).toContain("target model not configured");
270
+ expect(pi.selectedModels).toEqual([{ provider: "openrouter", id: "moonshotai/kimi-k2.6" }]);
271
+ });
165
272
  });
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { basename, dirname, join, resolve } from "node:path";
3
3
  import { hashText } from "./hash.js";
4
4
 
5
- export type RouterMode = "observe";
5
+ export type RouterMode = "observe" | "auto_model";
6
6
  export type RouterPrintMode = "all" | "mismatch_only" | "off";
7
7
 
8
8
  export interface RouterProfile {
@@ -124,6 +124,10 @@ function readJson<T>(path: string, fallback: T): T {
124
124
  }
125
125
  }
126
126
 
127
+ function normalizeRouterMode(value: unknown): RouterMode {
128
+ return value === "auto_model" || value === "auto" ? "auto_model" : "observe";
129
+ }
130
+
127
131
  export function normalizeRouterConfig(raw: Partial<RouterConfig> | null | undefined): RouterConfig {
128
132
  const mergedProfiles = { ...DEFAULT_ROUTER_CONFIG.profiles, ...(raw?.profiles ?? {}) };
129
133
  const profileOrder = Array.isArray(raw?.profileOrder) && raw.profileOrder.length > 0
@@ -135,7 +139,7 @@ export function normalizeRouterConfig(raw: Partial<RouterConfig> | null | undefi
135
139
  const print = raw?.print === "all" || raw?.print === "off" || raw?.print === "mismatch_only" ? raw.print : DEFAULT_ROUTER_CONFIG.print;
136
140
  return {
137
141
  enabled: Boolean(raw?.enabled ?? DEFAULT_ROUTER_CONFIG.enabled),
138
- mode: "observe",
142
+ mode: normalizeRouterMode(raw?.mode),
139
143
  print,
140
144
  activeProfile,
141
145
  profileOrder,
@@ -187,6 +191,17 @@ export function setRouterProfile(config: RouterConfig, name: string): RouterConf
187
191
  return { ...config, activeProfile: name, profileOrder: config.profileOrder.includes(name) ? config.profileOrder : [...config.profileOrder, name] };
188
192
  }
189
193
 
194
+ export function setRouterMode(config: RouterConfig, mode: string): RouterConfig | null {
195
+ if (mode === "observe") return { ...config, mode: "observe" };
196
+ if (mode === "auto" || mode === "auto_model") return { ...config, mode: "auto_model" };
197
+ return null;
198
+ }
199
+
200
+ export function setRouterPrint(config: RouterConfig, print: string): RouterConfig | null {
201
+ if (print === "all" || print === "mismatch_only" || print === "off") return { ...config, print };
202
+ return null;
203
+ }
204
+
190
205
  export function formatProfile(name: string, profile: RouterProfile): string {
191
206
  const subagents = [`explore=${profile.explore ?? profile.worker}`, `debug=${profile.debug_diagnose ?? profile.smart}`, `review=${profile.review ?? profile.reviewer}`, `verify=${profile.verify ?? profile.worker}`].join(" ");
192
207
  return `${name}: worker=${profile.worker} smart=${profile.smart} teacher=${profile.teacher} reviewer=${profile.reviewer} ${subagents}`;
@@ -8,6 +8,8 @@ import {
8
8
  routerConfigPath,
9
9
  routerEventsPath,
10
10
  saveRouterConfig,
11
+ setRouterMode,
12
+ setRouterPrint,
11
13
  setRouterProfile,
12
14
  type RouterConfig,
13
15
  } from "./config.js";
@@ -17,8 +19,8 @@ import { routerArgumentCompletions } from "./completions.js";
17
19
  function statusText(ctx: any, config: RouterConfig): string {
18
20
  const profile = activeProfile(config);
19
21
  return [
20
- `router: ${config.enabled ? "observe on" : "off"}`,
21
- `mode: ${config.mode}`,
22
+ `router: ${config.enabled ? "on" : "off"}`,
23
+ `model routing: ${config.mode === "auto_model" ? "auto_model (applies model switches only)" : "observe (recommendations only)"}`,
22
24
  `print: ${config.print}`,
23
25
  `profile: ${config.activeProfile}`,
24
26
  `worker: ${profile.worker}`,
@@ -35,11 +37,35 @@ function notifyProfile(ctx: any, config: RouterConfig, prefix = "router profile"
35
37
  ctx.ui.notify(`${prefix}: ${config.activeProfile}\nworker: ${profile.worker}\nsmart: ${profile.smart}\nteacher: ${profile.teacher}\nreviewer: ${profile.reviewer}`, "info");
36
38
  }
37
39
 
40
+ function helpText(ctx: any, config: RouterConfig): string {
41
+ return [
42
+ "router command tree:",
43
+ " /router status show current router state",
44
+ " /router help show this help",
45
+ " /router on enable router using current explicit mode",
46
+ " /router off disable router",
47
+ " /router mode observe recommendations only",
48
+ " /router mode auto_model apply model switches only",
49
+ " /router profile <name> choose active profile",
50
+ " /router print mismatch_only notify only mismatches",
51
+ " /router print all notify every router decision",
52
+ " /router print off suppress observe notifications",
53
+ " /router models show active role → model mapping",
54
+ " /router profiles list configured profiles",
55
+ " /router cycle cycle to next profile",
56
+ " /router configure create/show config",
57
+ "",
58
+ "safety: observe is recommendations only; auto_model applies model switches only, never agent/subagent/tool routing.",
59
+ "",
60
+ statusText(ctx, config),
61
+ ].join("\n");
62
+ }
63
+
38
64
  function setEnabled(ctx: any, enabled: boolean): void {
39
65
  const config = ensureRouterConfig(ctx);
40
66
  const next = { ...config, enabled };
41
67
  saveRouterConfig(ctx, next);
42
- ctx.ui.notify(enabled ? "router observe mode enabled" : "router disabled", "info");
68
+ ctx.ui.notify(enabled ? `router enabled: ${next.mode === "auto_model" ? "auto_model applies model switches only" : "observe recommendations only"}` : "router disabled", "info");
43
69
  }
44
70
 
45
71
  export function registerRouter(pi: ExtensionAPI): void {
@@ -48,7 +74,7 @@ export function registerRouter(pi: ExtensionAPI): void {
48
74
  p.__piRogueRouterRegistered = true;
49
75
 
50
76
  pi.registerCommand("router", {
51
- description: "Observe-only trajectory router. Usage: /router on|off|status|profile|profiles|models|configure",
77
+ description: "Trajectory router. Usage: /router status|help|on|off|mode|profile|print|profiles|models|configure|cycle. Default observe-only; auto_model applies model switches only.",
52
78
  getArgumentCompletions: (prefix: string, ctx?: any) => routerArgumentCompletions(prefix, ctx),
53
79
  handler: async (args, ctx) => {
54
80
  const input = String(args ?? "").trim();
@@ -65,11 +91,15 @@ export function registerRouter(pi: ExtensionAPI): void {
65
91
  }
66
92
  if (cmd === "configure" || cmd === "config") {
67
93
  const config = ensureRouterConfig(ctx);
68
- ctx.ui.notify(["router config ready", "", statusText(ctx, config)].join("\n"), "info");
94
+ ctx.ui.notify(["router config ready", "", "next: /router mode …, /router profile …, /router print …", "", statusText(ctx, config)].join("\n"), "info");
69
95
  return;
70
96
  }
71
97
 
72
98
  const config = ensureRouterConfig(ctx);
99
+ if (cmd === "help") {
100
+ ctx.ui.notify(helpText(ctx, config), "info");
101
+ return;
102
+ }
73
103
  if (cmd === "status" || cmd === "show") {
74
104
  ctx.ui.notify(statusText(ctx, config), "info");
75
105
  return;
@@ -78,6 +108,36 @@ export function registerRouter(pi: ExtensionAPI): void {
78
108
  notifyProfile(ctx, config, "router models");
79
109
  return;
80
110
  }
111
+ if (cmd === "mode") {
112
+ const mode = rest[0];
113
+ if (!mode) {
114
+ ctx.ui.notify(statusText(ctx, config), "info");
115
+ return;
116
+ }
117
+ const next = setRouterMode(config, mode);
118
+ if (!next) {
119
+ ctx.ui.notify("unknown router mode: use observe or auto_model", "error");
120
+ return;
121
+ }
122
+ saveRouterConfig(ctx, next);
123
+ ctx.ui.notify(`router model routing mode set: ${next.mode === "auto_model" ? "auto_model (model switches only)" : "observe (recommendations only)"}`, "info");
124
+ return;
125
+ }
126
+ if (cmd === "print") {
127
+ const print = rest[0];
128
+ if (!print) {
129
+ ctx.ui.notify(statusText(ctx, config), "info");
130
+ return;
131
+ }
132
+ const next = setRouterPrint(config, print);
133
+ if (!next) {
134
+ ctx.ui.notify("unknown router print mode: use mismatch_only, all, or off", "error");
135
+ return;
136
+ }
137
+ saveRouterConfig(ctx, next);
138
+ ctx.ui.notify(`router print mode set: ${next.print}`, "info");
139
+ return;
140
+ }
81
141
  if (cmd === "profiles") {
82
142
  ctx.ui.notify(config.profileOrder.map((name) => {
83
143
  const marker = name === config.activeProfile ? "*" : " ";
@@ -108,7 +168,7 @@ export function registerRouter(pi: ExtensionAPI): void {
108
168
  return;
109
169
  }
110
170
 
111
- ctx.ui.notify("Usage: /router on|off|status|profile [name]|profiles|models|configure|cycle", "error");
171
+ ctx.ui.notify("Usage: /router status|help|on|off|mode [observe|auto_model]|profile [name]|print [mismatch_only|all|off]|profiles|models|configure|cycle", "error");
112
172
  },
113
173
  });
114
174
 
@@ -127,7 +187,7 @@ export function registerRouter(pi: ExtensionAPI): void {
127
187
 
128
188
  pi.on("turn_end", async (_event: any, ctx: any) => {
129
189
  try {
130
- await observeRouterTurn(ctx);
190
+ await observeRouterTurn(ctx, pi);
131
191
  } catch (error) {
132
192
  ctx.ui?.notify?.(`router observe failed: ${error instanceof Error ? error.message : String(error)}`, "warning");
133
193
  }
@@ -1,3 +1,4 @@
1
+ export * from "./binary-gate.js";
1
2
  export * from "./checkpoints.js";
2
3
  export * from "./completions.js";
3
4
  export * from "./config.js";
@@ -10,6 +11,9 @@ export * from "./ledger.js";
10
11
  export * from "./observe.js";
11
12
  export * from "./outcomes.js";
12
13
  export * from "./progress.js";
14
+ export * from "./reports.js";
15
+ export * from "./sharpening.js";
13
16
  export * from "./session-reader.js";
14
17
  export * from "./subagents.js";
18
+ export * from "./teacher-runner.js";
15
19
  export * from "./types.js";