ultimate-pi 0.2.7 → 0.3.1

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 (53) hide show
  1. package/.agents/skills/harness-eval/SKILL.md +1 -1
  2. package/.agents/skills/harness-governor/SKILL.md +2 -2
  3. package/.agents/skills/harness-spec/SKILL.md +1 -1
  4. package/.pi/PACKAGING.md +3 -2
  5. package/.pi/extensions/custom-header.ts +0 -17
  6. package/.pi/extensions/pi-model-router-harness.ts +42 -0
  7. package/.pi/extensions/policy-gate.ts +18 -0
  8. package/.pi/extensions/provider-payload-sanitize.ts +66 -0
  9. package/.pi/extensions/sentrux-rules-sync.ts +0 -18
  10. package/.pi/harness/README.md +3 -2
  11. package/.pi/harness/docs/adrs/0004-defer-ci-agent-smoke.md +1 -1
  12. package/.pi/harness/docs/adrs/0006-sentrux-dual-layer.md +1 -1
  13. package/.pi/harness/docs/adrs/0009-sentrux-rules-lifecycle.md +2 -2
  14. package/.pi/harness/evals/smoke/README.md +1 -1
  15. package/.pi/harness/evolution/README.md +1 -1
  16. package/.pi/harness/evolution/chaos-drill.md +1 -1
  17. package/.pi/prompts/harness-setup.md +42 -35
  18. package/.pi/scripts/README.md +25 -9
  19. package/.pi/scripts/harness-cli-verify.sh +4 -2
  20. package/.pi/scripts/harness-seed-project-contracts.mjs +49 -0
  21. package/.pi/scripts/harness-sync-model-router.mjs +84 -0
  22. package/.pi/scripts/harness-verify.mjs +5 -3
  23. package/.pi/scripts/sentrux-rules-sync.mjs +2 -2
  24. package/.pi/scripts/vendor-sync-pi-model-router.sh +47 -0
  25. package/.pi/settings.example.json +0 -1
  26. package/.sentrux/rules.toml +1 -1
  27. package/AGENTS.md +1 -1
  28. package/CHANGELOG.md +62 -0
  29. package/README.md +1 -1
  30. package/THIRD_PARTY_NOTICES.md +8 -0
  31. package/biome.json +2 -1
  32. package/package.json +9 -10
  33. package/vendor/pi-model-router/.prettierignore +4 -0
  34. package/vendor/pi-model-router/.prettierrc +5 -0
  35. package/vendor/pi-model-router/AGENTS.md +39 -0
  36. package/vendor/pi-model-router/LICENSE +21 -0
  37. package/vendor/pi-model-router/README.md +99 -0
  38. package/vendor/pi-model-router/UPSTREAM_PIN.md +8 -0
  39. package/vendor/pi-model-router/docs/ARCHITECTURE.md +54 -0
  40. package/vendor/pi-model-router/extensions/commands.ts +720 -0
  41. package/vendor/pi-model-router/extensions/config.ts +348 -0
  42. package/vendor/pi-model-router/extensions/constants.ts +1 -0
  43. package/vendor/pi-model-router/extensions/index.ts +457 -0
  44. package/vendor/pi-model-router/extensions/provider.ts +529 -0
  45. package/vendor/pi-model-router/extensions/routing.ts +416 -0
  46. package/vendor/pi-model-router/extensions/state.ts +49 -0
  47. package/vendor/pi-model-router/extensions/types.ts +86 -0
  48. package/vendor/pi-model-router/extensions/ui.ts +130 -0
  49. package/vendor/pi-model-router/model-router.example.json +48 -0
  50. package/vendor/pi-model-router/package.json +48 -0
  51. package/vendor/pi-model-router/tsconfig.json +16 -0
  52. package/.pi/extensions/model-router-bootstrap.ts +0 -174
  53. package/.sentrux/.harness-rules-meta.json +0 -5
