@openscout/protocol 0.2.92 → 0.2.94

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.
@@ -0,0 +1,752 @@
1
+ const HOUR_MS = 60 * 60_000;
2
+ const DAY_MS = 24 * HOUR_MS;
3
+
4
+ export const ORCHESTRATION_ROLE_IDS = [
5
+ "product_judgment",
6
+ "large_context_synthesis",
7
+ "adversarial_critique",
8
+ "evidence_inventory",
9
+ "implementation",
10
+ "code_review",
11
+ ] as const;
12
+
13
+ export type OrchestrationRoleId = typeof ORCHESTRATION_ROLE_IDS[number];
14
+ export type QuotaPacingStatus = "underused" | "on_track" | "ahead" | "unknown";
15
+ export type QuotaAvailability = "abundant" | "available" | "guarded" | "constrained" | "unknown";
16
+ export type QuotaTelemetryConfidence = "fresh" | "stale" | "unknown";
17
+ export const ORCHESTRATION_ROUTING_BIASES = ["capability", "balanced", "quota"] as const;
18
+ export type OrchestrationRoutingBias = typeof ORCHESTRATION_ROUTING_BIASES[number];
19
+
20
+ export type ProviderUsageForMapping = {
21
+ generatedAt: number;
22
+ generatedAtLocal: string;
23
+ providers: Array<{
24
+ id: string;
25
+ label: string;
26
+ plan: string | null;
27
+ windows: Array<{
28
+ label: string;
29
+ usedPercent: number;
30
+ percentRemaining: number;
31
+ windowMs?: number | null;
32
+ resetAt: number | null;
33
+ resetIn: string;
34
+ freshness: {
35
+ ageMs: number | null;
36
+ label: string;
37
+ };
38
+ }>;
39
+ }>;
40
+ };
41
+
42
+ export type QuotaWindowPacing = {
43
+ label: string;
44
+ usedPercent: number;
45
+ percentRemaining: number;
46
+ durationMs: number | null;
47
+ elapsedPercent: number | null;
48
+ paceRatio: number | null;
49
+ projectedUsedPercent: number | null;
50
+ status: QuotaPacingStatus;
51
+ resetAt: number | null;
52
+ resetIn: string;
53
+ freshness: string;
54
+ confidence: QuotaTelemetryConfidence;
55
+ };
56
+
57
+ export type ProviderPacingSummary = {
58
+ providerId: string;
59
+ providerLabel: string;
60
+ plan: string | null;
61
+ availability: QuotaAvailability;
62
+ pacing: QuotaPacingStatus;
63
+ confidence: QuotaTelemetryConfidence;
64
+ minimumRemainingPercent: number | null;
65
+ bindingWindow: string | null;
66
+ nextResetAt: number | null;
67
+ nextResetIn: string;
68
+ windows: QuotaWindowPacing[];
69
+ };
70
+
71
+ export type OrchestrationRuntimeRoute = {
72
+ providerId: string;
73
+ harness: string;
74
+ model: string | null;
75
+ effort: string | null;
76
+ profile: string | null;
77
+ };
78
+
79
+ export type OrchestrationRuntimeAlternative = {
80
+ label: string;
81
+ route: OrchestrationRuntimeRoute;
82
+ score: number;
83
+ fit: number;
84
+ capability: string;
85
+ caution: string;
86
+ taskRationale: string;
87
+ quotaRisk: boolean;
88
+ quota: ProviderPacingSummary;
89
+ };
90
+
91
+ export type OrchestrationRuntimeAssignment = {
92
+ role: OrchestrationRoleId;
93
+ roleLabel: string;
94
+ modelLabel: string;
95
+ route: OrchestrationRuntimeRoute;
96
+ score: number;
97
+ fit: number;
98
+ objective: string;
99
+ taskRationale: string;
100
+ capability: string;
101
+ caution: string;
102
+ assertion: string;
103
+ quotaRisk: boolean;
104
+ quotaRiskMessage: string | null;
105
+ quota: ProviderPacingSummary;
106
+ alternatives: OrchestrationRuntimeAlternative[];
107
+ };
108
+
109
+ export type OrchestrationModelGuidanceStatus =
110
+ | "use_now"
111
+ | "available"
112
+ | "use_deliberately"
113
+ | "probe_first"
114
+ | "conserve";
115
+
116
+ export type OrchestrationModelGuidance = {
117
+ modelLabel: string;
118
+ route: OrchestrationRuntimeRoute;
119
+ roles: Array<{ role: OrchestrationRoleId; label: string }>;
120
+ strengths: string[];
121
+ cautions: string[];
122
+ guidance: OrchestrationModelGuidanceStatus;
123
+ quotaRisk: boolean;
124
+ quota: ProviderPacingSummary;
125
+ };
126
+
127
+ export type OrchestrationProviderMap = {
128
+ generatedAt: number;
129
+ generatedAtLocal: string;
130
+ evaluatedAt: number;
131
+ assignments: OrchestrationRuntimeAssignment[];
132
+ bias: OrchestrationRoutingBias;
133
+ models: OrchestrationModelGuidance[];
134
+ };
135
+
136
+ type RuntimeDefinition = {
137
+ label: string;
138
+ route: OrchestrationRuntimeRoute;
139
+ capability: string;
140
+ caution: string;
141
+ };
142
+
143
+ type RuntimeCandidate = RuntimeDefinition & {
144
+ fit: number;
145
+ taskRationale: string;
146
+ };
147
+
148
+ type RolePolicy = {
149
+ label: string;
150
+ objective: string;
151
+ strategy: string;
152
+ candidates: RuntimeCandidate[];
153
+ };
154
+
155
+ const RUNTIMES = {
156
+ fable: runtimeDefinition(
157
+ "Fable 5", "claude", "claude", "claude-fable-5", "max", "Fable",
158
+ "Premium product taste, rapid design iterations, and premise-level critique.",
159
+ "Uses the premium shared Claude allowance; do not spend it on routine mechanical work.",
160
+ ),
161
+ sol: runtimeDefinition(
162
+ "GPT-5.6 Sol", "codex", "codex", "gpt-5.6-sol", "xhigh", null,
163
+ "Load-bearing coding, debugging, integration, and technically grounded judgment.",
164
+ "Highest Codex tier; reserve deep turns for work where failure is expensive.",
165
+ ),
166
+ grok: runtimeDefinition(
167
+ "Grok 4.6", "grok", "grok", "grok-4.6", null, "Grok",
168
+ "Independent assumption checking, deletion pressure, and adversarial critique.",
169
+ "Do not make it the sole implementation authority; verify conclusions against repository evidence.",
170
+ ),
171
+ kimi: runtimeDefinition(
172
+ "Kimi Code", "kimi", "kimi", null, null, "Kimi",
173
+ "Dense repository and study synthesis through the native Kimi harness.",
174
+ "The harness owns the concrete model and quota telemetry can be stale; probe before large fan-out.",
175
+ ),
176
+ opus: runtimeDefinition(
177
+ "Opus 5", "claude", "claude", "claude-opus-5", "high", "Opus",
178
+ "Long-context synthesis, nuanced architecture, and coherent product reasoning.",
179
+ "Premium shared Claude quota; avoid routine scans and repetitive verification.",
180
+ ),
181
+ glm: runtimeDefinition(
182
+ "GLM-5.2", "opencode", "opencode", "opencode-go/glm-5.2", null, "OpenCode",
183
+ "Independent broad-context synthesis through OpenCode.",
184
+ "OpenCode quota is not visible in Scout; use a bounded canary before scaling.",
185
+ ),
186
+ terra: runtimeDefinition(
187
+ "GPT-5.6 Terra", "codex", "codex", "gpt-5.6-terra", "high", null,
188
+ "Balanced implementation, correctness review, and technical critique.",
189
+ "Prefer Sol for the hardest integration boundary when its additional spend is justified.",
190
+ ),
191
+ minimax: runtimeDefinition(
192
+ "MiniMax M3", "minimax", "opencode", "opencode-go/minimax-m3", null, "OpenCode",
193
+ "Cost-effective structured evidence collection and comparison.",
194
+ "Verify load-bearing conclusions with a stronger reviewer before acceptance.",
195
+ ),
196
+ luna: runtimeDefinition(
197
+ "GPT-5.6 Luna", "codex", "codex", "gpt-5.6-luna", "medium", null,
198
+ "Inexpensive source mapping, test enumeration, and mechanical audit work.",
199
+ "Keep it out of sole high-stakes product judgment and final acceptance.",
200
+ ),
201
+ haiku: runtimeDefinition(
202
+ "Haiku 4.5", "claude", "claude", "claude-haiku-4-5", "low", null,
203
+ "Fast narrow scans and classification inside the Claude harness.",
204
+ "Lighter reasoning and a smaller context window make broad synthesis a poor fit.",
205
+ ),
206
+ sonnet: runtimeDefinition(
207
+ "Sonnet 4.6", "claude", "claude", "claude-sonnet-4-6", "high", null,
208
+ "Strong generalist implementation with a large context window.",
209
+ "Shares Claude's provider allowance and can become a poor choice when that pool is guarded.",
210
+ ),
211
+ deepseek: runtimeDefinition(
212
+ "DeepSeek V4 Pro", "opencode", "opencode", "opencode-go/deepseek-v4-pro", null, "OpenCode",
213
+ "Code-centric review through an independent model lineage.",
214
+ "Quota is opaque and findings should not be the sole acceptance authority.",
215
+ ),
216
+ } as const;
217
+
218
+ const ROLE_POLICIES: Record<OrchestrationRoleId, RolePolicy> = {
219
+ product_judgment: {
220
+ label: "Product judgment",
221
+ objective: "Strategic tradeoffs, UX prioritization, scope shaping, and high-level product decisions.",
222
+ strategy: "Select models with high nuanced taste, product-level synthesis, and clear communication of architectural tradeoffs.",
223
+ candidates: [
224
+ roleCandidate("fable", 100, "Unmatched product taste, rapid design iteration synthesis, and high-fidelity UX direction."),
225
+ roleCandidate("sol", 87, "Technically grounded product decisions with strong execution realism."),
226
+ roleCandidate("grok", 82, "Unfiltered perspective on feature bloat and aggressive scope reduction."),
227
+ ],
228
+ },
229
+ large_context_synthesis: {
230
+ label: "Large-context synthesis",
231
+ objective: "Repository-wide investigations, deep architectural archaeology, and multi-file code synthesis.",
232
+ strategy: "Select models with expansive effective context windows and high-density information compression.",
233
+ candidates: [
234
+ roleCandidate("kimi", 100, "Native long-context retrieval and dense codebase relationship synthesis."),
235
+ roleCandidate("opus", 91, "Deep structural understanding across complex multi-package repositories."),
236
+ roleCandidate("glm", 84, "Independent wide-window scanning and structural cross-referencing."),
237
+ ],
238
+ },
239
+ adversarial_critique: {
240
+ label: "Adversarial critique",
241
+ objective: "Challenging design assumptions, rapid design iteration critique, identifying UX/logic gaps, and applying deletion pressure.",
242
+ strategy: "Select high-taste and contrarian model lineages that stress-test design iterations and probe corner cases without sycophantic approval.",
243
+ candidates: [
244
+ roleCandidate("fable", 100, "High-fidelity design critique, rapid UX/visual iteration stress-testing, and premise challenge."),
245
+ roleCandidate("grok", 91, "Contrarian perspective, uncompromising edge-case discovery, and aggressive premise challenge."),
246
+ roleCandidate("terra", 84, "Rigorous technical inspection of code correctness and runtime invariants."),
247
+ ],
248
+ },
249
+ evidence_inventory: {
250
+ label: "Evidence inventory",
251
+ objective: "Grep and AST inventory, reference tracing, log audit, and structured fact collection.",
252
+ strategy: "Select fast, cost-effective models with high precision on structured evidence gathering.",
253
+ candidates: [
254
+ roleCandidate("minimax", 100, "Fast, cost-effective structured evidence indexing and pattern enumeration."),
255
+ roleCandidate("luna", 95, "Low-latency source tracing, test inventory, and reference mapping."),
256
+ roleCandidate("haiku", 87, "Rapid mechanical file scans and categorization."),
257
+ ],
258
+ },
259
+ implementation: {
260
+ label: "Implementation",
261
+ objective: "Code creation, refactoring, type checking, unit test writing, and bug fixing.",
262
+ strategy: "Select code-generation champions with high instruction following and strong language typing skills.",
263
+ candidates: [
264
+ roleCandidate("sol", 100, "Deep technical accuracy, robust multi-file refactors, and complex logic execution."),
265
+ roleCandidate("terra", 91, "Reliable structured implementation with strong type-system adherence."),
266
+ roleCandidate("sonnet", 87, "Strong generalist coding with fast turn times and clear explanations."),
267
+ ],
268
+ },
269
+ code_review: {
270
+ label: "Code review",
271
+ objective: "Diff analysis, security audit, regression detection, and style conformity.",
272
+ strategy: "Select models with high sensitivity to subtle bugs, invariant violations, and edge-case behaviors.",
273
+ candidates: [
274
+ roleCandidate("terra", 100, "Strict invariant checking, subtle logic flaw detection, and boundary test coverage."),
275
+ roleCandidate("opus", 96, "High-level architectural consistency and API surface evolution review."),
276
+ roleCandidate("deepseek", 87, "Independent second-opinion review focused on code-level edge cases."),
277
+ ],
278
+ },
279
+ };
280
+
281
+ export function isOrchestrationRoleId(value: string): value is OrchestrationRoleId {
282
+ return (ORCHESTRATION_ROLE_IDS as readonly string[]).includes(value);
283
+ }
284
+
285
+ export function isQuotaRisk(quota: ProviderPacingSummary): boolean {
286
+ if (quota.availability === "constrained" || quota.availability === "guarded") return true;
287
+ if (quota.minimumRemainingPercent !== null && quota.minimumRemainingPercent <= 20) return true;
288
+ if (quota.pacing === "ahead" && (quota.minimumRemainingPercent ?? 100) <= 35) return true;
289
+ return false;
290
+ }
291
+
292
+ export function isQuotaSafe(quota: ProviderPacingSummary): boolean {
293
+ return quota.confidence === "fresh"
294
+ && quota.minimumRemainingPercent !== null
295
+ && !isQuotaRisk(quota);
296
+ }
297
+
298
+ export function quotaRiskDescription(quota: ProviderPacingSummary): string | null {
299
+ if (!isQuotaRisk(quota)) return null;
300
+ if (quota.minimumRemainingPercent !== null && quota.minimumRemainingPercent <= 15) {
301
+ return `${quota.providerLabel} is near capacity (${quota.minimumRemainingPercent}% remaining). High risk of quota exhaustion.`;
302
+ }
303
+ if (quota.pacing === "ahead") {
304
+ return `${quota.providerLabel} consumption rate exceeds window pace with limited buffer.`;
305
+ }
306
+ return `${quota.providerLabel} quota availability is ${quota.availability}. Failover recommended.`;
307
+ }
308
+
309
+ export function orchestrationRuntimeRouteKey(route: OrchestrationRuntimeRoute): string {
310
+ return [route.providerId, route.harness, route.model, route.effort, route.profile].join("|");
311
+ }
312
+
313
+ export function orchestrationRuntimeRouteLabel(route: OrchestrationRuntimeRoute): string {
314
+ return [
315
+ route.profile ? `profile ${route.profile}` : route.harness,
316
+ route.model,
317
+ route.effort,
318
+ ].filter(Boolean).join(" / ");
319
+ }
320
+
321
+ export function buildOrchestrationAskArguments(
322
+ route: OrchestrationRuntimeRoute,
323
+ request = "<request>",
324
+ ): string[] {
325
+ const normalizedProfile = route.profile?.trim().toLowerCase() ?? null;
326
+ const profileCanSelectExactModel = normalizedProfile !== "opencode" && normalizedProfile !== "oc";
327
+ const args = route.profile && profileCanSelectExactModel
328
+ ? ["--profile", route.profile]
329
+ : [
330
+ "--harness",
331
+ route.harness,
332
+ ...(route.model ? ["--model", route.model] : []),
333
+ ];
334
+ if (route.effort) args.push("--effort", route.effort);
335
+ args.push(request);
336
+ return args;
337
+ }
338
+
339
+ export function renderOrchestrationAskCommand(
340
+ route: OrchestrationRuntimeRoute,
341
+ request = "<request>",
342
+ ): string {
343
+ return ["scout", "ask", ...buildOrchestrationAskArguments(route, request)]
344
+ .map(shellQuoteArgument)
345
+ .join(" ");
346
+ }
347
+
348
+ export function buildOrchestrationProviderMap(
349
+ report: ProviderUsageForMapping,
350
+ options: {
351
+ now?: number;
352
+ role?: OrchestrationRoleId | null;
353
+ bias?: OrchestrationRoutingBias;
354
+ } = {},
355
+ ): OrchestrationProviderMap {
356
+ const now = finiteNumber(options.now) ?? Date.now();
357
+ const bias = options.bias ?? "balanced";
358
+ const quotaWeight = routingBiasWeight(bias);
359
+ const summaries = new Map(
360
+ report.providers.map((provider) => [provider.id, summarizeProviderPacing(provider, now)]),
361
+ );
362
+ const roles = options.role ? [options.role] : [...ORCHESTRATION_ROLE_IDS];
363
+ const assignments = roles.map((role) => {
364
+ const policy = ROLE_POLICIES[role];
365
+ const ranked = policy.candidates
366
+ .map((runtime) => {
367
+ const quota = summaries.get(runtime.route.providerId) ?? unknownProviderSummary(runtime.route.providerId);
368
+ return { runtime, quota, score: runtime.fit + quotaScoreAdjustment(quota, now) * quotaWeight };
369
+ })
370
+ .sort((left, right) => right.score - left.score || right.runtime.fit - left.runtime.fit);
371
+ const selected = ranked[0]!;
372
+ const quotaRisk = isQuotaRisk(selected.quota);
373
+ const alternatives = ranked.slice(1);
374
+ if (quotaRisk) {
375
+ alternatives.sort((left, right) => {
376
+ const leftRank = fallbackPreference(left, selected.runtime.route.providerId);
377
+ const rightRank = fallbackPreference(right, selected.runtime.route.providerId);
378
+ return leftRank - rightRank || right.score - left.score || right.runtime.fit - left.runtime.fit;
379
+ });
380
+ }
381
+ return {
382
+ role,
383
+ roleLabel: policy.label,
384
+ modelLabel: selected.runtime.label,
385
+ route: selected.runtime.route,
386
+ score: selected.score,
387
+ fit: selected.runtime.fit,
388
+ objective: policy.objective,
389
+ taskRationale: selected.runtime.taskRationale,
390
+ capability: selected.runtime.capability,
391
+ caution: selected.runtime.caution,
392
+ assertion: providerAssertion(selected.quota),
393
+ quotaRisk,
394
+ quotaRiskMessage: quotaRiskDescription(selected.quota),
395
+ quota: selected.quota,
396
+ alternatives: alternatives.slice(0, 2).map(({ runtime, quota, score }) => ({
397
+ label: runtime.label,
398
+ route: runtime.route,
399
+ score,
400
+ fit: runtime.fit,
401
+ capability: runtime.capability,
402
+ caution: runtime.caution,
403
+ taskRationale: runtime.taskRationale,
404
+ quotaRisk: isQuotaRisk(quota),
405
+ quota,
406
+ })),
407
+ };
408
+ });
409
+ const models = buildModelGuidance(roles, summaries, now);
410
+
411
+ return {
412
+ generatedAt: report.generatedAt,
413
+ generatedAtLocal: report.generatedAtLocal,
414
+ evaluatedAt: now,
415
+ assignments,
416
+ models,
417
+ bias,
418
+ };
419
+ }
420
+
421
+ function buildModelGuidance(
422
+ roles: OrchestrationRoleId[],
423
+ summaries: Map<string, ProviderPacingSummary>,
424
+ now: number,
425
+ ): OrchestrationModelGuidance[] {
426
+ const models = new Map<string, {
427
+ runtime: RuntimeDefinition;
428
+ roles: Array<{ role: OrchestrationRoleId; label: string }>;
429
+ }>();
430
+
431
+ for (const role of roles) {
432
+ const policy = ROLE_POLICIES[role];
433
+ for (const candidate of policy.candidates) {
434
+ const key = orchestrationRuntimeRouteKey(candidate.route);
435
+ const existing = models.get(key);
436
+ if (existing) {
437
+ existing.roles.push({ role, label: policy.label });
438
+ } else {
439
+ models.set(key, {
440
+ runtime: candidate,
441
+ roles: [{ role, label: policy.label }],
442
+ });
443
+ }
444
+ }
445
+ }
446
+
447
+ return [...models.values()].map(({ runtime, roles: mappedRoles }) => {
448
+ const quota = summaries.get(runtime.route.providerId) ?? unknownProviderSummary(runtime.route.providerId);
449
+ return {
450
+ modelLabel: runtime.label,
451
+ route: runtime.route,
452
+ roles: mappedRoles,
453
+ strengths: [runtime.capability],
454
+ cautions: [runtime.caution],
455
+ guidance: modelGuidanceStatus(quota, now),
456
+ quotaRisk: isQuotaRisk(quota),
457
+ quota,
458
+ };
459
+ });
460
+ }
461
+
462
+ export function renderOrchestrationProviderMap(map: OrchestrationProviderMap): string {
463
+ const lines = [
464
+ `Orchestration map · quota-aware · ${map.bias} bias · generated ${map.generatedAtLocal}`,
465
+ "Dispatch roles are recommendations, not durable `scout role` assignments.",
466
+ ];
467
+
468
+ for (const assignment of map.assignments) {
469
+ lines.push("", assignment.roleLabel);
470
+ lines.push(` ${assignment.modelLabel} · ${formatRoute(assignment.route)}`);
471
+ lines.push(` ${assignment.capability}`);
472
+ lines.push(` ${assignment.assertion}`);
473
+ if (assignment.alternatives.length > 0) {
474
+ lines.push(` Alternatives: ${assignment.alternatives.map((alternative) => alternative.label).join(" · ")}`);
475
+ }
476
+ }
477
+
478
+ return lines.join("\n");
479
+ }
480
+
481
+ export function summarizeProviderPacing(
482
+ provider: ProviderUsageForMapping["providers"][number],
483
+ now: number,
484
+ ): ProviderPacingSummary {
485
+ const windows = provider.windows
486
+ .filter((window) => !/^video\b/iu.test(window.label))
487
+ .map((window) => paceQuotaWindow(window, now));
488
+ const binding = [...windows].sort((left, right) => left.percentRemaining - right.percentRemaining)[0] ?? null;
489
+ const paced = windows
490
+ .filter((window) => window.paceRatio !== null)
491
+ .sort((left, right) => right.paceRatio! - left.paceRatio!)[0] ?? null;
492
+ const confidences = windows.map((window) => window.confidence);
493
+ const confidence: QuotaTelemetryConfidence = confidences.includes("stale")
494
+ ? "stale"
495
+ : confidences.includes("fresh")
496
+ ? "fresh"
497
+ : "unknown";
498
+ const nextReset = [...windows]
499
+ .filter((window) => window.resetAt !== null && window.resetAt > now)
500
+ .sort((left, right) => left.resetAt! - right.resetAt!)[0] ?? null;
501
+ const minimumRemainingPercent = binding?.percentRemaining ?? null;
502
+
503
+ return {
504
+ providerId: provider.id,
505
+ providerLabel: provider.label,
506
+ plan: provider.plan,
507
+ availability: availabilityFromRemaining(minimumRemainingPercent),
508
+ pacing: paced?.status ?? "unknown",
509
+ confidence,
510
+ minimumRemainingPercent,
511
+ bindingWindow: binding?.label ?? null,
512
+ nextResetAt: nextReset?.resetAt ?? null,
513
+ nextResetIn: nextReset?.resetIn ?? "",
514
+ windows,
515
+ };
516
+ }
517
+
518
+ function paceQuotaWindow(
519
+ window: ProviderUsageForMapping["providers"][number]["windows"][number],
520
+ now: number,
521
+ ): QuotaWindowPacing {
522
+ const durationMs = finiteNumber(window.windowMs) ?? inferWindowDurationMs(window.label, window.resetAt);
523
+ const elapsedPercent = durationMs !== null && window.resetAt !== null
524
+ ? roundPercent(clamp(100 - ((window.resetAt - now) / durationMs) * 100, 0, 100))
525
+ : null;
526
+ const paceRatio = elapsedPercent !== null && elapsedPercent >= 1
527
+ ? roundRatio(window.usedPercent / elapsedPercent)
528
+ : null;
529
+ const projectedUsedPercent = paceRatio === null ? null : roundPercent(paceRatio * 100);
530
+ const status = pacingStatus(window.usedPercent, elapsedPercent, paceRatio);
531
+ const staleAfterMs = durationMs === null ? DAY_MS : Math.min(DAY_MS, Math.max(HOUR_MS, durationMs / 4));
532
+ const confidence: QuotaTelemetryConfidence = window.freshness.ageMs === null
533
+ ? "unknown"
534
+ : window.freshness.ageMs > staleAfterMs
535
+ ? "stale"
536
+ : "fresh";
537
+
538
+ return {
539
+ label: window.label,
540
+ usedPercent: window.usedPercent,
541
+ percentRemaining: window.percentRemaining,
542
+ durationMs,
543
+ elapsedPercent,
544
+ paceRatio,
545
+ projectedUsedPercent,
546
+ status,
547
+ resetAt: window.resetAt,
548
+ resetIn: window.resetIn,
549
+ freshness: window.freshness.label,
550
+ confidence,
551
+ };
552
+ }
553
+
554
+ function runtimeDefinition(
555
+ label: string,
556
+ providerId: string,
557
+ harness: string,
558
+ model: string | null,
559
+ effort: string | null,
560
+ profile: string | null,
561
+ capability: string,
562
+ caution: string,
563
+ ): RuntimeDefinition {
564
+ return {
565
+ label,
566
+ route: { providerId, harness, model, effort, profile },
567
+ capability,
568
+ caution,
569
+ };
570
+ }
571
+
572
+ function roleCandidate(runtimeId: keyof typeof RUNTIMES, fit: number, rationale?: string): RuntimeCandidate {
573
+ return {
574
+ ...RUNTIMES[runtimeId],
575
+ fit,
576
+ taskRationale: rationale ?? RUNTIMES[runtimeId].capability,
577
+ };
578
+ }
579
+
580
+ function modelGuidanceStatus(
581
+ quota: ProviderPacingSummary,
582
+ now: number,
583
+ ): OrchestrationModelGuidanceStatus {
584
+ if (isQuotaRisk(quota)) return "conserve";
585
+ if (quota.confidence !== "fresh") return "probe_first";
586
+ if (quota.availability === "constrained") return "conserve";
587
+ if (
588
+ quota.nextResetAt !== null
589
+ && quota.nextResetAt > now
590
+ && quota.nextResetAt - now <= DAY_MS
591
+ && quota.pacing !== "ahead"
592
+ && (quota.minimumRemainingPercent ?? 0) >= 10
593
+ ) {
594
+ return "use_now";
595
+ }
596
+ if (quota.availability === "guarded") return "conserve";
597
+ if (quota.pacing === "ahead") return "use_deliberately";
598
+ if (quota.availability === "abundant" && quota.pacing === "underused") return "use_now";
599
+ return "available";
600
+ }
601
+
602
+ function routingBiasWeight(bias: OrchestrationRoutingBias): number {
603
+ switch (bias) {
604
+ case "capability": return 0.25;
605
+ case "quota": return 1.75;
606
+ case "balanced": return 1;
607
+ }
608
+ }
609
+
610
+ function quotaScoreAdjustment(quota: ProviderPacingSummary, now: number): number {
611
+ let score = 0;
612
+ switch (quota.availability) {
613
+ case "abundant": score += 10; break;
614
+ case "available": score += 5; break;
615
+ case "guarded": score -= 5; break;
616
+ case "constrained": score -= 30; break;
617
+ case "unknown": score -= 2; break;
618
+ }
619
+ if (quota.pacing === "underused") score += 5;
620
+ if (quota.pacing === "ahead") score -= 8;
621
+ if (quota.confidence === "stale") score -= 8;
622
+ if (quota.confidence === "unknown") score -= 3;
623
+ if (
624
+ quota.pacing === "underused"
625
+ && quota.nextResetAt !== null
626
+ && quota.nextResetAt > now
627
+ && quota.nextResetAt - now <= 2 * DAY_MS
628
+ ) {
629
+ score += 3;
630
+ }
631
+ return score;
632
+ }
633
+
634
+ function providerAssertion(quota: ProviderPacingSummary): string {
635
+ if (quota.minimumRemainingPercent === null) {
636
+ return `${titleCase(quota.providerId)} quota telemetry unavailable; use a bounded canary before scaling.`;
637
+ }
638
+ const pace = quota.pacing === "on_track"
639
+ ? "usage tracks elapsed time"
640
+ : quota.pacing === "underused"
641
+ ? "usage trails elapsed time"
642
+ : quota.pacing === "ahead"
643
+ ? "usage exceeds elapsed time"
644
+ : "pace unknown";
645
+ const reset = quota.nextResetIn ? ` · next reset ${quota.nextResetIn}` : "";
646
+ return `${formatPercent(quota.minimumRemainingPercent)} minimum remaining · ${pace} · telemetry ${quota.confidence}${reset}.`;
647
+ }
648
+
649
+ function unknownProviderSummary(providerId: string): ProviderPacingSummary {
650
+ return {
651
+ providerId,
652
+ providerLabel: titleCase(providerId),
653
+ plan: null,
654
+ availability: "unknown",
655
+ pacing: "unknown",
656
+ confidence: "unknown",
657
+ minimumRemainingPercent: null,
658
+ bindingWindow: null,
659
+ nextResetAt: null,
660
+ nextResetIn: "",
661
+ windows: [],
662
+ };
663
+ }
664
+
665
+ function inferWindowDurationMs(label: string, resetAt: number | null): number | null {
666
+ const normalized = label.trim().toLowerCase();
667
+ const duration = normalized.match(/(?:^|\s)(\d+(?:\.\d+)?)([hd])$/u);
668
+ if (duration) {
669
+ const value = Number(duration[1]);
670
+ return duration[2] === "h" ? value * HOUR_MS : value * DAY_MS;
671
+ }
672
+ if (normalized === "weekly" || normalized === "week" || normalized === "7-day") return 7 * DAY_MS;
673
+ if (normalized === "5-hour") return 5 * HOUR_MS;
674
+ if (normalized === "monthly" || normalized === "month") {
675
+ if (resetAt === null) return 30 * DAY_MS;
676
+ const reset = new Date(resetAt);
677
+ const prior = new Date(resetAt);
678
+ prior.setUTCMonth(prior.getUTCMonth() - 1);
679
+ const calendarDuration = reset.getTime() - prior.getTime();
680
+ return calendarDuration > 0 ? calendarDuration : 30 * DAY_MS;
681
+ }
682
+ return null;
683
+ }
684
+
685
+ function pacingStatus(
686
+ usedPercent: number,
687
+ elapsedPercent: number | null,
688
+ paceRatio: number | null,
689
+ ): QuotaPacingStatus {
690
+ if (elapsedPercent === null || paceRatio === null) return "unknown";
691
+ const delta = usedPercent - elapsedPercent;
692
+ if (paceRatio > 1.1 && delta > 3) return "ahead";
693
+ if (paceRatio < 0.7 && delta < -10) return "underused";
694
+ return "on_track";
695
+ }
696
+
697
+ function availabilityFromRemaining(remaining: number | null): QuotaAvailability {
698
+ if (remaining === null) return "unknown";
699
+ if (remaining >= 60) return "abundant";
700
+ if (remaining >= 30) return "available";
701
+ if (remaining >= 15) return "guarded";
702
+ return "constrained";
703
+ }
704
+
705
+ function formatRoute(route: OrchestrationRuntimeRoute): string {
706
+ return orchestrationRuntimeRouteLabel(route);
707
+ }
708
+
709
+ function fallbackPreference(
710
+ candidate: { runtime: RuntimeCandidate; quota: ProviderPacingSummary },
711
+ selectedProviderId: string,
712
+ ): number {
713
+ const quotaSafe = isQuotaSafe(candidate.quota);
714
+ const providerDiverse = candidate.runtime.route.providerId !== selectedProviderId;
715
+ if (quotaSafe && providerDiverse) return 0;
716
+ if (quotaSafe) return 1;
717
+ if (providerDiverse) return 2;
718
+ return 3;
719
+ }
720
+
721
+ function shellQuoteArgument(value: string): string {
722
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/u.test(value)) return value;
723
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
724
+ }
725
+
726
+ function titleCase(value: string): string {
727
+ return value
728
+ .split(/[-_\s]+/u)
729
+ .filter(Boolean)
730
+ .map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
731
+ .join(" ") || "Unknown";
732
+ }
733
+
734
+ function formatPercent(value: number): string {
735
+ return `${Number.isInteger(value) ? value.toFixed(0) : value.toFixed(1)}%`;
736
+ }
737
+
738
+ function finiteNumber(value: unknown): number | null {
739
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
740
+ }
741
+
742
+ function roundPercent(value: number): number {
743
+ return Math.round(value * 10) / 10;
744
+ }
745
+
746
+ function roundRatio(value: number): number {
747
+ return Math.round(value * 100) / 100;
748
+ }
749
+
750
+ function clamp(value: number, minimum: number, maximum: number): number {
751
+ return Math.min(maximum, Math.max(minimum, value));
752
+ }