//! Read-only native model-map command.
//!
//! The `models` family is a read-only inspection surface. In a project it loads
//! canonical model configuration for the requested or environment harness. In
//! an isolated directory with no explicit config it remains context-free and
//! materializes typed defaults without creating any filesystem state.
use std::{
collections::{BTreeMap, BTreeSet},
fs,
io::{self, Write},
path::Path,
};
use shepherd::{
Harness,
compiler::HarnessProfile,
loader::{self, ConfigContext},
settings::{
HarnessConfig, MODEL_ROLE_ALIASES as ROLE_ALIASES, ModelsConfig, PiModelTargetsConfig,
RoleClass, RoleClassesConfig,
},
};
use crate::{
ContextError, ContextInputs, ExecutionContext,
interface::{CliError, CliGlobals},
};
const ROLES: [&str; 9] = [
"root",
"planter",
"engineer",
"conductor",
"critic",
"discovery",
"coder",
"auditor",
"worker",
];
const HARNESSES: [&str; 3] = ["claude", "codex", "pi"];
// `root` is the canonical spelling on THIS surface only: it is the literal
// `[models]` TOML key operators write (`ModelsConfig::root`,
// `crates/core/src/settings.rs:546`), and `docs/configuration.md`'s default
// table is cross-checked against that field name by
// `scripts/check-workspace.sh`'s `rule_model_defaults_match_the_docs`. Every
// other surface in this plugin -- `content/roles/shepherd.md`,
// `skills/shepherd/SKILL.md`, `agents/shepherd.md`, the `shepherd:shepherd`
// subagent type, and `role_tier` in `crates/core/src/guard/engine.rs` --
// spells the same role `shepherd`. Renaming the models role to match would
// ripple into `crates/core` and `docs/`, both outside this file's scope, so
// `shepherd` is a documented INPUT alias that resolves to `root`, never a
// tenth entry in `ROLES` and never a second canonical spelling.
const USAGE: &str = "shepherd models <resolve|show> [args]\n\n resolve <role> Echo the provider-neutral route for one role.\n resolve <role> --harness <claude|codex|pi> [--model <target>]\n Resolve root, team-lead, bulk-work, or\n adversarial-review through an open harness route.\n --model is the final operator override.\n Roles: root planter engineer conductor critic\n discovery coder auditor worker\n Alias: shepherd -> root (the [models] root config key\n is canonical here; content/, the guard engine,\n and the agent cards spell this same role\n shepherd).\n show [--md|--json] Print every built-in and configured role + source.\n show --harness <claude|codex|pi> [--md|--json]\n Render every role's harness-native target.\n\n[role_classes] selects open provider-neutral routes. [models] is the exact\nper-role override map. [[harness]] blocks map routes and exact agents to\nnative targets. Precedence is adapter default, user config, project config,\nthen --model. See docs/configuration.md §models.";
const TEXT_FOOTER: &str = "root is advisory (your live session model). Spawned roles resolve their\nprovider-neutral class route through configured targets plus adapter defaults.\nSee docs/configuration.md §models.";
const MD_FOOTER: &str = "_root is advisory: it names the model your live session should run; a config key cannot rebind a running main-chat session._";
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, clap::Args)]
#[command(disable_help_flag = true, disable_help_subcommand = true)]
pub struct ModelsCmd {
/// Print the canonical models usage contract.
#[arg(short = 'h', long = "help")]
help: bool,
#[command(subcommand)]
action: Option<ModelsAction>,
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, clap::Subcommand)]
enum ModelsAction {
/// Print the canonical models usage contract.
Help,
/// Resolve one role's model slug.
Resolve(ModelsResolveCmd),
/// Render every resolved role.
Show(ModelsShowCmd),
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, clap::Args)]
#[command(disable_help_flag = true)]
struct ModelsResolveCmd {
/// Role to resolve. Kept optional so the legacy usage message remains stable.
role: Option<String>,
/// Translate an intent slug to one harness's native spelling.
#[arg(long)]
harness: Option<String>,
/// Override every configured route with one exact operator-selected model.
#[arg(long = "model", value_name = "MODEL")]
command_line_model: Option<String>,
/// Emit the one-role resolution as JSON.
#[arg(long)]
json: bool,
/// Print the canonical models usage contract.
#[arg(short = 'h', long = "help")]
help: bool,
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, clap::Args)]
#[command(disable_help_flag = true)]
struct ModelsShowCmd {
/// Render a markdown table.
#[arg(long)]
md: bool,
/// Render the role map as JSON.
#[arg(long)]
json: bool,
/// Translate every row's model to one harness's native spelling.
#[arg(long)]
harness: Option<String>,
/// Print the canonical models usage contract.
#[arg(short = 'h', long = "help")]
help: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct ModelRow {
role: String,
class: Option<RoleClass>,
model: String,
effort: Option<String>,
source: ModelSource,
}
#[derive(
Clone,
Copy,
Debug,
Eq,
PartialEq,
strum::AsRefStr,
strum::Display,
strum::EnumCount,
strum::EnumIs,
strum::EnumString,
strum::IntoStaticStr,
strum::VariantArray,
strum::VariantNames,
)]
#[strum(ascii_case_insensitive, serialize_all = "snake_case")]
enum ModelSource {
CommandLine,
Config,
Default,
}
impl ModelSource {
const fn as_str(self) -> &'static str {
match self {
Self::CommandLine => "command-line",
Self::Config => "config",
Self::Default => "default",
}
}
}
impl ModelsCmd {
pub(crate) fn run(self, globals: CliGlobals) -> Result<(), CliError> {
if self.help {
return write_stdout(USAGE);
}
match self.action {
Some(ModelsAction::Help) => write_stdout(USAGE),
Some(ModelsAction::Resolve(command)) => command.run(globals),
Some(ModelsAction::Show(command)) => command.run(globals),
None => {
let resolved = resolve_rows(&globals, None)?;
write_stdout(&render_text(&resolved.rows))
}
}
}
}
impl ModelsResolveCmd {
fn run(self, globals: CliGlobals) -> Result<(), CliError> {
if self.help {
return write_stdout(USAGE);
}
let Some(role) = self.role.as_deref() else {
return Err(CliError::message_with_code(
"usage: shepherd models resolve <role>",
2,
));
};
// Normalize an alias input (e.g. `shepherd`) to its canonical `ROLES`
// spelling BEFORE the membership check and the row lookup below, so
// both see only canonical role strings.
let role = canonical_role(role);
if let Some(harness) = self.harness.as_deref()
&& !HARNESSES.contains(&harness)
{
return Err(CliError::message_with_code(
format!(
"unknown harness: {harness} (valid: {})",
HARNESSES.join(" ")
),
2,
));
}
let requested_harness = self.harness.as_deref().map(harness_from_name);
let mut resolved = resolve_rows(&globals, requested_harness)?;
let mut row = resolved
.rows
.into_iter()
.find(|row| row.role == role)
.ok_or_else(|| CliError::message_with_code(unknown_role_message(role), 2))?;
if let Some(command_line_model) = self.command_line_model {
validate_command_line_model(&command_line_model)?;
row.model = command_line_model;
row.effort = None;
row.source = ModelSource::CommandLine;
} else if let Some(harness) = self.harness.as_deref() {
let harness_config = resolved.harness.remove(harness);
let translation = translate_for_harness(
&row,
harness,
harness_config.as_ref(),
&resolved.pi_targets,
)?;
row.model = translation.model;
row.effort = translation.effort;
if translation.configured {
row.source = ModelSource::Config;
}
}
if self.json {
let mut fields = vec![("role", json_string(&row.role))];
if let Some(class) = row.class {
fields.push(("class", json_string(class.as_str())));
}
fields.push(("model", json_string(&row.model)));
if let Some(effort) = row.effort.as_deref() {
fields.push(("effort", json_string(effort)));
}
fields.push(("source", json_string(row.source.as_str())));
if let Some(harness) = self.harness {
fields.push(("harness", json_string(&harness)));
}
let mut lines = vec!["{".to_owned()];
let fields_len = fields.len();
lines.extend(fields.into_iter().enumerate().map(|(index, (key, value))| {
let comma = if index + 1 == fields_len { "" } else { "," };
format!(" \"{key}\": {value}{comma}")
}));
lines.push("}".to_owned());
write_stdout(&lines.join("\n"))
} else {
write_stdout(&row.model)
}
}
}
impl ModelsShowCmd {
fn run(self, globals: CliGlobals) -> Result<(), CliError> {
if self.help {
return write_stdout(USAGE);
}
if let Some(harness) = self.harness.as_deref()
&& !HARNESSES.contains(&harness)
{
return Err(CliError::message_with_code(
format!(
"unknown harness: {harness} (valid: {})",
HARNESSES.join(" ")
),
2,
));
}
let requested_harness = self.harness.as_deref().map(harness_from_name);
let mut resolved = resolve_rows(&globals, requested_harness)?;
if let Some(harness) = self.harness.as_deref() {
let config = resolved.harness.remove(harness);
for row in &mut resolved.rows {
let translation =
translate_for_harness(row, harness, config.as_ref(), &resolved.pi_targets)?;
row.model = translation.model;
row.effort = translation.effort;
if translation.configured {
row.source = ModelSource::Config;
}
}
}
if self.json {
write_stdout(&render_json(&resolved.rows))
} else if self.md {
write_stdout(&render_markdown(&resolved.rows))
} else {
write_stdout(&render_text(&resolved.rows))
}
}
}
struct ResolvedRows {
rows: Vec<ModelRow>,
harness: BTreeMap<String, HarnessConfig>,
pi_targets: PiModelTargetsConfig,
}
fn resolve_rows(
globals: &CliGlobals,
requested_harness: Option<Harness>,
) -> Result<ResolvedRows, CliError> {
let cwd = std::env::current_dir()
.map_err(|error| CliError::message(format!("cannot resolve current directory: {error}")))?;
let outside_repository = !has_repository_marker(&cwd)?;
let mut inputs = ContextInputs::from_environment(cwd)
.map_err(|error| CliError::message(error.to_string()))?;
inputs.explicit_config = globals.config.clone();
if let Some(harness) = requested_harness {
inputs.active_harness = Some(harness);
}
inputs.verbosity = globals.verbosity;
let context = match ExecutionContext::discover(inputs) {
Ok(context) => Some(context),
Err(ContextError::Primary(_)) if globals.config.is_none() && outside_repository => None,
Err(error) => return Err(CliError::message(error.to_string())),
};
let (models, role_classes, configured_roles, configured_classes, harness, pi_targets) =
if let Some(context) = context {
reject_inert_json_configuration(&ConfigContext {
primary_root: context.primary_root.clone(),
user_home: context.user_home.clone(),
harness: context.active_harness,
})?;
// The loader already walked every merged layer's parsed table once to
// build `explicit_keys` (see `shepherd_core::loader::LoadedConfig`); a
// role is "configured" exactly when its dotted `models.<role>` key was
// present in some layer, never by comparing the merged value against
// `ModelsConfig::default()`.
let configured_roles = ROLES
.into_iter()
.filter(|role| context.explicit_keys.contains(&format!("models.{role}")))
.map(ToOwned::to_owned)
.chain(context.config.models.additional.keys().cloned())
.collect::<BTreeSet<String>>();
let configured_classes = [
RoleClass::Root,
RoleClass::TeamLead,
RoleClass::BulkWork,
RoleClass::AdversarialReview,
]
.into_iter()
.filter(|class| {
context
.explicit_keys
.contains(&format!("role_classes.{}", class.as_str()))
})
.collect::<BTreeSet<_>>();
let mut harness = context
.config
.harness
.iter()
.cloned()
.map(|config| (harness_name(config.kind).to_owned(), config))
.collect::<BTreeMap<_, _>>();
let pi_targets = context.config.model_targets.pi.clone();
if !pi_targets.is_empty() {
harness.insert(
"pi".into(),
HarnessConfig {
kind: Harness::Pi,
models: pi_targets.routes(),
agents: Vec::new(),
},
);
}
(
context.config.models.clone(),
context.config.role_classes.clone(),
configured_roles,
configured_classes,
harness,
pi_targets,
)
} else {
(
ModelsConfig::default(),
RoleClassesConfig::default(),
BTreeSet::new(),
BTreeSet::new(),
BTreeMap::new(),
PiModelTargetsConfig::default(),
)
};
let mut roles = ROLES
.iter()
.map(|role| (*role).to_owned())
.collect::<Vec<_>>();
let mut additional = models.additional.keys().cloned().collect::<BTreeSet<_>>();
if let Some(requested_harness) = requested_harness
&& let Some(config) = harness.get(harness_name(requested_harness))
{
additional.extend(config.agents.iter().map(|agent| agent.name.clone()));
}
additional.retain(|role| !ROLES.contains(&role.as_str()));
roles.extend(additional);
let rows = roles
.into_iter()
.map(|role| {
let class = RoleClass::for_role(&role);
let role_configured = configured_roles.contains(role.as_str());
let route = if role_configured {
models
.get(&role)
.expect("configured role remains present in ModelsConfig")
.to_owned()
} else if let Some(class) = class {
role_classes.route(class).to_owned()
} else if let Some(route) = models.get(&role) {
route.to_owned()
} else {
harness
.get(requested_harness.map_or("", harness_name))
.and_then(|config| config.agents.iter().find(|agent| agent.name == role))
.map_or_else(String::new, |agent| agent.model.clone())
};
let class_configured = class.is_some_and(|class| configured_classes.contains(&class));
ModelRow {
role,
class,
model: route,
effort: None,
source: if role_configured || class_configured {
ModelSource::Config
} else {
ModelSource::Default
},
}
})
.collect();
Ok(ResolvedRows {
rows,
harness,
pi_targets,
})
}
fn reject_inert_json_configuration(context: &ConfigContext) -> Result<(), CliError> {
for path in loader::inert_json_candidates(context) {
match fs::symlink_metadata(&path) {
Ok(_) => {
return Err(CliError::message_with_code(
format!(
"unsupported Shepherd configuration file {} is inert; use `.shepherd/shepherd.toml` or run `shepherd migrate --layout v5`",
path.display()
),
2,
));
}
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(error) => {
return Err(CliError::message(format!(
"cannot inspect unsupported configuration candidate {}: {error}",
path.display()
)));
}
}
}
Ok(())
}
fn has_repository_marker(start: &Path) -> Result<bool, CliError> {
for ancestor in start.ancestors() {
match fs::symlink_metadata(ancestor.join(".git")) {
Ok(_) => return Ok(true),
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(error) => {
return Err(CliError::message(format!(
"cannot inspect repository marker below {}: {error}",
ancestor.display()
)));
}
}
}
Ok(false)
}
fn harness_from_name(harness: &str) -> Harness {
match harness {
"claude" => Harness::ClaudeCode,
"codex" => Harness::Codex,
"pi" => Harness::Pi,
_ => unreachable!("callers validate against the fixed harness map"),
}
}
fn harness_name(harness: Harness) -> &'static str {
match harness {
Harness::ClaudeCode => "claude",
Harness::Codex => "codex",
Harness::Pi => "pi",
Harness::PrimeAgent => "prime_agent",
_ => "unsupported",
}
}
/// Map an input role spelling to its canonical `ROLES` spelling through
/// `ROLE_ALIASES`. A role with no alias entry passes through unchanged.
/// Aliases are input-only: the return value is always either the input
/// itself or a member of `ROLES`, never a synthesized third spelling.
fn canonical_role(role: &str) -> &str {
ROLE_ALIASES
.iter()
.find(|(alias, _)| *alias == role)
.map_or(role, |(_, canonical)| *canonical)
}
/// Build the `unknown role` error message from `ROLES` and `ROLE_ALIASES`
/// rather than a hand-typed literal, so the valid-role list and the alias
/// hint cannot drift out of sync with the const arrays that define them.
fn unknown_role_message(role: &str) -> String {
let aliases = ROLE_ALIASES
.iter()
.map(|(alias, canonical)| format!("{alias} -> {canonical}"))
.collect::<Vec<_>>()
.join(", ");
format!(
"unknown role: {role} (valid: {}; alias: {aliases})",
ROLES.join(" ")
)
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct ModelTranslation {
model: String,
effort: Option<String>,
configured: bool,
}
fn translate_for_harness(
row: &ModelRow,
harness: &str,
config: Option<&HarnessConfig>,
pi_targets: &PiModelTargetsConfig,
) -> Result<ModelTranslation, CliError> {
if let Some(agent) =
config.and_then(|config| config.agents.iter().find(|agent| agent.name == row.role))
{
return Ok(ModelTranslation {
model: resolve_harness_route(&agent.model, config)?
.unwrap_or_else(|| agent.model.clone()),
effort: agent.effort.clone(),
configured: true,
});
}
if let Some(model) = resolve_harness_route(&row.model, config)? {
return Ok(ModelTranslation {
effort: canonical_effort(&row.model, harness),
model,
configured: true,
});
}
if let Some(class) = RoleClass::from_route(&row.model) {
if harness == "pi"
&& let Some(model) = resolve_harness_route(legacy_route_for_class(class), config)?
{
return Ok(ModelTranslation {
model,
effort: None,
configured: true,
});
}
return default_class_target(class, harness, pi_targets);
}
let profile = HarnessProfile::canonical()
.into_iter()
.find(|profile| profile.target.as_str() == harness)
.expect("callers validate against the fixed harness map");
let resolution = profile.model_by_hint.get(&row.model).ok_or_else(|| {
CliError::message_with_code(
format!(
"unknown model route `{}` for {harness}; add it to the matching [[harness]].models map",
row.model
),
2,
)
})?;
if harness == "pi" {
if row.model == "inherit-caller" {
return Ok(ModelTranslation {
model: "inherit".into(),
effort: None,
configured: false,
});
}
return pi_targets
.get(&row.model)
.map(|model| ModelTranslation {
model: model.to_owned(),
effort: None,
configured: true,
})
.ok_or_else(|| {
CliError::message_with_code(
format!(
"Pi model target missing for route `{}`. Add it to \
`[[harness]] kind = \"pi\"` and `[harness.models]` in Shepherd configuration.",
row.model
),
2,
)
});
}
Ok(ModelTranslation {
model: resolution
.model
.clone()
.unwrap_or_else(|| row.model.clone()),
effort: resolution.reasoning_effort.clone(),
configured: false,
})
}
fn default_class_target(
class: RoleClass,
harness: &str,
pi_targets: &PiModelTargetsConfig,
) -> Result<ModelTranslation, CliError> {
let (model, effort) = match (harness, class) {
("claude", RoleClass::Root | RoleClass::TeamLead) => ("opus[1m]", None),
("claude", RoleClass::BulkWork) => ("haiku", None),
("claude", RoleClass::AdversarialReview) => ("sonnet", None),
("codex", RoleClass::Root | RoleClass::TeamLead) => ("gpt-5.6-sol", Some("high")),
("codex", RoleClass::BulkWork) => ("gpt-5.6-luna", Some("max")),
("codex", RoleClass::AdversarialReview) => ("gpt-5.6-terra", Some("high")),
("pi", class) => {
let legacy_route = legacy_route_for_class(class);
return pi_targets
.get(legacy_route)
.map(|model| ModelTranslation {
model: model.to_owned(),
effort: None,
configured: true,
})
.ok_or_else(|| {
CliError::message_with_code(
format!(
"Pi model target missing for class `{}`. Add it to `[[harness]] kind = \"pi\"` and `[harness.models]` in Shepherd configuration.",
class.as_str()
),
2,
)
});
}
_ => unreachable!("callers validate against the fixed harness map"),
};
Ok(ModelTranslation {
model: model.into(),
effort: effort.map(str::to_owned),
configured: false,
})
}
const fn legacy_route_for_class(class: RoleClass) -> &'static str {
match class {
RoleClass::Root | RoleClass::TeamLead => "reasoning-high",
RoleClass::BulkWork => "economy",
RoleClass::AdversarialReview => "standard",
}
}
fn resolve_harness_route(
route: &str,
config: Option<&HarnessConfig>,
) -> Result<Option<String>, CliError> {
let Some(config) = config else {
return Ok(None);
};
if let Some(target) = config.models.get(route) {
return Ok(Some(target.clone()));
}
let mut reverse = config
.models
.iter()
.filter(|(_, alias)| alias.as_str() == route)
.map(|(target, _)| target.as_str());
let Some(target) = reverse.next() else {
return Ok(None);
};
if reverse.next().is_some() {
return Err(CliError::message_with_code(
format!(
"ambiguous harness model alias `{route}` for {}",
harness_name(config.kind)
),
2,
));
}
Ok(Some(target.to_owned()))
}
fn canonical_effort(route: &str, harness: &str) -> Option<String> {
HarnessProfile::canonical()
.into_iter()
.find(|profile| profile.target.as_str() == harness)
.and_then(|profile| profile.model_by_hint.get(route).cloned())
.and_then(|resolution| resolution.reasoning_effort)
}
fn validate_command_line_model(model: &str) -> Result<(), CliError> {
if model.is_empty() || model.trim() != model || model.chars().any(char::is_control) {
return Err(CliError::message_with_code(
"--model requires a non-empty target without surrounding whitespace or control characters",
2,
));
}
Ok(())
}
fn render_text(rows: &[ModelRow]) -> String {
let mut lines = vec!["shepherd model map (resolved)".to_owned()];
lines.extend(rows.iter().map(|row| {
format!(
" {:<10} {:<10} ({})",
row.role,
row.model,
row.source.as_str()
)
}));
lines.push(String::new());
lines.push(TEXT_FOOTER.to_owned());
lines.join("\n")
}
fn render_markdown(rows: &[ModelRow]) -> String {
let mut lines = vec![
"| role | model | source |".to_owned(),
"|---|---|---|".to_owned(),
];
lines.extend(rows.iter().map(|row| {
format!(
"| {} | `{}` | {} |",
row.role,
row.model,
row.source.as_str()
)
}));
lines.push(String::new());
lines.push(MD_FOOTER.to_owned());
lines.join("\n")
}
fn render_json(rows: &[ModelRow]) -> String {
let entries = rows.iter().map(|row| {
format!(
" \"{}\": {{\"model\": {}, \"source\": {}}}",
row.role,
json_string(&row.model),
json_string(row.source.as_str())
)
});
format!("{{\n{}\n}}", entries.collect::<Vec<_>>().join(",\n"))
}
fn json_string(value: &str) -> String {
serde_json::to_string(value).expect("serializing a string cannot fail")
}
fn write_stdout(text: &str) -> Result<(), CliError> {
let mut stdout = io::stdout().lock();
stdout
.write_all(text.as_bytes())
.and_then(|()| stdout.write_all(b"\n"))
.map_err(|error| CliError::message(format!("cannot write stdout: {error}")))
}
#[cfg(test)]
mod tests {
use shepherd::settings::{PiModelTargetsConfig, RoleClass};
use super::{
ModelRow, ModelSource, ROLE_ALIASES, ROLES, USAGE, canonical_role, render_json,
render_markdown, render_text, translate_for_harness, unknown_role_message,
};
fn defaults() -> Vec<ModelRow> {
vec![
ModelRow {
role: "root".into(),
class: Some(RoleClass::Root),
model: "inherit-caller".into(),
effort: None,
source: ModelSource::Default,
},
ModelRow {
role: "coder".into(),
class: Some(RoleClass::BulkWork),
model: "standard".into(),
effort: None,
source: ModelSource::Default,
},
]
}
#[test]
fn renderers_preserve_the_legacy_row_order_and_shape() {
let rows = defaults();
assert_eq!(
render_json(&rows),
"{\n \"root\": {\"model\": \"inherit-caller\", \"source\": \"default\"},\n \"coder\": {\"model\": \"standard\", \"source\": \"default\"}\n}"
);
assert!(render_text(&rows).starts_with("shepherd model map (resolved)\n"));
assert!(render_markdown(&rows).starts_with("| role | model | source |\n"));
}
#[test]
fn harness_translation_fails_closed_for_unknown_claude_models() {
let pi_targets = PiModelTargetsConfig {
inherit_caller: "inherit".into(),
reasoning_high: "openai-codex/gpt-5.6-sol:xhigh".into(),
standard: "openai-codex/gpt-5.6-luna:max".into(),
economy: "openai-codex/gpt-5.6-luna:max".into(),
};
let row = |model: &str| ModelRow {
role: "engineer".into(),
class: Some(RoleClass::TeamLead),
model: model.into(),
effort: None,
source: ModelSource::Default,
};
assert_eq!(
translate_for_harness(&row("reasoning-high"), "claude", None, &pi_targets)
.expect("known hint")
.model,
"opus[1m]"
);
assert_eq!(
translate_for_harness(&row("reasoning-high"), "codex", None, &pi_targets)
.expect("known hint")
.model,
"gpt-5.6-sol"
);
assert_eq!(
translate_for_harness(&row("reasoning-high"), "pi", None, &pi_targets)
.expect("known hint")
.model,
"openai-codex/gpt-5.6-sol:xhigh"
);
assert_eq!(
translate_for_harness(
&row("inherit-caller"),
"pi",
None,
&PiModelTargetsConfig::default(),
)
.expect("Pi inheritance is concrete")
.model,
"inherit"
);
assert!(
translate_for_harness(
&row("standard"),
"pi",
None,
&PiModelTargetsConfig::default(),
)
.is_err()
);
assert!(translate_for_harness(&row("custom"), "claude", None, &pi_targets).is_err());
}
/// Anti-drift tripwire: `ROLES` and the USAGE text are two of the three
/// hand-maintained copies of the role vocabulary this step exists to stop
/// from drifting apart (the third is the pinned CLI-test assertion in
/// `tests/models_cli.rs`). This iterates `ROLES` and
/// `ROLE_ALIASES` rather than checking today's nine literal names, so it
/// fails the moment a role or alias is added to either const without also
/// reaching the usage text.
#[test]
fn usage_names_every_role_and_every_alias_pair() {
for role in ROLES {
assert!(
USAGE.contains(role),
"a role was added to ROLES without reaching USAGE: `{role}` is \
missing from:\n{USAGE}"
);
}
for (alias, canonical) in ROLE_ALIASES {
let direction = format!("{alias} -> {canonical}");
assert!(
USAGE.contains(&direction),
"an alias was added to ROLE_ALIASES without reaching USAGE: \
`{direction}` is missing from:\n{USAGE}"
);
}
}
#[test]
fn canonical_role_maps_the_documented_alias_and_passes_through_everything_else() {
assert_eq!(canonical_role("shepherd"), "root");
assert_eq!(canonical_role("root"), "root");
assert_eq!(canonical_role("coder"), "coder");
assert_eq!(canonical_role("nonsense"), "nonsense");
}
#[test]
fn unknown_role_message_names_the_alias_direction_and_every_valid_role() {
let message = unknown_role_message("nonsense");
assert_eq!(
message,
"unknown role: nonsense (valid: root planter engineer conductor \
critic discovery coder auditor worker; alias: shepherd -> root)"
);
for role in ROLES {
assert!(message.contains(role), "missing role `{role}`: {message}");
}
}
}