Summary
RuvNet Brain's model router does not acquire newly available Claude models, and the Brain Console reduces the available inventory to one row per tier, silently hiding launchable models already present.
On the reproduced machine, Claude Code 2.1.220 advertises Opus 5 and has observed both claude-opus-5 and claude-fable-5. The active Brain 4.0.2 / knowledge bundle 4.0.3 catalog contains Fable 5 but not Opus 5.
Two related defects are visible:
- Opus 5 is absent because the managed catalog is static and an existing user's catalog is installed only once.
- Fable 5 is in the effective pool but hidden because the Console collapses each tier to one row. Every subscription-covered development model gets the same synthetic price;
bestPerTier() keeps the first equal-cost entry, and Opus 4.8 precedes Fable 5.
This is separate from #86. That issue concerns the packaged provider-key catalog (data/model-catalog.json); this issue concerns the user-level router candidate catalog (~/.claude/model-router/catalog.json) and its Console projection.
User-path reproduction
- Start with an existing Brain router profile created before Opus 5 became available.
- Update/restart Brain normally and run Claude Code 2.1.220 where Opus 5 is offered.
- Open the Brain Console and expand Who routes your work — and with what.
- Inspect the active catalog and Console API:
jq '.candidates[] | select(.provider == "anthropic") | .id' \
~/.claude/model-router/catalog.json
curl -fsS http://127.0.0.1:7411/api/state |
jq '.sections.savings.routerEngine |
{catalogSource, models: [.pool[] |
select(.provider == "anthropic") |
{id, tier, subscriptionCovered, marginalPerMTok}]}'
Observed catalog:
claude-haiku-4-5-20251001
claude-sonnet-5
claude-opus-4-8
claude-fable-5
Observed API:
catalogSource is catalog.
- Opus 4.8 and Fable 5 are both frontier, subscription-covered, and $0 marginal.
- Opus 5 is absent.
- The UI shows only Opus 4.8 for the development frontier bucket.
The local catalog is stamped 2026-07-16, but the current upstream template is still stamped 2026-07-12 and also lacks Opus 5. Deleting the local catalog would not solve the underlying problem.
Root cause
Static managed catalog
The template contains Opus 4.8 and Fable 5, but no Opus 5:
|
{ |
|
"_comment": "Model candidate catalog for model-router-engine.mjs. EDIT ME freely. Pricing is $/Mtok. 'verified' names the source+date a price was confirmed live; null pricing means UNKNOWN \u2014 the engine will NOT invent a cost for it (same honesty rule as route-cheap.mjs). 'harness' = which agent(s) can launch each model \u2014 EMPTY [] means landscape-only: known to exist, but no wired execution path yet, so the engine will never select it (see model-router-engine.mjs pool filter). Do not add a real execution path (an entry to route-cheap.mjs PRICING or a harness value) until dispatch is actually implemented and tested \u2014 an engine that 'chooses' a model it can't run is worse than one that doesn't know the model exists. 'subscription' = harness(es) under which this model is covered by a flat subscription (Claude Max, Codex/ChatGPT), so its marginal cost to the user is ~$0 \u2014 the default policy treats those as free and will NOT prefer a billed model over them (never spend where the subscription is free).", |
|
"updated": "2026-07-12 (template)", |
|
"candidates": [ |
|
{ |
|
"id": "claude-haiku-4-5-20251001", |
|
"provider": "anthropic", |
|
"harness": [ |
|
"claude-code" |
|
], |
|
"subscription": [ |
|
"claude-code" |
|
], |
|
"tier": "cheap", |
|
"costPerMTok": null, |
|
"verified": null, |
|
"note": "covered by Claude Max under Claude Code (marginal $0); API price unverified" |
|
}, |
|
{ |
|
"id": "claude-sonnet-5", |
|
"provider": "anthropic", |
|
"harness": [ |
|
"claude-code" |
|
], |
|
"subscription": [ |
|
"claude-code" |
|
], |
|
"tier": "mid", |
|
"costPerMTok": null, |
|
"verified": null |
|
}, |
|
{ |
|
"id": "claude-opus-4-8", |
|
"provider": "anthropic", |
|
"harness": [ |
|
"claude-code" |
|
], |
|
"subscription": [ |
|
"claude-code" |
|
], |
|
"tier": "frontier", |
|
"costPerMTok": { |
|
"in": 5, |
|
"out": 25 |
|
}, |
|
"verified": "2026-07-07 route-cheap.mjs FRONTIER (API price; $0 under Max)" |
|
}, |
|
{ |
|
"id": "claude-fable-5", |
|
"provider": "anthropic", |
|
"harness": [ |
|
"claude-code" |
|
], |
|
"subscription": [ |
|
"claude-code" |
|
], |
|
"tier": "frontier", |
|
"costPerMTok": null, |
|
"verified": "2026-07-12 Artificial Analysis leaderboard (WebSearch) \u2014 listed as top-intelligence tier alongside GPT-5.6 Sol", |
|
"note": "covered by Claude Max under Claude Code (marginal $0); API price unverified. Use sparingly per Stuart's directive \u2014 Opus 4.8 measures ~96% of comparable quality at far lower cost; Fable 5 is for tasks that specifically need its top-tier profile, not a default." |
|
}, |
|
{ |
The path has had one commit, when introduced on 2026-07-12:
5f80f3f
Existing users never receive managed additions
The installer copies catalog.template.json to ~/.claude/model-router/catalog.json only when the destination does not exist:
|
const routerDir = path.join(os.homedir(), '.claude', 'model-router'); |
|
const pkgRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); |
|
step( |
|
'MetaHarness model router — the right model for each task, cheapest first', |
|
"your subscription models are $0 marginal; the router just needs to know which ones YOU have", |
|
); |
|
|
|
fs.mkdirSync(path.join(routerDir, 'bin'), { recursive: true }); |
|
for (const [src, dst] of [['catalog.template.json', 'catalog.json'], ['policy.default.mjs', 'policy.default.mjs']]) { |
|
const s = path.join(pkgRoot, 'config', 'model-router', src); |
|
const d = path.join(routerDir, dst); |
|
if (fs.existsSync(s) && !fs.existsSync(d)) { fs.copyFileSync(s, d); ok(`installed ${dst} (edit freely — goldie keeps prices fresh where scheduled)`); } |
|
} |
The router reads that persistent file directly; its built-in fallback also pins Opus 4.8:
|
// Honest provenance of the catalog the engine is actually using, so no surface can pass the |
|
// built-in stub off as a real personal catalog (trust rule: never present a fallback as the thing). |
|
// Returns 'catalog' when a real ~/.claude/model-router/catalog.json is present + valid, else |
|
// 'built-in-fallback'. Same check loadCatalog() uses — kept in lockstep. |
|
export function catalogSource() { |
|
try { |
|
const j = JSON.parse(fs.readFileSync(CATALOG_PATH, 'utf8')); |
|
if (Array.isArray(j.candidates) && j.candidates.length) return 'catalog'; |
|
} catch { /* fall through */ } |
|
return 'built-in-fallback'; |
|
} |
|
|
|
export function loadCatalog() { |
|
try { |
|
const j = JSON.parse(fs.readFileSync(CATALOG_PATH, 'utf8')); |
|
if (Array.isArray(j.candidates) && j.candidates.length) return j.candidates; |
|
} catch { |
|
/* fall through to a minimal built-in so the engine still answers */ |
|
} |
|
// Built-in fallback (verified OpenRouter prices from route-cheap; Anthropic frontier from same). |
|
return [ |
|
{ id: 'deepseek/deepseek-chat', provider: 'openrouter', harness: ['claude-code', 'codex'], tier: 'cheap', costPerMTok: { in: 0.2, out: 0.8 }, verified: '2026-07-07' }, |
|
{ id: 'claude-opus-4-8', provider: 'anthropic', harness: ['claude-code'], tier: 'frontier', costPerMTok: { in: 5.0, out: 25.0 }, verified: '2026-07-07' }, |
|
{ id: 'gpt-5.5', provider: 'openai', harness: ['codex'], tier: 'frontier', costPerMTok: null, verified: null }, |
|
]; |
A normal Brain lifecycle update therefore cannot add a new managed candidate to an existing user catalog.
Goldie cannot repair Claude candidates
Goldie explicitly updates prices and verification stamps. Its deterministic refresh handles existing OpenRouter entries and has narrow Codex cache logic, but no Claude/Anthropic discovery or managed-catalog merge:
|
#!/usr/bin/env node |
|
// goldie-research.mjs — Goldie's DETERMINISTIC core: keep the model-router catalog verifiably fresh. |
|
// |
|
// Stuart's standing mandate (2026-07-12): the router must never run on stale beliefs about the model |
|
// landscape. Goldie runs WEEKLY (scripts/goldie-weekly.sh via launchd) and answers, with live data: |
|
// • what do the router's candidate models ACTUALLY cost right now (OpenRouter /models, public API)? |
|
// • did any price drift >20% since the catalog was last verified (ruflo ADR-149's own re-measure |
|
// trigger — that threshold is rUv's, not invented here)? |
|
// • which Codex tiers exist on this machine right now (~/.codex/models_cache.json, fetched live |
|
// by Codex itself)? |
|
// • which cheap, tool-capable OpenRouter models exist that we DON'T track (the "worth a look" radar)? |
|
// |
|
// It updates ONLY prices + verified-stamps in ~/.claude/model-router/catalog.json. It NEVER adds an |
|
// execution path (harness/subscription fields) — the catalog's own rule: an engine that "chooses" a |
|
// model it can't run is worse than one that doesn't know it exists. New-model adoption and bucket |
|
// taxonomy are JUDGMENT calls: goldie-weekly.sh layers a headless-Claude research pass on top, and |
|
// its output lands in the same brief as a PROPOSAL for a human/session to apply. |
|
function refreshCatalogPrices(catalog, orModels) { |
|
const byId = new Map(orModels.map((m) => [m.id, m])); |
|
const changes = []; |
|
for (const c of catalog.candidates) { |
|
if (c.provider !== 'openrouter') continue; |
|
const live = byId.get(c.id); |
|
if (!live) { changes.push({ id: c.id, note: 'NOT FOUND on OpenRouter anymore — investigate before next route' }); continue; } |
|
const fresh = { in: perMTok(live.pricing?.prompt), out: perMTok(live.pricing?.completion) }; |
|
if (fresh.in == null || fresh.out == null) { changes.push({ id: c.id, note: 'listed but pricing unparsable — left as-is' }); continue; } |
|
const old = c.costPerMTok; |
|
if (old && typeof old.out === 'number') { |
|
const drift = Math.max(Math.abs(fresh.in - old.in) / old.in, Math.abs(fresh.out - old.out) / old.out); |
|
if (drift > DRIFT_THRESHOLD) changes.push({ id: c.id, note: `PRICE DRIFT ${(drift * 100).toFixed(0)}%: in $${old.in}->$${fresh.in}, out $${old.out}->$${fresh.out} /MTok (>${DRIFT_THRESHOLD * 100}% — ADR-149 says re-measure quality/cost now)` }); |
|
else if (fresh.in !== old.in || fresh.out !== old.out) changes.push({ id: c.id, note: `price updated: in $${old.in}->$${fresh.in}, out $${old.out}->$${fresh.out} /MTok` }); |
|
} |
|
c.costPerMTok = fresh; |
|
c.verified = `${TODAY} OpenRouter API (goldie)`; |
|
} |
|
catalog.updated = TODAY; |
|
return changes; |
|
function codexTiers() { |
|
try { |
|
const cache = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.codex', 'models_cache.json'), 'utf8')); |
|
const models = cache.models || cache.data || []; |
|
const names = models.map((m) => m.id || m.slug || m.name).filter(Boolean); |
|
// `models` (below) is truncated to 20 for the brief's display list; `allModels` is the FULL set, |
|
// kept separately so the gpt-5.6 reinstate check (reinstateGpt56, below) can never miss a real |
|
// slug just because it landed past position #20 in the cache. |
|
return { fetchedAt: cache.fetched_at || cache.fetchedAt || 'unknown', models: names.slice(0, 20), allModels: names }; |
|
} catch { return null; } |
|
} |
|
|
|
// GPT-5.6 auto-reinstate (Stuart's mandate, 2026-07-12): gpt-5.6-sol/terra/luna were demoted to |
|
// landscape-only THAT SAME DAY because their "verified live" stamps were false — two independent |
|
// reads of models_cache.json showed only gpt-5.5/5.4/5.4-mini/codex-auto-review. The demotion note |
|
// on each entry says: reinstate ONLY when a fresh read of models_cache.json actually shows the slug. |
|
// This is that fresh read, automated, every week Goldie runs — no more relying on a human to notice. |
|
// |
|
// Inverse direction (any 'codex' harness entry whose gpt-* id vanishes from this week's cache) is |
|
// FLAG-ONLY, never auto-demote: a cache miss could be rollout timing or a transient cache staleness, |
|
// not proof the model is gone — the same false-confidence mistake in reverse. Demotion stays a |
|
// judgment call for a human/Goldie's judgment pass, same as the 2026-07-12 demotion itself was. |
|
function reinstateGpt56(catalog, codex) { |
|
const result = { reinstated: [], warnings: [] }; |
|
if (!codex) return result; // no live cache this run (codexTiers() returned null) — skip both checks silently |
|
const live = new Set(codex.allModels); |
|
for (const c of catalog.candidates) { |
|
if (c.id.startsWith('gpt-5.6') && Array.isArray(c.harness) && c.harness.length === 0 && live.has(c.id)) { |
|
c.harness = ['codex']; |
|
c.subscription = ['codex']; |
|
c.verified = `${TODAY} ~/.codex/models_cache.json live (goldie auto-reinstate)`; |
|
c.note = `auto-reinstated by goldie on ${TODAY}; was demoted for false verification 2026-07-12`; |
|
result.reinstated.push(c.id); |
|
} |
|
if (c.id.startsWith('gpt-') && Array.isArray(c.harness) && c.harness.includes('codex') && !live.has(c.id)) { |
|
result.warnings.push(c.id); |
|
} |
|
} |
|
return result; |
|
} |
One-row-per-tier hides Fable 5
The Console reduces each tier to one row. For development, every subscription-covered candidate is assigned -1; replacement happens only when the next price is strictly lower:
|
const TIER_ORDER = { mechanical: 0, cheap: 1, mid: 2, frontier: 3 }; |
|
const byTier = (a, b) => (TIER_ORDER[a.tier] ?? 9) - (TIER_ORDER[b.tier] ?? 9); |
|
const lensTable = (rows, costOf, costHead) => el('div', { class: 'scroll-x' }, |
|
el('table', { class: 'tb rp-tb' }, |
|
el('thead', {}, el('tr', {}, |
|
el('th', { scope: 'col' }, 'Bucket'), el('th', { scope: 'col' }, 'Model'), |
|
el('th', { scope: 'col' }, costHead))), |
|
el('tbody', {}, rows.map((p) => el('tr', {}, |
|
el('td', { class: 'rp-band' }, p.tier || '—'), |
|
el('td', {}, el('div', { class: 'rp-model' }, prettyModel(p.id))), |
|
el('td', { class: 'cell-mono num' }, costOf(p)), |
|
))))); |
|
// Development: only what this machine's harness can launch; best (cheapest-marginal) per bucket. |
|
const bestPerTier = (rows, price) => { |
|
const seen = {}; |
|
for (const p of rows) { |
|
const k = p.tier || '?'; |
|
if (!seen[k] || price(p) < price(seen[k])) seen[k] = p; |
|
} |
|
return Object.values(seen).sort(byTier); |
|
}; |
|
const devRows = bestPerTier( |
|
pool.filter((p) => (p.harness || []).includes('claude-code')), |
|
(p) => (p.subscriptionCovered ? -1 : p.marginalPerMTok ?? Infinity)); |
|
const prodRows = bestPerTier( |
|
pool.filter((p) => p.listPerMTok != null && p.provider !== 'local'), |
|
(p) => p.listPerMTok ?? Infinity); |
|
const devBlock = el('div', { class: 'rp-profile' }, |
|
el('div', { class: 'rp-head' }, |
|
el('span', { class: 'rp-name' }, 'Development'), |
|
el('span', { class: 'rp-obj' }, 'you, in Claude Code — models your plan covers win at $0 marginal')), |
|
lensTable(devRows, |
|
(p) => (p.subscriptionCovered ? el('b', { title: 'covered by your subscription — zero marginal cost' }, '$0 · yours') : money(p.marginalPerMTok)), |
|
'Your cost')); |
|
const prodBlock = el('div', { class: 'rp-profile' }, |
|
el('div', { class: 'rp-head' }, |
|
el('span', { class: 'rp-name' }, 'Production'), |
|
el('span', { class: 'rp-obj' }, 'your deployed app on metered APIs — a personal plan can’t apply there')), |
|
lensTable(prodRows, (p) => money(p.listPerMTok), 'API price')); |
|
const lensGrid = el('div', { class: 'rp-grid' }, devBlock, prodBlock); |
|
const poolFoot = el('p', { class: 'fineprint' }, |
|
re.catalogSource === 'built-in-fallback' |
|
? `No personal catalog found — showing a minimal built-in set of ${pool.length}. Run \`node scripts/model-router-setup.mjs\` to build your real catalog, then the engine weighs yours on every call.` |
|
: `Best pick per bucket shown; the engine weighs all ${pool.length} candidates in its catalog on every call — nothing is retired by being off this summary.`); |
Opus 4.8 and Fable 5 tie, so JSON order—not quality, recency, policy, or a routing decision—selects Opus 4.8. Yet the UI calls this the "Best pick per bucket."
Even a deterministic tie-break would be insufficient. A panel describing which models can route the user's work should show the complete launchable effective inventory, then mark any recommendation separately. On the observed host that means Haiku, Sonnet, Opus 5, and Fable 5. If Opus 4.8 remains launchable, it should remain visible as an older option.
Expected behavior
- Normal Brain updates bring managed model additions to existing users.
- User edits and subscription constraints survive upgrades.
- Managed updates never silently enable a metered provider or invent an executable path.
- Opus 5 appears when the host can launch it.
- The Console lists every launchable model from the effective catalog: Haiku, Sonnet, Opus, and Fable; older launchable versions remain visible and identified.
- The preferred/default model is a separate, explained routing marker; availability is not filtered by recommendation.
- Catalog source, freshness, and degraded/unknown states are visible.
Suggested implementation
Separate managed facts from user choices
Use three layers:
- A versioned managed base catalog shipped with Brain: identity, provider, proven harness capability, default tier metadata, provenance, and catalog/schema version.
- A user overlay: explicit enable/disable, priority/tier overrides, constraints, and local additions.
- The existing subscription profile: per-user availability and coverage.
Build the effective catalog by model ID. On normal install/session update, atomically replace the managed base and reapply the user overlay. Do not overwrite user choices or infer that a billed provider is enabled.
Migrate today's catalog.json by comparing it with the managed baseline and preserving real user differences in the overlay. Back up the original and make migration idempotent.
Refresh against the real host surface
Use a host-native Claude model inventory or stable alias where one is available and verifiable. If no authoritative machine-readable source exists, keep a reviewed managed base but expose its age and render stale/unknown instead of presenting it as current. Record the concrete model actually used in routing receipts.
Extend freshness checks beyond OpenRouter/Codex so a newly launchable Claude model creates either a safely managed candidate with proven reachability or a visible review proposal.
Show full inventory; mark recommendations separately
Show all launchable effective candidates, grouped by tier or provider. Mark the router's preferred/default model with a badge or column plus the policy-backed reason.
If a compact one-per-tier summary is useful, make it a separate Recommended view. Never use it to hide the available inventory.
For equal marginal cost, show all tied candidates or apply an explicit, tested priority/quality/recency rule. Recommendations must be invariant under candidate-array reordering.
Regression coverage / acceptance criteria
- Start from an old catalog that lacks a newly shipped managed Claude model and contains user overrides.
- Run the normal Brain lifecycle update.
- Assert the new managed candidate is present, all overrides survive, and no metered provider becomes enabled.
- Assert Opus 5 reaches the effective catalog,
/api/state, and Console when the host reports it launchable.
- Assert the Console inventory shows every launchable Claude model: Haiku, Sonnet, Opus, and Fable; older launchable versions remain visible and identified.
- Assert the recommended/default marker is separate from availability and carries a policy-backed reason.
- Reverse equal-cost frontier candidate order and assert the recommendation is unchanged.
- Assert ties are displayed or resolved by an explicit tested rule, never catalog order.
- Pack/install the release artifact and exercise the real Console user path, not only source-checkout unit tests.
- Add a release gate requiring reviewed catalog freshness or an explicit degraded-state test.
Version + health
RuvNet Brain: 4.0.2
Knowledge bundle: 4.0.3
Claude Code: 2.1.220
Platform: macOS 26.4 arm64
Upstream main checked: 924d29a8a5d7b5c09effdd21d75250ea15140c60
Brain Console API: healthy and returning router state
Summary
RuvNet Brain's model router does not acquire newly available Claude models, and the Brain Console reduces the available inventory to one row per tier, silently hiding launchable models already present.
On the reproduced machine, Claude Code 2.1.220 advertises Opus 5 and has observed both
claude-opus-5andclaude-fable-5. The active Brain 4.0.2 / knowledge bundle 4.0.3 catalog contains Fable 5 but not Opus 5.Two related defects are visible:
bestPerTier()keeps the first equal-cost entry, and Opus 4.8 precedes Fable 5.This is separate from #86. That issue concerns the packaged provider-key catalog (
data/model-catalog.json); this issue concerns the user-level router candidate catalog (~/.claude/model-router/catalog.json) and its Console projection.User-path reproduction
Observed catalog:
Observed API:
catalogSourceiscatalog.The local catalog is stamped 2026-07-16, but the current upstream template is still stamped 2026-07-12 and also lacks Opus 5. Deleting the local catalog would not solve the underlying problem.
Root cause
Static managed catalog
The template contains Opus 4.8 and Fable 5, but no Opus 5:
ruvnet-brain/config/model-router/catalog.template.json
Lines 1 to 62 in 924d29a
The path has had one commit, when introduced on 2026-07-12:
5f80f3f
Existing users never receive managed additions
The installer copies
catalog.template.jsonto~/.claude/model-router/catalog.jsononly when the destination does not exist:ruvnet-brain/bin/install.mjs
Lines 2882 to 2894 in 924d29a
The router reads that persistent file directly; its built-in fallback also pins Opus 4.8:
ruvnet-brain/scripts/model-router-engine.mjs
Lines 97 to 121 in 924d29a
A normal Brain lifecycle update therefore cannot add a new managed candidate to an existing user catalog.
Goldie cannot repair Claude candidates
Goldie explicitly updates prices and verification stamps. Its deterministic refresh handles existing OpenRouter entries and has narrow Codex cache logic, but no Claude/Anthropic discovery or managed-catalog merge:
ruvnet-brain/scripts/goldie-research.mjs
Lines 1 to 17 in 924d29a
ruvnet-brain/scripts/goldie-research.mjs
Lines 44 to 63 in 924d29a
ruvnet-brain/scripts/goldie-research.mjs
Lines 80 to 119 in 924d29a
One-row-per-tier hides Fable 5
The Console reduces each tier to one row. For development, every subscription-covered candidate is assigned
-1; replacement happens only when the next price is strictly lower:ruvnet-brain/console/app.js
Lines 2100 to 2143 in 924d29a
Opus 4.8 and Fable 5 tie, so JSON order—not quality, recency, policy, or a routing decision—selects Opus 4.8. Yet the UI calls this the "Best pick per bucket."
Even a deterministic tie-break would be insufficient. A panel describing which models can route the user's work should show the complete launchable effective inventory, then mark any recommendation separately. On the observed host that means Haiku, Sonnet, Opus 5, and Fable 5. If Opus 4.8 remains launchable, it should remain visible as an older option.
Expected behavior
Suggested implementation
Separate managed facts from user choices
Use three layers:
Build the effective catalog by model ID. On normal install/session update, atomically replace the managed base and reapply the user overlay. Do not overwrite user choices or infer that a billed provider is enabled.
Migrate today's
catalog.jsonby comparing it with the managed baseline and preserving real user differences in the overlay. Back up the original and make migration idempotent.Refresh against the real host surface
Use a host-native Claude model inventory or stable alias where one is available and verifiable. If no authoritative machine-readable source exists, keep a reviewed managed base but expose its age and render stale/unknown instead of presenting it as current. Record the concrete model actually used in routing receipts.
Extend freshness checks beyond OpenRouter/Codex so a newly launchable Claude model creates either a safely managed candidate with proven reachability or a visible review proposal.
Show full inventory; mark recommendations separately
Show all launchable effective candidates, grouped by tier or provider. Mark the router's preferred/default model with a badge or column plus the policy-backed reason.
If a compact one-per-tier summary is useful, make it a separate Recommended view. Never use it to hide the available inventory.
For equal marginal cost, show all tied candidates or apply an explicit, tested priority/quality/recency rule. Recommendations must be invariant under candidate-array reordering.
Regression coverage / acceptance criteria
/api/state, and Console when the host reports it launchable.Version + health