@oeronteros-1/opencode-orchestra 1.0.28 → 1.0.30

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 (43) hide show
  1. package/README.md +79 -1
  2. package/dashboard-dist/assets/index-Bl2J2mOP.js +132 -0
  3. package/dashboard-dist/assets/index-CX2uv1EJ.css +1 -0
  4. package/dashboard-dist/index.html +2 -2
  5. package/dist/agents/integrator.js +1 -1
  6. package/dist/agents/integrator.js.map +1 -1
  7. package/dist/agents/lead.js +2 -13
  8. package/dist/agents/lead.js.map +1 -1
  9. package/dist/config/load.d.ts +5 -0
  10. package/dist/config/load.js +20 -5
  11. package/dist/config/load.js.map +1 -1
  12. package/dist/dashboard/server.js +38 -17
  13. package/dist/dashboard/server.js.map +1 -1
  14. package/dist/diagnostics/doctor.js +194 -0
  15. package/dist/diagnostics/doctor.js.map +1 -1
  16. package/dist/diagnostics/update.js +8 -7
  17. package/dist/diagnostics/update.js.map +1 -1
  18. package/dist/index.js +21 -3
  19. package/dist/index.js.map +1 -1
  20. package/dist/orchestration/ownership.d.ts +19 -0
  21. package/dist/orchestration/ownership.js +26 -0
  22. package/dist/orchestration/ownership.js.map +1 -1
  23. package/dist/routing/classifier.js +20 -2
  24. package/dist/routing/classifier.js.map +1 -1
  25. package/dist/routing/fallback.d.ts +19 -10
  26. package/dist/routing/fallback.js +89 -18
  27. package/dist/routing/fallback.js.map +1 -1
  28. package/dist/routing/model-resolver.d.ts +47 -0
  29. package/dist/routing/model-resolver.js +110 -2
  30. package/dist/routing/model-resolver.js.map +1 -1
  31. package/dist/spawn.d.ts +14 -1
  32. package/dist/spawn.js +67 -4
  33. package/dist/spawn.js.map +1 -1
  34. package/dist/telemetry/ledger.d.ts +49 -0
  35. package/dist/telemetry/ledger.js +157 -0
  36. package/dist/telemetry/ledger.js.map +1 -1
  37. package/dist/telemetry/live.js +1 -1
  38. package/dist/telemetry/live.js.map +1 -1
  39. package/dist/tools.js +71 -3
  40. package/dist/tools.js.map +1 -1
  41. package/package.json +4 -4
  42. package/dashboard-dist/assets/index-BTleCKc7.js +0 -124
  43. package/dashboard-dist/assets/index-igZBUoKO.css +0 -2
