You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Model rosters churn faster than config. Three surfaces currently bake the concrete roster into sequant: the values users write into run.phases.<phase>.model (#914), the proposed run.modelLadder (#971, currently exampled as ["sonnet","opus","fable"]), and the stale agents.model enum + "opus = most capable" doc (settings.ts:389, settings.ts:52 — both already wrong post-Fable). Every roster change invalidates user settings, skill docs, and memory files that name models directly.
The fix is one level of indirection: config expresses roles; a single map resolves roles to model strings; everything else (phase policy, ladder, benchmark policies) references roles and survives roster changes untouched.
Phase-0 findings this design builds on (model-string contract spike, 2026-08-28, Claude Code 2.1.251)
Family aliases already track releases: --model sonnet resolved to claude-sonnet-5, haiku → claude-haiku-4-5-20251001, fable → claude-fable-5 (observed via modelUsage keys). Shipped role defaults should therefore be aliases, never dated IDs — point releases then need no map update at all.
The resolved concrete model is observable per run in the SDK result's modelUsage keys — so metrics can record ground truth without sequant maintaining a roster.
Opportunistic cleanup in scope: drop the agents.model enum to a free string (it is inert per claude-code#43869 but currently rejects valid current models), and fix the capability-rank language at settings.ts:52.
AC-2: a role with no map entry fails at config-resolution time with the role name and available roles in the message, before any agent session is spawned, verified by a unit test
AC-4: run metrics and the phase marker record the requested value and the resolved concrete model ID from modelUsage for each phase execution, verified by a unit test on the metrics writer
AC-6: agents.model accepts a free string with the inert-per-#43869 note retained, and settings.ts docs no longer rank family capability, verified by settings schema tests
Done when
A roster change (new family, renamed tier) requires editing only the modelRoles map — no change to phase policy, ladder config, skills, or docs — and a stale map fails loudly instead of silently succeeding.
Blocked by: #974 (role shape spike), #973 (driver is_error fix — supplies the loud-failure contract)
Why
Model rosters churn faster than config. Three surfaces currently bake the concrete roster into sequant: the values users write into
run.phases.<phase>.model(#914), the proposedrun.modelLadder(#971, currently exampled as["sonnet","opus","fable"]), and the staleagents.modelenum + "opus = most capable" doc (settings.ts:389,settings.ts:52— both already wrong post-Fable). Every roster change invalidates user settings, skill docs, and memory files that name models directly.The fix is one level of indirection: config expresses roles; a single map resolves roles to model strings; everything else (phase policy, ladder, benchmark policies) references roles and survives roster changes untouched.
Phase-0 findings this design builds on (model-string contract spike, 2026-08-28, Claude Code 2.1.251)
--model sonnetresolved toclaude-sonnet-5,haiku→claude-haiku-4-5-20251001,fable→claude-fable-5(observed viamodelUsagekeys). Shipped role defaults should therefore be aliases, never dated IDs — point releases then need no map update at all.unrecognized_model), nothing runs on a wrong model. But the failure is currently swallowed by the driver (resultsubtype:"success"+is_error:true→ reported as phase success, fix(driver): result subtype "success" with is_error:true is reported as phase success — unrecognized model yields a zero-work "successful" phase #973). With fix(driver): result subtype "success" with is_error:true is reported as phase success — unrecognized model yields a zero-work "successful" phase #973 fixed, a bad resolution fails loudly at dispatch, which means the role map can stay thin — no roster-validation layer in sequant.modelUsagekeys — so metrics can record ground truth without sequant maintaining a roster.Design
run.modelRoles: the map (exact schema per spike(config): model-role shape — flat map vs per-driver map, role vocabulary (gates the modelRoles feature) #974's recommendation — flat vs per-driver is that spike's decision). Role names per spike(config): model-role shape — flat map vs per-driver map, role vocabulary (gates the modelRoles feature) #974's vocabulary.config-resolver.ts), applied identically by both ExecutionConfig producers (fix(cli): bare parseInt on run --timeout lets a malformed value abort every phase instantly (sibling of #818) #833 drift guard). Precedence unchanged: CLI > settings > absent. A raw model string in any surface that expects a role-or-model remains legal and passes through verbatim — roles are additive, not a migration.modelUsage) — this is what makes cross-time benchmark comparisons (bench: replay outcome-distribution smoke for roster/policy changes (descoped from A/B ranking — phase-0) #916/feat(run): adopt cheaper per-phase model/effort defaults from #916 benchmark results — testgen, docs, assess candidates #944) meaningful as the roster moves under the aliases.run.modelLadderentries are defined over the same role-or-model strings, so the ladder survives roster changes.agents.modelenum to a free string (it is inert per claude-code#43869 but currently rejects valid current models), and fix the capability-rank language atsettings.ts:52.Not in scope
/upstreammodel-roster watch (separate small follow-up once this lands).Acceptance Criteria
run.modelRolesparses per spike(config): model-role shape — flat map vs per-driver map, role vocabulary (gates the modelRoles feature) #974's schema; a phase policy or ladder entry naming a role dispatches with the mapped model string, verified by a driver-options unit test at the resolverrun.phases.<phase>.model,--models, and ladder entries passes through verbatim exactly as feat(run): per-phase model and effort configuration for the claude-code driver #914 ships today, verified by existing tests staying green plus a mixed role/raw casemodelUsagefor each phase execution, verified by a unit test on the metrics writeragents.modelaccepts a free string with the inert-per-#43869 note retained, andsettings.tsdocs no longer rank family capability, verified by settings schema testsDone when
A roster change (new family, renamed tier) requires editing only the
modelRolesmap — no change to phase policy, ladder config, skills, or docs — and a stale map fails loudly instead of silently succeeding.