Model Resolver — 5-Layer Cross-Platform Model Resolution
Translates agent-author intent (model_tier: deep) into concrete platform-specific model strings (claude-opus-4-8 on CC, opencode-go/kimi-k2.6 on OC, gpt-5.5 on Codex). Preloaded by running-a-pipeline Phase 0.45. Pure function — never writes to disk, never calls APIs.
Public API
RESOLVE(agent_frontmatter, profile, prefs) → resolved
LOAD_PREFS(workspace_root) → { workspace, user }
EMIT(resolved, target_format) → string
RENDER_RESOLUTION_TABLE(entries[]) → string
REVERSE_MAP(concrete_model, profile) → tier_id | null
DETECT_CATALOG_DRIFT(prefs, profile) → { drifted: bool, message: string | null }
Where each entries[] element is { step_id, agent_name, model_tier, resolved }.
resolved object schema
{
"model": "claude-opus-4-8",
"effort": "high",
"effort_field_name": null,
"model_field_format": "shorthand",
"source": "user_prefs",
"warnings": []
}
model: null means "omit the field; let the host pick" (Tier 2, dynamic-subagent platforms, or inherit tier).
Algorithm
Normative source: references/resolution-algorithm.md — all six operations (RESOLVE, LOAD_PREFS, EMIT, RENDER_RESOLUTION_TABLE, REVERSE_MAP, DETECT_CATALOG_DRIFT) are specified there. Both the skill adapter (this file) and the inline adapter (running-a-pipeline Phase 0.45) execute the same algorithm. Do not restate steps here.
Preference File Schema
{
"platforms": {
"tier_1": {
"name": "Claude Code",
"model_tiers_version_acked": "2026-05-19",
"tiers": {
"triage": "claude-haiku-4-5-20251001",
"fast": "claude-haiku-4-5-20251001",
"medium": "claude-sonnet-4-6",
"deep": "claude-sonnet-4-6"
},
"effort_default": {
"deep": "high"
}
},
"tier_1b": {
"name": "OpenCode",
"model_tiers_version_acked": "2026-05-19",
"tiers": { "fast": "opencode-go/deepseek-v4-flash" }
}
}
}
Key rules:
tier_1, tier_1b, tier_1c, tier_1d, tier_2 are the canonical keys — never localize them. They map directly to platform_profile.tier.
name is a display-only sibling field. Writers (wizard/change-models) MUST stamp it from platform_profile.name so future readers (and users browsing the JSON) can identify the platform without consulting profile JSONs. Readers (resolver) MUST NOT branch on name — it has no effect on resolution.
tiers holds the four-tier model overrides (triage | fast | medium | deep). Partial maps allowed; missing keys fall through to profile default.
model_tiers_version_acked mirrors profile.model_tiers_version at write-time; drift detector compares to current.
effort_default is optional; per-tier effort override.
Red Flags — STOP
- "I'll cache the resolved model across runs." → STOP. State file caches per run; mid-run pref edits must take effect on the next run, not be stale-cached.
- "I'll re-resolve at dispatch time per step." → STOP. Phase 0.45 resolves once upfront and writes to state. Phase 3 reads state. Re-resolution at dispatch causes mid-run inconsistency if prefs change.
- "I'll merge workspace and user prefs into one object." → STOP. Consult-in-order preserves provenance (which layer won); merging loses the
source field.
- "I'll call platform APIs to validate the resolved model exists." → STOP. Resolver is pure. Validation belongs in
change-models Phase 4.
Reference Files
references/resolution-algorithm.md — Normative algorithm spec (RESOLVE, LOAD_PREFS, EMIT, RENDER_RESOLUTION_TABLE, REVERSE_MAP, DETECT_CATALOG_DRIFT) + worked examples.
references/emit-formats.md — Per-platform serialization specs with byte-for-byte examples.
fixtures/ — Input/expected fixture pairs for each branch of the algorithm.
sk-platform-dispatch/profiles/{tier}.json — Profile defaults, single source of truth.
sk-pipeline-state/SKILL.md — State file schema (metadata.resolved_models).
1---2name: sk-model-resolver3description: Resolves agent `model_tier:` declarations into concrete platform-specific model strings and effort values via a 5-layer precedence chain, returning a serializable `resolved` object for dispatch. Use when `running-a-pipeline` Phase 0.45 needs to determine the concrete model + effort for each pipeline step on the active platform before dispatch.4---56# Model Resolver — 5-Layer Cross-Platform Model Resolution78> Translates agent-author intent (`model_tier: deep`) into concrete platform-specific model strings (`claude-opus-4-8` on CC, `opencode-go/kimi-k2.6` on OC, `gpt-5.5` on Codex). Preloaded by `running-a-pipeline` Phase 0.45. Pure function — never writes to disk, never calls APIs.910<overview>11The resolver decouples agent-author intent from runtime model selection. Five resolution layers ranked top-wins: (1) explicit `model:` in agent frontmatter, (2) workspace preferences, (3) user-global preferences, (4) platform profile default, (5) native host inherit. Callers obtain a `resolved` object, persist it to `pipeline-state.json metadata.resolved_models`, and pass it to dispatch.12</overview>1314<glossary>15 <term name="tier">Role category: triage | fast | medium | deep | inherit. Author-declared.</term>16 <term name="effort_tier">Orthogonal reasoning intensity: low | medium | high. Optional.</term>17 <term name="resolved">Output object: {model, effort, effort_field_name, model_field_format, source, warnings}.</term>18 <term name="source">Which layer won resolution: frontmatter_override | workspace_prefs | user_prefs | profile_default | host_inherit | blocked. The `blocked` enum (Q4) signals v1-legacy schema detected — callers MUST migrate via Phase 0.4 before dispatch.</term>19</glossary>2021## Public API2223```24RESOLVE(agent_frontmatter, profile, prefs) → resolved25LOAD_PREFS(workspace_root) → { workspace, user }26EMIT(resolved, target_format) → string27RENDER_RESOLUTION_TABLE(entries[]) → string28REVERSE_MAP(concrete_model, profile) → tier_id | null29DETECT_CATALOG_DRIFT(prefs, profile) → { drifted: bool, message: string | null }30```3132Where each `entries[]` element is `{ step_id, agent_name, model_tier, resolved }`.3334### resolved object schema3536```json37{38 "model": "claude-opus-4-8",39 "effort": "high",40 "effort_field_name": null,41 "model_field_format": "shorthand",42 "source": "user_prefs",43 "warnings": []44}45```4647`model: null` means "omit the field; let the host pick" (Tier 2, dynamic-subagent platforms, or `inherit` tier).4849## Algorithm5051Normative source: **`references/resolution-algorithm.md`** — all six operations (RESOLVE, LOAD_PREFS, EMIT, RENDER_RESOLUTION_TABLE, REVERSE_MAP, DETECT_CATALOG_DRIFT) are specified there. Both the skill adapter (this file) and the inline adapter (`running-a-pipeline` Phase 0.45) execute the same algorithm. Do not restate steps here.5253## Preference File Schema5455```json56{57 "platforms": {58 "tier_1": {59 "name": "Claude Code",60 "model_tiers_version_acked": "2026-05-19",61 "tiers": {62 "triage": "claude-haiku-4-5-20251001",63 "fast": "claude-haiku-4-5-20251001",64 "medium": "claude-sonnet-4-6",65 "deep": "claude-sonnet-4-6"66 },67 "effort_default": {68 "deep": "high"69 }70 },71 "tier_1b": {72 "name": "OpenCode",73 "model_tiers_version_acked": "2026-05-19",74 "tiers": { "fast": "opencode-go/deepseek-v4-flash" }75 }76 }77}78```7980**Key rules:**81- `tier_1`, `tier_1b`, `tier_1c`, `tier_1d`, `tier_2` are the **canonical keys** — never localize them. They map directly to `platform_profile.tier`.82- `name` is a **display-only** sibling field. Writers (wizard/change-models) MUST stamp it from `platform_profile.name` so future readers (and users browsing the JSON) can identify the platform without consulting profile JSONs. Readers (resolver) MUST NOT branch on `name` — it has no effect on resolution.83- `tiers` holds the four-tier model overrides (`triage | fast | medium | deep`). Partial maps allowed; missing keys fall through to profile default.84- `model_tiers_version_acked` mirrors `profile.model_tiers_version` at write-time; drift detector compares to current.85- `effort_default` is optional; per-tier effort override.868788<invariants>89- Resolver NEVER writes to disk. All preference writes flow through `change-models`.90- Resolver NEVER calls platform APIs. Pure function of inputs.91- Resolver MUST return a `warnings` array (possibly empty); callers MUST surface to user-facing output.92- `frontmatter_override` is the only escape hatch — preserved for advanced users; surfaced in audit reports as SEV-3 info.93- Resolver MUST NOT mutate inputs. Outputs are fresh objects.94- DETECT_CATALOG_DRIFT MUST NOT emit advisory when prefs are empty for the tier — the wizard handles first-run setup separately.95</invariants>9697## Red Flags — STOP9899- "I'll cache the resolved model across runs." → **STOP**. State file caches per run; mid-run pref edits must take effect on the next run, not be stale-cached.100- "I'll re-resolve at dispatch time per step." → **STOP**. Phase 0.45 resolves once upfront and writes to state. Phase 3 reads state. Re-resolution at dispatch causes mid-run inconsistency if prefs change.101- "I'll merge workspace and user prefs into one object." → **STOP**. Consult-in-order preserves provenance (which layer won); merging loses the `source` field.102- "I'll call platform APIs to validate the resolved model exists." → **STOP**. Resolver is pure. Validation belongs in `change-models` Phase 4.103104## Reference Files105106- `references/resolution-algorithm.md` — **Normative algorithm spec** (RESOLVE, LOAD_PREFS, EMIT, RENDER_RESOLUTION_TABLE, REVERSE_MAP, DETECT_CATALOG_DRIFT) + worked examples.107- `references/emit-formats.md` — Per-platform serialization specs with byte-for-byte examples.108- `fixtures/` — Input/expected fixture pairs for each branch of the algorithm.109- `sk-platform-dispatch/profiles/{tier}.json` — Profile defaults, single source of truth.110- `sk-pipeline-state/SKILL.md` — State file schema (`metadata.resolved_models`).