@@ -1 +1 @@
1
- {"version":3,"file":"classifier.js","sourceRoot":"","sources":["../../src/routing/classifier.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"classifier.js","sourceRoot":"","sources":["../../src/routing/classifier.ts"],"names":[],"mappings":"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"}
@@ -1,33 +1,42 @@
1
- import type { BudgetMode, ModelCandidateInput, ModelCost } from "../config/schema.js";
1
+ import type { BudgetMode, ModelCandidateInput, ModelCost, ModelTier } from "../config/schema.js";
2
2
  import { resolveModel } from "./model-resolver.js";
3
3
  export interface FallbackEntry {
4
4
  id: string;
5
5
  cost: ModelCost;
6
- /** Lower is cheaper; used to prefer cheaper fallbacks after a failure. */
6
+ /** Static cost-class rank: paid (2) > subscription (1) > free (0). */
7
7
  costRank: number;
8
+ /** True when the capability is declared or explicitly scored; false when unknown. */
9
+ compatible: boolean;
10
+ priority: number;
11
+ tier: ModelTier;
8
12
  }
9
13
  export interface FallbackChain {
10
14
  /** The winning model id (first choice). */
11
15
  primary: string;
12
- /** Ordered cheaper alternatives after the primary. */
16
+ /** Ordered alternatives after the primary (compatibility first). */
13
17
  alternatives: FallbackEntry[];
14
18
  /** All entries (primary + alternatives) in failover order. */
15
19
  all: FallbackEntry[];
16
20
  }
21
+ export type ErrorKind = "rate-limit" | "server" | "timeout" | "auth" | "invalid-request" | "other";
17
22
  interface RetryableError {
18
- /** HTTP status or "timeout". Rate-limit (429) and 5xx are retryable. */
19
- kind: "rate-limit" | "server" | "timeout" | "other";
23
+ /** Retryable failures trigger failover; auth and invalid-request are terminal. */
24
+ kind: ErrorKind;
20
25
  }
21
26
  /**
22
- * Classify an error into retryable categories. A retryable failure should
23
- * trigger failover to a cheaper model in the chain.
27
+ * Classify an error into policy categories. Only rate-limit, timeout, and
28
+ * server failures are retryable; auth and invalid-request stop the chain.
24
29
  */
25
30
  export declare function classifyError(error: unknown): RetryableError;
26
31
  export declare function isRetryable(kind: RetryableError["kind"]): boolean;
27
32
  /**
28
33
  * Build a failover chain for a capability pool. The primary is the current
29
- * winner; alternatives are the remaining candidates sorted by increasing cost
30
- * (cheaper first), so a retry steps down in price instead of up.
34
+ * winner; alternatives are the remaining candidates ranked deterministically:
35
+ * capability compatibility, then priority, tier, budget-aware cost class, and
36
+ * model id. Explicitly incompatible candidates are excluded before resolution
37
+ * so the primary is always compatible; unknown-capability candidates stay
38
+ * eligible but rank below compatible ones. Budget filters (paid exclusion)
39
+ * apply to alternatives as well as the primary.
31
40
  */
32
41
  export declare function buildFallbackChain(pool: ModelCandidateInput[], capability: Parameters<typeof resolveModel>[0]["capability"], budget: BudgetMode, allowPaid: boolean, options?: {
33
42
  preferredCosts?: ModelCost[];
@@ -37,7 +46,7 @@ export declare function buildFallbackChain(pool: ModelCandidateInput[], capabili
37
46
  }): FallbackChain | undefined;
38
47
  /**
39
48
  * Given a chain and the id of a model that just failed, return the next
40
- * cheaper alternative to try, or undefined when the chain is exhausted.
49
+ * alternative to try, or undefined when the chain is exhausted.
41
50
  */
42
51
  export declare function nextAfterFailure(chain: FallbackChain, failedId: string): string | undefined;
43
52
  export {};
@@ -1,11 +1,39 @@
1
- import { normalizeCandidate, resolveModel } from "./model-resolver.js";
1
+ import { isCapabilityCompatible, isCapabilityIncompatible, normalizeCandidate, resolveModel, } from "./model-resolver.js";
2
2
  const COST_RANK = { paid: 2, subscription: 1, free: 0 };
3
+ const TIER_RANK = { frontier: 2, lead: 1, worker: 0 };
4
+ /**
5
+ * Budget-aware cost-class preference applied as the tie-break after priority
6
+ * and tier. `ModelCost` is a closed union, so an unknown cost can never be
7
+ * coerced into a cheaper class.
8
+ */
9
+ const BUDGET_COST_PREFERENCE = {
10
+ eco: ["free", "subscription", "paid"],
11
+ balanced: ["subscription", "free", "paid"],
12
+ quality: ["subscription", "paid", "free"],
13
+ ebobo: ["paid", "subscription", "free"],
14
+ };
3
15
  function compareCodepoints(a, b) {
4
16
  return a < b ? -1 : a > b ? 1 : 0;
5
17
  }
18
+ function costPreferenceOrder(budget, preferredCosts) {
19
+ if (preferredCosts && preferredCosts.length > 0)
20
+ return preferredCosts;
21
+ return BUDGET_COST_PREFERENCE[budget];
22
+ }
23
+ function costPreferenceRank(cost, order) {
24
+ const index = order.indexOf(cost);
25
+ return index === -1 ? order.length : index;
26
+ }
27
+ function isBudgetEligible(candidate, allowPaid, paidCallsUsed, maxPaidCalls) {
28
+ if (candidate.cost !== "paid")
29
+ return true;
30
+ if (!allowPaid)
31
+ return false;
32
+ return maxPaidCalls === undefined || paidCallsUsed < maxPaidCalls;
33
+ }
6
34
  /**
7
- * Classify an error into retryable categories. A retryable failure should
8
- * trigger failover to a cheaper model in the chain.
35
+ * Classify an error into policy categories. Only rate-limit, timeout, and
36
+ * server failures are retryable; auth and invalid-request stop the chain.
9
37
  */
10
38
  export function classifyError(error) {
11
39
  const candidate = typeof error === "object" && error !== null ? error : undefined;
@@ -18,6 +46,12 @@ export function classifyError(error) {
18
46
  : String(error);
19
47
  const status = candidate?.status ?? candidate?.statusCode;
20
48
  const text = (status !== undefined ? String(status) + " " : "") + message.toLowerCase();
49
+ if (status === 401 || status === 403 || text.includes("unauthorized") || text.includes("forbidden") || text.includes("invalid api key") || text.includes("authentication")) {
50
+ return { kind: "auth" };
51
+ }
52
+ if (status === 400 || status === 404 || text.includes("not found") || text.includes("bad request") || text.includes("invalid request")) {
53
+ return { kind: "invalid-request" };
54
+ }
21
55
  if (status === 429 || text.includes("429") || text.includes("rate limit") || text.includes("rate-limit") || text.includes("too many requests")) {
22
56
  return { kind: "rate-limit" };
23
57
  }
@@ -34,15 +68,25 @@ export function isRetryable(kind) {
34
68
  }
35
69
  /**
36
70
  * Build a failover chain for a capability pool. The primary is the current
37
- * winner; alternatives are the remaining candidates sorted by increasing cost
38
- * (cheaper first), so a retry steps down in price instead of up.
71
+ * winner; alternatives are the remaining candidates ranked deterministically:
72
+ * capability compatibility, then priority, tier, budget-aware cost class, and
73
+ * model id. Explicitly incompatible candidates are excluded before resolution
74
+ * so the primary is always compatible; unknown-capability candidates stay
75
+ * eligible but rank below compatible ones. Budget filters (paid exclusion)
76
+ * apply to alternatives as well as the primary.
39
77
  */
40
78
  export function buildFallbackChain(pool, capability, budget, allowPaid, options = {}) {
41
79
  const normalized = pool.map(normalizeCandidate);
42
80
  if (normalized.length === 0)
43
81
  return undefined;
82
+ // The primary must always be capability-compatible: drop explicitly
83
+ // incompatible candidates before resolution so a high-priority incompatible
84
+ // model (e.g. an ebobo frontier) cannot take the primary slot.
85
+ const compatiblePool = normalized.filter((candidate) => !isCapabilityIncompatible(candidate, capability));
86
+ if (compatiblePool.length === 0)
87
+ return undefined;
44
88
  const winner = resolveModel({
45
- pool,
89
+ pool: compatiblePool,
46
90
  capability,
47
91
  budget,
48
92
  allowPaid,
@@ -51,31 +95,58 @@ export function buildFallbackChain(pool, capability, budget, allowPaid, options
51
95
  ...(options.paidCallsUsed !== undefined ? { paidCallsUsed: options.paidCallsUsed } : {}),
52
96
  ...(options.maxPaidCalls !== undefined ? { maxPaidCalls: options.maxPaidCalls } : {}),
53
97
  });
54
- const primary = winner?.id ?? normalized[0]?.id;
55
- if (!primary)
98
+ // If every compatible candidate was budget-excluded (e.g. an all-paid pool
99
+ // under an eco budget or a fully exhausted paid cap), `resolveModel` has no
100
+ // winner and there is no valid primary — never fail open to the first raw
101
+ // pool entry, which could surface a paid model as primary.
102
+ if (!winner)
56
103
  return undefined;
57
- const byId = new Map(normalized.map((candidate) => [candidate.id, candidate]));
58
- const primaryEntry = toEntry(byId.get(primary));
59
- const alternatives = normalized
104
+ const primary = winner.id;
105
+ const paidCallsUsed = options.paidCallsUsed ?? 0;
106
+ const costOrder = costPreferenceOrder(budget, options.preferredCosts);
107
+ const byId = new Map(compatiblePool.map((candidate) => [candidate.id, candidate]));
108
+ const primaryCandidate = byId.get(primary);
109
+ const primaryEntry = primaryCandidate ? toEntry(primaryCandidate, capability) : undefined;
110
+ const alternatives = compatiblePool
60
111
  .filter((candidate) => candidate.id !== primary)
61
- .sort((a, b) => COST_RANK[a.cost] - COST_RANK[b.cost] || compareCodepoints(a.id, b.id))
62
- .map((candidate) => toEntry(candidate));
112
+ .filter((candidate) => isBudgetEligible(candidate, allowPaid, paidCallsUsed, options.maxPaidCalls))
113
+ .sort((a, b) => {
114
+ const compat = compatibilityTier(b, capability) - compatibilityTier(a, capability);
115
+ if (compat !== 0)
116
+ return compat;
117
+ if (a.priority !== b.priority)
118
+ return b.priority - a.priority;
119
+ const tier = TIER_RANK[b.tier] - TIER_RANK[a.tier];
120
+ if (tier !== 0)
121
+ return tier;
122
+ const cost = costPreferenceRank(a.cost, costOrder) - costPreferenceRank(b.cost, costOrder);
123
+ if (cost !== 0)
124
+ return cost;
125
+ return compareCodepoints(a.id, b.id);
126
+ })
127
+ .map((candidate) => toEntry(candidate, capability));
63
128
  return {
64
129
  primary,
65
130
  alternatives,
66
131
  all: primaryEntry ? [primaryEntry, ...alternatives] : alternatives,
67
132
  };
68
133
  }
69
- function toEntry(candidate) {
134
+ function compatibilityTier(candidate, capability) {
135
+ return isCapabilityCompatible(candidate, capability) ? 1 : 0;
136
+ }
137
+ function toEntry(candidate, capability) {
70
138
  return {
71
- id: candidate?.id ?? "",
72
- cost: candidate?.cost ?? "free",
73
- costRank: COST_RANK[candidate?.cost ?? "free"],
139
+ id: candidate.id,
140
+ cost: candidate.cost,
141
+ costRank: COST_RANK[candidate.cost],
142
+ compatible: isCapabilityCompatible(candidate, capability),
143
+ priority: candidate.priority,
144
+ tier: candidate.tier,
74
145
  };
75
146
  }
76
147
  /**
77
148
  * Given a chain and the id of a model that just failed, return the next
78
- * cheaper alternative to try, or undefined when the chain is exhausted.
149
+ * alternative to try, or undefined when the chain is exhausted.
79
150
  */
80
151
  export function nextAfterFailure(chain, failedId) {
81
152
  const index = chain.all.findIndex((entry) => entry.id === failedId);
@@ -1 +1 @@
1
- {"version":3,"file":"fallback.js","sourceRoot":"","sources":["../../src/routing/fallback.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"fallback.js","sourceRoot":"","sources":["../../src/routing/fallback.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,24 @@
1
1
  import type { BudgetMode, CapabilityName, ModelCandidateInput, ModelCost, ModelTier } from "../config/schema.js";
2
+ /**
3
+ * Stable machine-readable routing decision identifier. The resolver only emits
4
+ * the scoring codes; `exact_override` is reserved for an explicit orch-lead
5
+ * agent override and is produced by the tool layer, not the resolver.
6
+ */
7
+ export type RoutingReasonCode = "frontier" | "preferred_tier" | "preferred_cost" | "capability_match" | "price" | "priority" | "exact_override";
8
+ export interface RoutingReason {
9
+ code: RoutingReasonCode;
10
+ /** Bounded diagnostic copy; never contains secrets, prompts, or credentials. */
11
+ text: string;
12
+ matchedCapabilities: CapabilityName[];
13
+ score: number;
14
+ budget: BudgetMode;
15
+ }
16
+ /**
17
+ * Bound a routing reason's diagnostic text to a fixed length. The text only
18
+ * carries model id, cost class, tier, and score — never secrets, prompts, or
19
+ * credentials — so a plain truncation is safe.
20
+ */
21
+ export declare function boundReasonText(text: string, limit?: number): string;
2
22
  export interface ModelCandidate {
3
23
  id: string;
4
24
  cost: ModelCost;
@@ -23,6 +43,13 @@ export interface ResolveModelRequest {
23
43
  paidCallsUsed?: number;
24
44
  maxPaidCalls?: number;
25
45
  }
46
+ /**
47
+ * The shared resolution parameters for the orch-lead agent. Both the tool
48
+ * layer (routing metadata) and `createLeadAgent` (the actually assigned model)
49
+ * resolve the lead pool through this exact request so the reported lead always
50
+ * matches the assigned one.
51
+ */
52
+ export declare function leadResolveRequest(budget: BudgetMode): Omit<ResolveModelRequest, "pool" | "capability">;
26
53
  export interface ResolvedModel {
27
54
  id: string;
28
55
  score: number;
@@ -35,6 +62,26 @@ export interface ResolvedModel {
35
62
  * with a retryable error, step down this list instead of re-hitting it.
36
63
  */
37
64
  fallback: string[];
65
+ /** Structured, machine-readable routing decision for the winning model. */
66
+ routingReason?: RoutingReason;
38
67
  }
39
68
  export declare function normalizeCandidate(input: ModelCandidateInput): ModelCandidate;
69
+ /**
70
+ * Shared capability scoring helpers. `resolveModel` uses these for scoring and
71
+ * `buildFallbackChain` uses them to filter and rank fallback alternatives.
72
+ */
73
+ export declare function hasDeclaredCapability(candidate: ModelCandidate, capability: CapabilityName): boolean;
74
+ export declare function hasExplicitScore(candidate: ModelCandidate, capability: CapabilityName): boolean;
75
+ /**
76
+ * A candidate is compatible when it either declares the capability or carries
77
+ * an explicit positive score for it.
78
+ */
79
+ export declare function isCapabilityCompatible(candidate: ModelCandidate, capability: CapabilityName): boolean;
80
+ /**
81
+ * A candidate is explicitly incompatible when it declares *some* capabilities,
82
+ * does not declare the required one, and has no explicit score for it. Unknown
83
+ * candidates (empty capabilities) are NOT incompatible: they stay eligible but
84
+ * rank at lower confidence.
85
+ */
86
+ export declare function isCapabilityIncompatible(candidate: ModelCandidate, capability: CapabilityName): boolean;
40
87
  export declare function resolveModel(request: ResolveModelRequest): ResolvedModel | undefined;
@@ -1,4 +1,38 @@
1
1
  import { costAdjustment, getBudgetPolicy } from "./budget.js";
2
+ /** Maximum length of the `RoutingReason.text` diagnostic string. */
3
+ const REASON_TEXT_LIMIT = 200;
4
+ /**
5
+ * Bound a routing reason's diagnostic text to a fixed length. The text only
6
+ * carries model id, cost class, tier, and score — never secrets, prompts, or
7
+ * credentials — so a plain truncation is safe.
8
+ */
9
+ export function boundReasonText(text, limit = REASON_TEXT_LIMIT) {
10
+ if (text.length <= limit)
11
+ return text;
12
+ return text.slice(0, limit - 1) + "…";
13
+ }
14
+ /**
15
+ * The shared resolution parameters for the orch-lead agent. Both the tool
16
+ * layer (routing metadata) and `createLeadAgent` (the actually assigned model)
17
+ * resolve the lead pool through this exact request so the reported lead always
18
+ * matches the assigned one.
19
+ */
20
+ export function leadResolveRequest(budget) {
21
+ return {
22
+ budget,
23
+ allowPaid: budget === "quality" || budget === "ebobo",
24
+ preferredCosts: budget === "balanced"
25
+ ? ["subscription"]
26
+ : budget === "eco"
27
+ ? ["free"]
28
+ : [],
29
+ preferredTiers: budget === "balanced"
30
+ ? ["lead"]
31
+ : budget === "quality" || budget === "ebobo"
32
+ ? ["frontier", "lead"]
33
+ : [],
34
+ };
35
+ }
2
36
  export function normalizeCandidate(input) {
3
37
  if (typeof input === "string") {
4
38
  return {
@@ -22,6 +56,34 @@ export function normalizeCandidate(input) {
22
56
  ...(input.priceOutput !== undefined ? { priceOutput: input.priceOutput } : {}),
23
57
  };
24
58
  }
59
+ /**
60
+ * Shared capability scoring helpers. `resolveModel` uses these for scoring and
61
+ * `buildFallbackChain` uses them to filter and rank fallback alternatives.
62
+ */
63
+ export function hasDeclaredCapability(candidate, capability) {
64
+ return candidate.capabilities.includes(capability);
65
+ }
66
+ export function hasExplicitScore(candidate, capability) {
67
+ return (candidate.scores[capability] ?? 0) > 0;
68
+ }
69
+ /**
70
+ * A candidate is compatible when it either declares the capability or carries
71
+ * an explicit positive score for it.
72
+ */
73
+ export function isCapabilityCompatible(candidate, capability) {
74
+ return hasDeclaredCapability(candidate, capability) || hasExplicitScore(candidate, capability);
75
+ }
76
+ /**
77
+ * A candidate is explicitly incompatible when it declares *some* capabilities,
78
+ * does not declare the required one, and has no explicit score for it. Unknown
79
+ * candidates (empty capabilities) are NOT incompatible: they stay eligible but
80
+ * rank at lower confidence.
81
+ */
82
+ export function isCapabilityIncompatible(candidate, capability) {
83
+ return candidate.capabilities.length > 0
84
+ && !hasDeclaredCapability(candidate, capability)
85
+ && !hasExplicitScore(candidate, capability);
86
+ }
25
87
  const COST_ORDER = { paid: 2, subscription: 1, free: 0 };
26
88
  function compareCodepoints(a, b) {
27
89
  return a < b ? -1 : a > b ? 1 : 0;
@@ -42,7 +104,7 @@ export function resolveModel(request) {
42
104
  .filter((candidate) => candidate.cost !== "paid" || request.maxPaidCalls === undefined || (request.paidCallsUsed ?? 0) < request.maxPaidCalls)
43
105
  .map((candidate) => {
44
106
  const explicit = candidate.scores[request.capability] ?? 0;
45
- const declared = candidate.capabilities.includes(request.capability) ? 18 : 0;
107
+ const declared = hasDeclaredCapability(candidate, request.capability) ? 18 : 0;
46
108
  const contextBonus = candidate.context === "xlarge" ? 8 : candidate.context === "large" ? 4 : 0;
47
109
  const frontierBonus = request.budget === "ebobo"
48
110
  ? candidate.tier === "frontier"
@@ -67,7 +129,17 @@ export function resolveModel(request) {
67
129
  ...(preferredTierBonus ? ["preferred-tier=" + preferredTierBonus] : []),
68
130
  ...(priceAdj !== 0 ? ["price=" + priceAdj] : []),
69
131
  ];
70
- return { candidate, score, reason };
132
+ return {
133
+ candidate,
134
+ score,
135
+ reason,
136
+ explicit,
137
+ declared,
138
+ frontierBonus,
139
+ preferredCostBonus,
140
+ preferredTierBonus,
141
+ priceAdj,
142
+ };
71
143
  })
72
144
  .sort((a, b) => b.score - a.score || compareCodepoints(a.candidate.id, b.candidate.id));
73
145
  const winner = ranked[0];
@@ -77,6 +149,41 @@ export function resolveModel(request) {
77
149
  .slice(1)
78
150
  .sort((a, b) => COST_ORDER[a.candidate.cost] - COST_ORDER[b.candidate.cost] || a.score - b.score || compareCodepoints(a.candidate.id, b.candidate.id))
79
151
  .map((item) => item.candidate.id);
152
+ const matchedCapabilities = winner.explicit || winner.declared ? [request.capability] : [];
153
+ // Deterministic reason code by dominant scoring signal, strongest first.
154
+ let code;
155
+ let component;
156
+ if (winner.frontierBonus > 0) {
157
+ code = "frontier";
158
+ component = "frontier=" + winner.frontierBonus;
159
+ }
160
+ else if (winner.preferredTierBonus > 0) {
161
+ code = "preferred_tier";
162
+ component = "tier=" + winner.candidate.tier;
163
+ }
164
+ else if (winner.preferredCostBonus > 0) {
165
+ code = "preferred_cost";
166
+ component = "cost=" + winner.candidate.cost;
167
+ }
168
+ else if (winner.explicit || winner.declared) {
169
+ code = "capability_match";
170
+ component = "capability=" + (winner.explicit ? "explicit" : "declared");
171
+ }
172
+ else if (winner.priceAdj !== 0) {
173
+ code = "price";
174
+ component = "price=" + winner.priceAdj;
175
+ }
176
+ else {
177
+ code = "priority";
178
+ component = "priority=" + winner.candidate.priority;
179
+ }
180
+ const routingReason = {
181
+ code,
182
+ text: boundReasonText(`id=${winner.candidate.id} cost=${winner.candidate.cost} ${component} score=${winner.score}`),
183
+ matchedCapabilities,
184
+ score: winner.score,
185
+ budget: request.budget,
186
+ };
80
187
  return {
81
188
  id: winner.candidate.id,
82
189
  score: winner.score,
@@ -85,6 +192,7 @@ export function resolveModel(request) {
85
192
  ...(winner.candidate.priceOutput !== undefined ? { priceOutput: winner.candidate.priceOutput } : {}),
86
193
  reason: winner.reason,
87
194
  fallback,
195
+ routingReason,
88
196
  };
89
197
  }
90
198
  //# sourceMappingURL=model-resolver.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"model-resolver.js","sourceRoot":"","sources":["../../src/routing/model-resolver.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"model-resolver.js","sourceRoot":"","sources":["../../src/routing/model-resolver.ts"],"names":[],"mappings":"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"}
package/dist/spawn.d.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  export type SpawnSyncOptions = NonNullable<Parameters<typeof spawnSync>[2]>;
3
+ type SpawnSyncDependency = (command: string, args: string[], options: SpawnSyncOptions) => ReturnType<typeof spawnSync>;
4
+ export interface SpawnFallbackDependencies {
5
+ platform?: NodeJS.Platform;
6
+ spawnSync?: SpawnSyncDependency;
7
+ }
3
8
  /**
4
9
  * Quote a command line for cmd.exe: parts containing spaces or cmd
5
10
  * metacharacters are wrapped in double quotes so paths with spaces and
@@ -16,10 +21,18 @@ export declare function safeForCmdRetry(parts: string[]): boolean;
16
21
  * with EINVAL on CVE-2024-27980-patched Node). The retry is skipped for
17
22
  * inputs cmd.exe could reinterpret — see {@link safeForCmdRetry} — and
18
23
  * off-Windows the native result is always returned untouched.
24
+ *
25
+ * When a bare command name resolves to a `.cmd`/`.bat` shim ahead of any
26
+ * spawnable executable on Windows, the direct spawn is skipped entirely and
27
+ * the command runs through cmd.exe, mirroring cmd.exe's own resolution order.
28
+ * This keeps PATH-based shims authoritative even when a real `.exe` of the
29
+ * same name sits further down PATH (patched Node would otherwise bypass the
30
+ * shim and run the real tool).
19
31
  */
20
- export declare function spawnWithCmdFallback(command: string, args: string[], options?: SpawnSyncOptions): ReturnType<typeof spawnSync>;
32
+ export declare function spawnWithCmdFallback(command: string, args: string[], options?: SpawnSyncOptions, deps?: SpawnFallbackDependencies): ReturnType<typeof spawnSync>;
21
33
  /**
22
34
  * User home for tool discovery. Windows tests override HOME to shadow the
23
35
  * real per-user toolchain, so honor it before falling back to os.homedir().
24
36
  */
25
37
  export declare function homeDirectory(): string;
38
+ export {};
package/dist/spawn.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { statSync } from "node:fs";
2
3
  import os from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
3
6
  /**
4
7
  * Quote a command line for cmd.exe: parts containing spaces or cmd
5
8
  * metacharacters are wrapped in double quotes so paths with spaces and
@@ -26,17 +29,77 @@ const UNSAFE_FOR_CMD_RETRY = /["%\r\n;,!]/;
26
29
  export function safeForCmdRetry(parts) {
27
30
  return !parts.some((part) => UNSAFE_FOR_CMD_RETRY.test(part));
28
31
  }
32
+ /** cmd.exe's default executable search order within a directory. */
33
+ const DEFAULT_PATHEXT = [".COM", ".EXE", ".BAT", ".CMD"];
34
+ /**
35
+ * Mirror cmd.exe's PATH resolution for a bare command name: scan the current
36
+ * directory and then PATH in order, trying PATHEXT extensions within each
37
+ * directory, and return the first existing file. Patched Windows Node skips
38
+ * `.cmd`/`.bat` candidates when resolving direct spawns, so a `.cmd` shim in
39
+ * an earlier PATH entry would otherwise be silently shadowed by a real `.exe`
40
+ * further down PATH. Knowing which candidate wins lets us route `.cmd`/`.bat`
41
+ * wins through the cmd.exe retry deterministically.
42
+ */
43
+ function resolveCommandCandidate(command, env, cwd) {
44
+ const pathext = (env.PATHEXT ?? DEFAULT_PATHEXT.join(";"))
45
+ .split(";")
46
+ .map((extension) => extension.trim())
47
+ .filter((extension) => extension.length > 0)
48
+ .map((extension) => (extension.startsWith(".") ? extension : "." + extension));
49
+ const directories = [cwd, ...(env.PATH ?? "").split(";").map((directory) => directory.replace(/^"|"$/g, ""))].filter((directory) => directory.length > 0);
50
+ for (const directory of directories) {
51
+ for (const extension of pathext) {
52
+ const candidate = path.join(directory, command + extension);
53
+ try {
54
+ if (statSync(candidate).isFile())
55
+ return candidate;
56
+ }
57
+ catch {
58
+ // Not a file; try the next extension.
59
+ }
60
+ }
61
+ }
62
+ return undefined;
63
+ }
29
64
  /**
30
65
  * Run a command natively, retrying through cmd.exe when Windows cannot
31
66
  * spawn the target directly (`.cmd`/`.bat` shims and shell scripts fail
32
67
  * with EINVAL on CVE-2024-27980-patched Node). The retry is skipped for
33
68
  * inputs cmd.exe could reinterpret — see {@link safeForCmdRetry} — and
34
69
  * off-Windows the native result is always returned untouched.
70
+ *
71
+ * When a bare command name resolves to a `.cmd`/`.bat` shim ahead of any
72
+ * spawnable executable on Windows, the direct spawn is skipped entirely and
73
+ * the command runs through cmd.exe, mirroring cmd.exe's own resolution order.
74
+ * This keeps PATH-based shims authoritative even when a real `.exe` of the
75
+ * same name sits further down PATH (patched Node would otherwise bypass the
76
+ * shim and run the real tool).
35
77
  */
36
- export function spawnWithCmdFallback(command, args, options = {}) {
37
- const direct = spawnSync(command, args, options);
38
- if (process.platform === "win32" && direct.error !== undefined && safeForCmdRetry([command, ...args])) {
39
- const retry = spawnSync(quoteLineForCmd([command, ...args]), [], { ...options, shell: true });
78
+ export function spawnWithCmdFallback(command, args, options = {}, deps = {}) {
79
+ const runSpawn = deps.spawnSync || spawnSync;
80
+ const platform = deps.platform || process.platform;
81
+ const isBareName = !command.includes("\\") && !command.includes("/") && path.extname(command).length === 0;
82
+ let shellAttempt;
83
+ if (platform === "win32" && isBareName && safeForCmdRetry([command, ...args])) {
84
+ const env = options.env ?? process.env;
85
+ const cwd = typeof options.cwd === "string"
86
+ ? options.cwd
87
+ : options.cwd instanceof URL
88
+ ? fileURLToPath(options.cwd)
89
+ : process.cwd();
90
+ const candidate = resolveCommandCandidate(command, env, cwd);
91
+ if (candidate !== undefined && /\.(cmd|bat)$/i.test(candidate)) {
92
+ shellAttempt = runSpawn(quoteLineForCmd([command, ...args]), [], { ...options, shell: true });
93
+ if (shellAttempt.error === undefined)
94
+ return shellAttempt;
95
+ }
96
+ }
97
+ const direct = runSpawn(command, args, options);
98
+ // When the shim already ran through cmd.exe above and failed to SPAWN, do
99
+ // not shell out a second time — the shim could execute twice, doubling its
100
+ // side effects. Fail closed with the direct-spawn result instead.
101
+ if (platform === "win32" && direct.error !== undefined && shellAttempt === undefined && safeForCmdRetry([command, ...args])) {
102
+ const retry = runSpawn(quoteLineForCmd([command, ...args]), [], { ...options, shell: true });
40
103
  if (retry.error === undefined)
41
104
  return retry;
42
105
  }