@@ -0,0 +1,720 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionContext,
4
+ } from '@mariozechner/pi-coding-agent';
5
+ import type { AutocompleteItem } from '@mariozechner/pi-tui';
6
+ import type {
7
+ RouterConfig,
8
+ RouterPinByProfile,
9
+ RouterThinkingByProfile,
10
+ RoutingDecision,
11
+ RouterTier,
12
+ } from './types.js';
13
+ import {
14
+ profileNames,
15
+ resolveProfileName,
16
+ THINKING_LEVELS,
17
+ ROUTER_PIN_VALUES,
18
+ ROUTER_TIERS,
19
+ parseCanonicalModelRef,
20
+ } from './config.js';
21
+ import {
22
+ formatPinSummary,
23
+ formatThinkingSummary,
24
+ formatModelRef,
25
+ formatDecision,
26
+ } from './ui.js';
27
+
28
+ export const registerCommands = (
29
+ pi: ExtensionAPI,
30
+ state: {
31
+ readonly currentConfig: RouterConfig;
32
+ routerEnabled: boolean;
33
+ selectedProfile: string;
34
+ readonly pinnedTierByProfile: RouterPinByProfile;
35
+ readonly thinkingByProfile: RouterThinkingByProfile;
36
+ readonly lastDecision: RoutingDecision | undefined;
37
+ lastNonRouterModel: string | undefined;
38
+ readonly accumulatedCost: number;
39
+ debugEnabled: boolean;
40
+ widgetEnabled: boolean;
41
+ readonly debugHistory: RoutingDecision[];
42
+ },
43
+ actions: {
44
+ persistState: () => void;
45
+ updateStatus: (ctx: ExtensionContext) => void;
46
+ reloadConfig: (
47
+ ctx?: ExtensionContext,
48
+ options?: { preserveDebug?: boolean },
49
+ ) => void;
50
+ ensureValidActiveRouterProfile: (ctx: ExtensionContext) => Promise<void>;
51
+ switchToRouterProfile: (
52
+ profileName: string,
53
+ ctx: ExtensionContext,
54
+ strict?: boolean,
55
+ ) => Promise<boolean>;
56
+ },
57
+ ) => {
58
+ const SUBCOMMAND_DETAILS = [
59
+ { name: 'status', desc: 'Show current router status' },
60
+ { name: 'profile', desc: 'Switch to a different router profile' },
61
+ { name: 'pin', desc: 'Pin routing for a profile to a specific tier' },
62
+ { name: 'thinking', desc: 'Override thinking level for a tier or profile' },
63
+ { name: 'disable', desc: 'Disable the router and restore last model' },
64
+ {
65
+ name: 'fix',
66
+ desc: 'Correct the last routing decision and pin that tier',
67
+ },
68
+ { name: 'widget', desc: 'Toggle the router status widget' },
69
+ { name: 'debug', desc: 'Toggle or clear router debug history' },
70
+ { name: 'reload', desc: 'Reload the model router configuration' },
71
+ { name: 'help', desc: 'Show usage help for subcommands' },
72
+ ];
73
+
74
+ const getSubcommandCompletions = (
75
+ prefix: string,
76
+ ): AutocompleteItem[] | null => {
77
+ const items = SUBCOMMAND_DETAILS.filter((s) =>
78
+ s.name.startsWith(prefix),
79
+ ).map((s) => ({
80
+ value: s.name,
81
+ label: s.name,
82
+ description: s.desc,
83
+ }));
84
+ return items.length > 0 ? items : null;
85
+ };
86
+
87
+ const getPinCompletions = (args: string[]): AutocompleteItem[] | null => {
88
+ // pin [profile] <tier|auto>
89
+ if (args.length <= 1) {
90
+ const token = args[0] ?? '';
91
+ const pinItems = ROUTER_PIN_VALUES.filter((value) =>
92
+ value.startsWith(token),
93
+ ).map((value) => ({
94
+ value,
95
+ label: value,
96
+ }));
97
+ const profileItems = profileNames(state.currentConfig)
98
+ .filter((name) => name.startsWith(token))
99
+ .map((name) => ({ value: name, label: `router/${name}` }));
100
+ const items = [...pinItems, ...profileItems];
101
+ return items.length > 0 ? items : null;
102
+ }
103
+
104
+ const profileToken = args[0];
105
+ if (!state.currentConfig.profiles[profileToken]) {
106
+ return null;
107
+ }
108
+ const pinPrefix = args[1] ?? '';
109
+ const items = ROUTER_PIN_VALUES.filter((value) =>
110
+ value.startsWith(pinPrefix),
111
+ ).map((value) => ({
112
+ value: `${profileToken} ${value}`,
113
+ label: `${profileToken} ${value}`,
114
+ }));
115
+ return items.length > 0 ? items : null;
116
+ };
117
+
118
+ const getThinkingCompletions = (
119
+ args: string[],
120
+ ): AutocompleteItem[] | null => {
121
+ // thinking [profile] [tier] <level|auto>
122
+ const tierValues = [...ROUTER_TIERS];
123
+ const levelValues = ['auto', ...THINKING_LEVELS];
124
+
125
+ if (args.length <= 1) {
126
+ const token = args[0] ?? '';
127
+ return [
128
+ ...levelValues
129
+ .filter((v) => v.startsWith(token))
130
+ .map((v) => ({ value: v, label: v })),
131
+ ...tierValues
132
+ .filter((v) => v.startsWith(token))
133
+ .map((v) => ({ value: v, label: v })),
134
+ ...profileNames(state.currentConfig)
135
+ .filter((name) => name.startsWith(token))
136
+ .map((name) => ({ value: name, label: `router/${name}` })),
137
+ ];
138
+ }
139
+
140
+ if (levelValues.includes(args[0])) {
141
+ return null;
142
+ }
143
+
144
+ if ((tierValues as string[]).includes(args[0])) {
145
+ const tier = args[0];
146
+ const levelPrefix = args[1] ?? '';
147
+ return levelValues
148
+ .filter((v) => v.startsWith(levelPrefix))
149
+ .map((v) => ({ value: `${tier} ${v}`, label: `${tier} ${v}` }));
150
+ }
151
+
152
+ if (state.currentConfig.profiles[args[0]]) {
153
+ const profile = args[0];
154
+ const nextPrefix = args[1] ?? '';
155
+
156
+ if (args.length === 2) {
157
+ return [
158
+ ...tierValues
159
+ .filter((v) => v.startsWith(nextPrefix))
160
+ .map((v) => ({ value: `${profile} ${v}`, label: v })),
161
+ ...levelValues
162
+ .filter((v) => v.startsWith(nextPrefix))
163
+ .map((v) => ({ value: `${profile} ${v}`, label: v })),
164
+ ];
165
+ }
166
+
167
+ if (levelValues.includes(args[1])) {
168
+ return null;
169
+ }
170
+
171
+ if ((tierValues as string[]).includes(args[1])) {
172
+ const tier = args[1];
173
+ const levelPrefix = args[2] ?? '';
174
+ return levelValues
175
+ .filter((v) => v.startsWith(levelPrefix))
176
+ .map((v) => ({ value: `${profile} ${tier} ${v}`, label: v }));
177
+ }
178
+ }
179
+
180
+ return null;
181
+ };
182
+
183
+
184
+ const handleStatus = async (args: string[], ctx: ExtensionContext) => {
185
+ if (args.length > 0) {
186
+ ctx.ui.notify('Usage: /router status (no arguments)', 'error');
187
+ return;
188
+ }
189
+ const names = profileNames(state.currentConfig).join(', ');
190
+ const lines = [
191
+ 'Model Router Status:',
192
+ `Router enabled: ${state.routerEnabled ? 'yes' : 'off'}`,
193
+ `Selected profile: ${state.selectedProfile}`,
194
+ `Selected profile pin: ${state.pinnedTierByProfile[state.selectedProfile] ?? 'auto'}`,
195
+ `Pins by profile: ${formatPinSummary(state.pinnedTierByProfile)}`,
196
+ `Thinking overrides: ${formatThinkingSummary(state.thinkingByProfile)}`,
197
+ `Widget: ${state.widgetEnabled ? 'on' : 'off'}`,
198
+ `Phase bias: ${state.currentConfig.phaseBias}`,
199
+ `Session cost: $${state.accumulatedCost.toFixed(4)}` +
200
+ (state.currentConfig.maxSessionBudget
201
+ ? ` / $${state.currentConfig.maxSessionBudget.toFixed(2)}`
202
+ : ''),
203
+ `Default profile: ${resolveProfileName(state.currentConfig, state.currentConfig.defaultProfile)}`,
204
+ `Available profiles: ${names}`,
205
+ `Last non-router model: ${formatModelRef(state.lastNonRouterModel)}`,
206
+ `Debug: ${state.debugEnabled ? 'on' : 'off'}`,
207
+ `Debug history: ${state.debugHistory.length} decisions`,
208
+ ];
209
+ if (state.lastDecision) {
210
+ lines.push(
211
+ `Last routed tier: ${state.lastDecision.tier}`,
212
+ `Last phase: ${state.lastDecision.phase}`,
213
+ `Last model: ${state.lastDecision.targetProvider}/${state.lastDecision.targetModelId} (${state.lastDecision.thinking})`,
214
+ `Reason: ${state.lastDecision.reasoning}`,
215
+ );
216
+ }
217
+ ctx.ui.notify(lines.join('\n'), 'info');
218
+ actions.updateStatus(ctx);
219
+ };
220
+
221
+ const handleProfile = async (args: string[], ctx: ExtensionContext) => {
222
+ if (args.length > 1) {
223
+ ctx.ui.notify('Usage: /router profile [name]', 'error');
224
+ return;
225
+ }
226
+ const profileName = args[0];
227
+ if (!profileName) {
228
+ ctx.ui.notify(
229
+ `Current profile: ${state.selectedProfile}. Available: ${profileNames(state.currentConfig).join(', ')}`,
230
+ 'info',
231
+ );
232
+ return;
233
+ }
234
+ const success = await actions.switchToRouterProfile(profileName, ctx);
235
+ if (success) {
236
+ ctx.ui.notify(
237
+ `Switched to router profile: ${state.selectedProfile}`,
238
+ 'info',
239
+ );
240
+ }
241
+ };
242
+
243
+ const handlePin = async (args: string[], ctx: ExtensionContext) => {
244
+ const currentProfile = state.selectedProfile;
245
+ if (args.length === 0) {
246
+ ctx.ui.notify(
247
+ [
248
+ `Profile: ${currentProfile}`,
249
+ `Pinned tier: ${state.pinnedTierByProfile[currentProfile] ?? 'auto'}`,
250
+ `Pins by profile: ${formatPinSummary(state.pinnedTierByProfile)}`,
251
+ `Usage: /router pin <high|medium|low|auto>`,
252
+ ` or: /router pin <profile> <high|medium|low|auto>`,
253
+ ].join('\n'),
254
+ 'info',
255
+ );
256
+ actions.updateStatus(ctx);
257
+ return;
258
+ }
259
+
260
+ if (args.length > 2) {
261
+ ctx.ui.notify(
262
+ 'Usage: /router pin [profile] <high|medium|low|auto>',
263
+ 'error',
264
+ );
265
+ return;
266
+ }
267
+
268
+ let profileName = currentProfile;
269
+ let pinValue = '';
270
+
271
+ if (args.length === 1) {
272
+ pinValue = args[0];
273
+ } else {
274
+ profileName = args[0];
275
+ pinValue = args[1];
276
+ }
277
+
278
+ if (!state.currentConfig.profiles[profileName]) {
279
+ // If we had two args and the first wasn't a profile, it's definitely an error
280
+ if (args.length === 2) {
281
+ ctx.ui.notify(`Unknown router profile: ${profileName}`, 'error');
282
+ return;
283
+ }
284
+ // If one arg, maybe they meant the pin value for the current profile
285
+ if (ROUTER_PIN_VALUES.includes(args[0] as any)) {
286
+ profileName = currentProfile;
287
+ pinValue = args[0];
288
+ } else {
289
+ ctx.ui.notify(`Unknown router profile: ${profileName}`, 'error');
290
+ return;
291
+ }
292
+ }
293
+
294
+ if (!ROUTER_PIN_VALUES.includes(pinValue as any)) {
295
+ ctx.ui.notify(
296
+ `Invalid router pin: ${pinValue}. Use one of: ${ROUTER_PIN_VALUES.join(', ')}`,
297
+ 'error',
298
+ );
299
+ return;
300
+ }
301
+
302
+ const nextTier = pinValue === 'auto' ? undefined : (pinValue as RouterTier);
303
+ if (nextTier) {
304
+ state.pinnedTierByProfile[profileName] = nextTier;
305
+ } else {
306
+ delete state.pinnedTierByProfile[profileName];
307
+ }
308
+ actions.persistState();
309
+ actions.updateStatus(ctx);
310
+ ctx.ui.notify(
311
+ nextTier
312
+ ? `Router profile ${profileName} pinned to ${nextTier}`
313
+ : `Router profile ${profileName} pin cleared; heuristic routing restored`,
314
+ 'info',
315
+ );
316
+ };
317
+
318
+ const handleThinking = async (args: string[], ctx: ExtensionContext) => {
319
+ const currentProfile = state.selectedProfile;
320
+ if (args.length === 0) {
321
+ ctx.ui.notify(
322
+ [
323
+ `Profile: ${currentProfile}`,
324
+ `Thinking overrides: ${JSON.stringify(state.thinkingByProfile[currentProfile] ?? {})}`,
325
+ 'Usage: /router thinking <level|auto>',
326
+ ' or: /router thinking <tier> <level|auto>',
327
+ ' or: /router thinking <profile> <tier> <level|auto>',
328
+ ].join('\n'),
329
+ 'info',
330
+ );
331
+ return;
332
+ }
333
+
334
+ if (args.length > 3) {
335
+ ctx.ui.notify('Too many arguments for /router thinking.', 'error');
336
+ return;
337
+ }
338
+
339
+ let profileName = currentProfile;
340
+ let tier: RouterTier | 'all' | undefined = undefined;
341
+ let levelValue = '';
342
+
343
+ const tierValues = ['high', 'medium', 'low'];
344
+ const levelValues = ['auto', ...THINKING_LEVELS];
345
+
346
+ if (args.length === 1) {
347
+ levelValue = args[0];
348
+ tier =
349
+ state.pinnedTierByProfile[profileName] ??
350
+ (state.lastDecision?.profile === profileName
351
+ ? state.lastDecision.tier
352
+ : 'medium');
353
+ } else if (args.length === 2) {
354
+ if (tierValues.includes(args[0]) || args[0] === 'all') {
355
+ tier = args[0] as RouterTier | 'all';
356
+ levelValue = args[1];
357
+ } else {
358
+ profileName = args[0];
359
+ levelValue = args[1];
360
+ tier =
361
+ state.pinnedTierByProfile[profileName] ??
362
+ (state.lastDecision?.profile === profileName
363
+ ? state.lastDecision.tier
364
+ : 'medium');
365
+ }
366
+ } else if (args.length === 3) {
367
+ profileName = args[0];
368
+ tier = args[1] as RouterTier | 'all';
369
+ levelValue = args[2];
370
+ }
371
+
372
+ if (!state.currentConfig.profiles[profileName]) {
373
+ ctx.ui.notify(`Unknown router profile: ${profileName}`, 'error');
374
+ return;
375
+ }
376
+ if (tier !== 'all' && !tierValues.includes(tier as string)) {
377
+ ctx.ui.notify(
378
+ `Invalid tier: ${tier}. Use high, medium, or low.`,
379
+ 'error',
380
+ );
381
+ return;
382
+ }
383
+ if (!levelValues.includes(levelValue)) {
384
+ ctx.ui.notify(
385
+ `Invalid thinking level: ${levelValue}. Use auto or: ${THINKING_LEVELS.join(', ')}`,
386
+ 'error',
387
+ );
388
+ return;
389
+ }
390
+
391
+ const nextLevel = levelValue === 'auto' ? undefined : (levelValue as any);
392
+ if (tier === 'all') {
393
+ for (const t of ROUTER_TIERS) {
394
+ if (!state.thinkingByProfile[profileName])
395
+ state.thinkingByProfile[profileName] = {};
396
+ if (nextLevel) state.thinkingByProfile[profileName]![t] = nextLevel;
397
+ else delete state.thinkingByProfile[profileName]![t];
398
+ }
399
+ } else {
400
+ if (!state.thinkingByProfile[profileName])
401
+ state.thinkingByProfile[profileName] = {};
402
+ if (nextLevel)
403
+ state.thinkingByProfile[profileName]![tier as RouterTier] = nextLevel;
404
+ else delete state.thinkingByProfile[profileName]![tier as RouterTier];
405
+ }
406
+ if (
407
+ state.thinkingByProfile[profileName] &&
408
+ Object.keys(state.thinkingByProfile[profileName]!).length === 0
409
+ ) {
410
+ delete state.thinkingByProfile[profileName];
411
+ }
412
+
413
+ actions.persistState();
414
+ actions.updateStatus(ctx);
415
+ ctx.ui.notify(
416
+ nextLevel
417
+ ? `Router profile ${profileName} thinking (${tier}) set to ${nextLevel}`
418
+ : `Router profile ${profileName} thinking (${tier}) reset to config defaults`,
419
+ 'info',
420
+ );
421
+ };
422
+
423
+ const handleDisable = async (args: string[], ctx: ExtensionContext) => {
424
+ if (args.length > 0) {
425
+ ctx.ui.notify('Usage: /router disable (no arguments)', 'error');
426
+ return;
427
+ }
428
+ if (!state.lastNonRouterModel) {
429
+ ctx.ui.notify(
430
+ 'No previous non-router model recorded. Use /model to pick a concrete model.',
431
+ 'warning',
432
+ );
433
+ return;
434
+ }
435
+ const { provider, modelId } = parseCanonicalModelRef(
436
+ state.lastNonRouterModel,
437
+ );
438
+ const targetModel = ctx.modelRegistry.find(provider, modelId);
439
+ if (!targetModel) {
440
+ ctx.ui.notify(
441
+ `Recorded non-router model is unavailable: ${state.lastNonRouterModel}`,
442
+ 'error',
443
+ );
444
+ return;
445
+ }
446
+ const success = await pi.setModel(targetModel);
447
+ if (!success) {
448
+ ctx.ui.notify(`Failed to switch to ${state.lastNonRouterModel}`, 'error');
449
+ return;
450
+ }
451
+ state.routerEnabled = false;
452
+ actions.persistState();
453
+ actions.updateStatus(ctx);
454
+ ctx.ui.notify(
455
+ `Router disabled. Restored ${state.lastNonRouterModel}`,
456
+ 'info',
457
+ );
458
+ };
459
+
460
+ const handleFix = async (args: string[], ctx: ExtensionContext) => {
461
+ if (args.length !== 1) {
462
+ ctx.ui.notify('Usage: /router fix <high|medium|low>', 'error');
463
+ return;
464
+ }
465
+ const tier = args[0]?.toLowerCase();
466
+ if (!ROUTER_TIERS.includes(tier as RouterTier)) {
467
+ ctx.ui.notify('Usage: /router fix <high|medium|low>', 'error');
468
+ return;
469
+ }
470
+ if (!state.lastDecision) {
471
+ ctx.ui.notify('No recent routing decision to fix.', 'warning');
472
+ return;
473
+ }
474
+ state.pinnedTierByProfile[state.lastDecision.profile] = tier as RouterTier;
475
+ actions.persistState();
476
+ actions.updateStatus(ctx);
477
+ ctx.ui.notify(
478
+ `Router decision corrected. ${state.lastDecision.profile} is now pinned to ${tier}.`,
479
+ 'info',
480
+ );
481
+ };
482
+
483
+ const handleWidget = async (args: string[], ctx: ExtensionContext) => {
484
+ if (args.length > 1) {
485
+ ctx.ui.notify('Usage: /router widget <on|off|toggle>', 'error');
486
+ return;
487
+ }
488
+ const cmd = args[0]?.toLowerCase();
489
+ if (cmd === 'on') state.widgetEnabled = true;
490
+ else if (cmd === 'off') state.widgetEnabled = false;
491
+ else state.widgetEnabled = !state.widgetEnabled;
492
+ actions.persistState();
493
+ actions.updateStatus(ctx);
494
+ ctx.ui.notify(
495
+ `Router widget ${state.widgetEnabled ? 'enabled' : 'disabled'}.`,
496
+ 'info',
497
+ );
498
+ };
499
+
500
+ const handleDebug = async (args: string[], ctx: ExtensionContext) => {
501
+ if (args.length > 1) {
502
+ ctx.ui.notify('Usage: /router debug <on|off|show|clear>', 'error');
503
+ return;
504
+ }
505
+ const cmd = args[0]?.toLowerCase();
506
+ if (cmd === 'on') state.debugEnabled = true;
507
+ else if (cmd === 'off') state.debugEnabled = false;
508
+ else if (cmd === 'clear') state.debugHistory.length = 0;
509
+ else if (cmd === 'show') {
510
+ if (state.debugHistory.length === 0) {
511
+ ctx.ui.notify('No recent routing decisions.', 'info');
512
+ } else {
513
+ const history = state.debugHistory
514
+ .map(
515
+ (d) =>
516
+ `[${new Date(d.timestamp).toLocaleTimeString()}] ${formatDecision(d)}`,
517
+ )
518
+ .join('\n');
519
+ ctx.ui.notify(`Recent Routing Decisions:\n${history}`, 'info');
520
+ }
521
+ return;
522
+ } else {
523
+ state.debugEnabled = !state.debugEnabled;
524
+ }
525
+ actions.persistState();
526
+ ctx.ui.notify(
527
+ `Router debug ${state.debugEnabled ? 'enabled' : 'disabled'}.`,
528
+ 'info',
529
+ );
530
+ };
531
+
532
+ const handleReload = async (args: string[], ctx: ExtensionContext) => {
533
+ if (args.length > 0) {
534
+ ctx.ui.notify('Usage: /router reload (no arguments)', 'error');
535
+ return;
536
+ }
537
+ actions.reloadConfig(ctx, { preserveDebug: true });
538
+ await actions.ensureValidActiveRouterProfile(ctx);
539
+ ctx.ui.notify(
540
+ `Router config reloaded. Profiles: ${profileNames(state.currentConfig).join(', ')}`,
541
+ 'info',
542
+ );
543
+ };
544
+
545
+ pi.registerCommand('router', {
546
+ description: 'Model router control center',
547
+ getArgumentCompletions: (prefix) => {
548
+ const trimmedLeft = prefix.trimStart();
549
+ const hasTrailingSpace = /\s$/.test(prefix);
550
+ const parts = trimmedLeft.length > 0 ? trimmedLeft.split(/\s+/) : [];
551
+
552
+ if (parts.length === 0) {
553
+ return getSubcommandCompletions('');
554
+ }
555
+
556
+ if (parts.length === 1 && !hasTrailingSpace) {
557
+ return getSubcommandCompletions(parts[0]);
558
+ }
559
+
560
+ const subcommand = parts[0];
561
+ const subArgs = parts.slice(1);
562
+ if (hasTrailingSpace && parts.length === 1) {
563
+ subArgs.push('');
564
+ }
565
+
566
+ switch (subcommand) {
567
+ case 'profile': {
568
+ const profilePrefix = subArgs[0] ?? '';
569
+ const items = profileNames(state.currentConfig)
570
+ .filter((name) => name.startsWith(profilePrefix))
571
+ .map((name) => ({
572
+ value: `profile ${name}`,
573
+ label: `router/${name}`,
574
+ description: `Switch to router profile "${name}"`,
575
+ }));
576
+ return items.length > 0 ? items : null;
577
+ }
578
+ case 'pin': {
579
+ const completions = getPinCompletions(subArgs);
580
+ return (
581
+ completions?.map((c) => ({
582
+ ...c,
583
+ value: `pin ${c.value}`,
584
+ description: `Pin profile to ${c.label}`,
585
+ })) ?? null
586
+ );
587
+ }
588
+ case 'thinking': {
589
+ const completions = getThinkingCompletions(subArgs);
590
+ return (
591
+ completions?.map((c) => ({
592
+ ...c,
593
+ value: `thinking ${c.value}`,
594
+ description: `Set thinking level to ${c.label}`,
595
+ })) ?? null
596
+ );
597
+ }
598
+ case 'fix': {
599
+ const fixPrefix = subArgs[0] ?? '';
600
+ const items = ['high', 'medium', 'low']
601
+ .filter((t) => t.startsWith(fixPrefix.toLowerCase()))
602
+ .map((t) => ({
603
+ value: `fix ${t}`,
604
+ label: t,
605
+ description: `Correct decision and pin to ${t} tier`,
606
+ }));
607
+ return items.length > 0 ? items : null;
608
+ }
609
+ case 'widget': {
610
+ const widgetPrefix = subArgs[0] ?? '';
611
+ const items = ['on', 'off', 'toggle']
612
+ .filter((v) => v.startsWith(widgetPrefix))
613
+ .map((v) => ({
614
+ value: `widget ${v}`,
615
+ label: v,
616
+ description: `Set widget to ${v}`,
617
+ }));
618
+ return items.length > 0 ? items : null;
619
+ }
620
+ case 'debug': {
621
+ const debugPrefix = subArgs[0] ?? '';
622
+ const items = ['on', 'off', 'toggle', 'clear', 'show']
623
+ .filter((v) => v.startsWith(debugPrefix))
624
+ .map((v) => ({
625
+ value: `debug ${v}`,
626
+ label: v,
627
+ description: `Router debug: ${v}`,
628
+ }));
629
+ return items.length > 0 ? items : null;
630
+ }
631
+ }
632
+
633
+ return null;
634
+ },
635
+ handler: async (args, ctx) => {
636
+ const parts = args?.trim().split(/\s+/) ?? [];
637
+ const subcommand = parts[0];
638
+ const subArgs = parts.slice(1);
639
+
640
+ switch (subcommand) {
641
+ case 'profile':
642
+ await handleProfile(subArgs, ctx);
643
+ break;
644
+ case 'pin':
645
+ await handlePin(subArgs, ctx);
646
+ break;
647
+ case 'thinking':
648
+ await handleThinking(subArgs, ctx);
649
+ break;
650
+ case 'disable':
651
+ await handleDisable(subArgs, ctx);
652
+ break;
653
+ case 'fix':
654
+ await handleFix(subArgs, ctx);
655
+ break;
656
+ case 'widget':
657
+ await handleWidget(subArgs, ctx);
658
+ break;
659
+ case 'debug':
660
+ await handleDebug(subArgs, ctx);
661
+ break;
662
+ case 'reload':
663
+ await handleReload(subArgs, ctx);
664
+ break;
665
+ case 'status':
666
+ await handleStatus(subArgs, ctx);
667
+ break;
668
+ case 'help':
669
+ case '?':
670
+ if (subArgs.length > 0) {
671
+ ctx.ui.notify('Usage: /router help (no arguments)', 'error');
672
+ return;
673
+ }
674
+ ctx.ui.notify(
675
+ [
676
+ 'Router Subcommands:',
677
+ ' status Show current status, profile, pin, cost, and last decision.',
678
+ ' profile [name] Switch to a profile (enables router if off). Lists available if no name.',
679
+ ' pin [profile] <tier|auto> Force a tier (high|medium|low) for a profile or set to auto.',
680
+ ' thinking [prof] [tier] <lv> Override thinking level for a profile/tier (off|minimal|...|xhigh|auto).',
681
+ ' disable Disable the router and restore the last used non-router model.',
682
+ ' fix <tier> Correct the last routing decision and pin that tier for the current profile.',
683
+ ' widget <on|off|toggle> Control the persistent status widget visibility.',
684
+ ' debug <on|off|show|clear> Control routing debug logging to notifications and history.',
685
+ ' reload Hot-reload the configuration JSON from .pi/model-router.json.',
686
+ ' help, ? Show this help message.',
687
+ ].join('\n'),
688
+ 'info',
689
+ );
690
+ break;
691
+ default:
692
+ if (subcommand) {
693
+ // Check if subcommand is actually a profile name (backwards compatible-ish with /router-on)
694
+ if (state.currentConfig.profiles[subcommand]) {
695
+ if (subArgs.length > 0) {
696
+ ctx.ui.notify(
697
+ `Usage: /router ${subcommand} (no extra arguments allowed)`,
698
+ 'error',
699
+ );
700
+ return;
701
+ }
702
+ await actions.switchToRouterProfile(subcommand, ctx);
703
+ ctx.ui.notify(
704
+ `Router enabled with profile: ${state.selectedProfile}`,
705
+ 'info',
706
+ );
707
+ } else {
708
+ ctx.ui.notify(
709
+ `Unknown router subcommand: ${subcommand}. Try /router help`,
710
+ 'error',
711
+ );
712
+ }
713
+ } else {
714
+ await handleStatus(subArgs, ctx);
715
+ }
716
+ break;
717
+ }
718
+ },
719
+ });
720
+ };