use std::fmt;
use std::path::Path;
use std::process::Output;
use anyhow::{Result, bail};
mod adapter;
pub(crate) mod agy;
pub(crate) mod claude;
pub(crate) mod codex;
pub(crate) mod grok;
mod instructions;
pub(crate) mod interpretation;
#[cfg(test)]
pub(crate) mod interpretation_tests;
pub(crate) mod muse;
pub(crate) mod opencode;
pub(crate) mod process;
pub(crate) mod settings;
mod skill_load;
mod summary;
pub use adapter::Adapter;
pub use summary::write as write_summary;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Harness {
Claude,
Codex,
Agy,
Grok,
Muse,
OpenCode,
}
impl Harness {
pub fn named(name: &str) -> Option<Harness> {
adapter::REGISTERED
.iter()
.find(|(_, adapter)| adapter.name() == name)
.map(|(harness, _)| *harness)
}
pub fn name(self) -> &'static str {
self.adapter().name()
}
pub fn adapter(self) -> &'static dyn Adapter {
adapter::REGISTERED
.iter()
.find(|(harness, _)| *harness == self)
.map(|(_, adapter)| *adapter)
.expect("every Harness has an adapter")
}
pub const ALL: [Harness; adapter::REGISTERED.len()] = {
let mut all = [adapter::REGISTERED[0].0; adapter::REGISTERED.len()];
let mut at = 0;
while at < all.len() {
all[at] = adapter::REGISTERED[at].0;
at += 1;
}
all
};
pub fn installed(self) -> bool {
on_path(self.name())
}
}
pub fn names() -> String {
adapter::REGISTERED
.iter()
.map(|(_, adapter)| adapter.name())
.collect::<Vec<_>>()
.join(" or ")
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct ModelAndEffort {
pub model: Option<String>,
pub effort: Option<String>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Settings {
pub default: Option<Harness>,
pub claude: ModelAndEffort,
pub codex: ModelAndEffort,
pub agy: ModelAndEffort,
pub grok: ModelAndEffort,
pub muse: ModelAndEffort,
pub opencode: ModelAndEffort,
}
impl Settings {
pub fn of(&self, harness: Harness) -> &ModelAndEffort {
harness.adapter().settings(self)
}
pub fn of_mut(&mut self, harness: Harness) -> &mut ModelAndEffort {
harness.adapter().settings_mut(self)
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Asked {
pub harness: Option<Harness>,
pub model_and_effort: ModelAndEffort,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChosenBy {
Command,
UserConfig,
SecurityConfig,
Default,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Choice {
pub harness: Harness,
pub model: Option<String>,
pub effort: Option<String>,
pub chosen_by: ChosenBy,
}
impl Default for Choice {
fn default() -> Self {
Choice {
harness: Harness::Claude,
model: None,
effort: None,
chosen_by: ChosenBy::Default,
}
}
}
impl Choice {
pub fn of(asked: &Asked, settings: &Settings) -> Choice {
let (harness, chosen_by) = match (asked.harness, settings.default) {
(Some(harness), _) => (harness, ChosenBy::Command),
(None, Some(harness)) => (harness, ChosenBy::UserConfig),
(None, None) => (Harness::Claude, ChosenBy::Default),
};
let set = settings.of(harness);
let asked = &asked.model_and_effort;
Choice {
harness,
model: asked.model.clone().or_else(|| set.model.clone()),
effort: asked.effort.clone().or_else(|| set.effort.clone()),
chosen_by,
}
}
pub fn built_with(&self) -> String {
format!("Built with {self}")
}
#[cfg(test)]
pub fn claude_args(&self) -> Vec<&str> {
claude::model_args(self)
}
#[cfg(test)]
pub fn codex_args(&self) -> Vec<String> {
codex::model_args(self)
}
pub fn check(&mut self) -> Result<()> {
let adapter = self.harness.adapter();
let cli = adapter.name();
if !on_path(cli) {
bail!(
"{cli} is not on PATH, and the Harness {cli} is chosen by {}: install it, \
or choose another Harness",
self.chosen_by
);
}
adapter.check(self)
}
}
fn said(output: &Output) -> String {
[&output.stderr, &output.stdout]
.map(|said| String::from_utf8_lossy(said).trim().to_string())
.into_iter()
.filter(|said| !said.is_empty())
.collect::<Vec<_>>()
.join("\n")
}
impl fmt::Display for Choice {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{} · {} · {}",
self.harness.name(),
self.model.as_deref().unwrap_or("default model"),
self.effort.as_deref().unwrap_or("default effort")
)
}
}
impl fmt::Display for ChosenBy {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(match self {
ChosenBy::Command => "the harness flag",
ChosenBy::UserConfig => "harness.default in the User config",
ChosenBy::SecurityConfig => "security.harness in the User config",
ChosenBy::Default => {
"the default, as neither the harness flag nor harness.default in the User \
config names one"
}
})
}
}
fn on_path(cli: &str) -> bool {
use std::os::unix::fs::PermissionsExt;
let Some(path) = std::env::var_os("PATH") else {
return false;
};
std::env::split_paths(&path).any(|dir| {
Path::new(&dir)
.join(cli)
.metadata()
.is_ok_and(|file| file.is_file() && file.permissions().mode() & 0o111 != 0)
})
}
#[cfg(test)]
mod tests {
use super::codex::Catalog;
use super::*;
fn set(model: &str, effort: &str) -> ModelAndEffort {
let named = |value: &str| (!value.is_empty()).then(|| value.to_string());
ModelAndEffort {
model: named(model),
effort: named(effort),
}
}
fn codex_default() -> Settings {
Settings {
default: Some(Harness::Codex),
claude: set("opus", "high"),
codex: set("gpt-6.1-sol", "max"),
..Settings::default()
}
}
fn choice(harness: Harness, model: &str, effort: &str, chosen_by: ChosenBy) -> Choice {
let set = set(model, effort);
Choice {
harness,
model: set.model,
effort: set.effort,
chosen_by,
}
}
#[test]
fn with_nothing_asked_and_nothing_set_sessions_run_on_claude_with_its_own_model_and_effort() {
let chosen = Choice::of(&Asked::default(), &Settings::default());
assert_eq!(chosen, Choice::default());
assert!(chosen.claude_args().is_empty());
}
#[test]
fn the_user_config_chooses_the_harness_and_its_own_model_and_effort() {
let chosen = Choice::of(&Asked::default(), &codex_default());
assert_eq!(
chosen,
choice(Harness::Codex, "gpt-6.1-sol", "max", ChosenBy::UserConfig)
);
}
#[test]
fn a_harness_on_the_command_takes_that_harnesss_model_and_effort_from_the_user_config() {
let asked = Asked {
harness: Some(Harness::Claude),
..Asked::default()
};
let chosen = Choice::of(&asked, &codex_default());
assert_eq!(
chosen,
choice(Harness::Claude, "opus", "high", ChosenBy::Command)
);
}
#[test]
fn a_model_or_effort_on_the_command_applies_to_whichever_harness_is_chosen() {
for (asked, model, effort) in [
(set("sonnet", ""), "sonnet", "max"),
(set("", "low"), "gpt-6.1-sol", "low"),
(set("sonnet", "low"), "sonnet", "low"),
] {
let asked = Asked {
harness: None,
model_and_effort: asked,
};
let chosen = Choice::of(&asked, &codex_default());
assert_eq!(
chosen,
choice(Harness::Codex, model, effort, ChosenBy::UserConfig),
"{asked:?}"
);
}
}
#[test]
fn a_harness_with_nothing_set_leaves_its_model_and_effort_to_it() {
let settings = Settings {
default: Some(Harness::Claude),
..codex_default()
};
let asked = Asked {
harness: Some(Harness::Codex),
..Asked::default()
};
let chosen = Choice::of(
&asked,
&Settings {
codex: ModelAndEffort::default(),
..settings
},
);
assert_eq!(chosen, choice(Harness::Codex, "", "", ChosenBy::Command));
}
#[test]
fn claude_is_given_the_model_and_effort_that_are_set() {
for (model, effort, args) in [
("opus", "", vec!["--model", "opus"]),
("", "max", vec!["--effort", "max"]),
(
"claude-opus-5-5",
"high",
vec!["--model", "claude-opus-5-5", "--effort", "high"],
),
] {
let chosen = choice(Harness::Claude, model, effort, ChosenBy::Command);
assert_eq!(chosen.claude_args(), args);
}
}
#[test]
fn the_built_with_line_names_the_harness_model_and_effort_or_the_harnesss_default() {
for (chosen, line) in [
(
choice(
Harness::Claude,
"claude-opus-5-5",
"high",
ChosenBy::Command,
),
"Built with claude · claude-opus-5-5 · high",
),
(
Choice::default(),
"Built with claude · default model · default effort",
),
(
choice(Harness::Codex, "", "max", ChosenBy::UserConfig),
"Built with codex · default model · max",
),
] {
assert_eq!(chosen.built_with(), line);
}
}
#[test]
fn codex_is_given_the_model_and_the_effort_in_its_config() {
for (model, effort, args) in [
("", "", vec![]),
("gpt-6.1-sol", "", vec!["-m", "gpt-6.1-sol"]),
(
"gpt-6.1-sol",
"max",
vec!["-m", "gpt-6.1-sol", "-c", "model_reasoning_effort=\"max\""],
),
] {
let chosen = choice(Harness::Codex, model, effort, ChosenBy::Command);
assert_eq!(chosen.codex_args(), args);
}
}
fn catalog() -> Catalog {
Catalog::parse(
r#"{"models": [
{"slug": "gpt-6.1-sol", "display_name": "GPT-6.1-Sol", "visibility": "list",
"supported_reasoning_levels": [{"effort": "low"}, {"effort": "high"}, {"effort": "max"}]},
{"slug": "gpt-5.5", "display_name": "GPT-5.5 Classic", "visibility": "list",
"supported_reasoning_levels": [{"effort": "low"}, {"effort": "xhigh"}]},
{"slug": "codex-auto-review", "display_name": "Auto review", "visibility": "hide",
"supported_reasoning_levels": [{"effort": "minimal"}]}
]}"#,
)
.unwrap()
}
fn settled(model: Option<&str>, effort: Option<&str>) -> Result<ModelAndEffort> {
catalog().settle(model, effort)
}
#[test]
fn a_codex_model_and_effort_match_regardless_of_case_and_become_codexs_names() {
for (model, effort, settled_as) in [
(Some("GPT-6.1-Sol"), Some("Max"), set("gpt-6.1-sol", "max")),
(Some("gpt-6.1-sol"), None, set("gpt-6.1-sol", "")),
(
Some("gpt-5.5 classic"),
Some("XHIGH"),
set("gpt-5.5", "xhigh"),
),
(
Some("codex-auto-review"),
None,
set("codex-auto-review", ""),
),
(None, Some("High"), set("", "high")),
(None, None, set("", "")),
] {
assert_eq!(
settled(model, effort).unwrap(),
settled_as,
"{model:?} {effort:?}"
);
}
}
#[test]
fn an_unknown_codex_model_fails_naming_the_listed_models() {
let error = settled(Some("gpt-7"), Some("max")).unwrap_err();
assert_eq!(
error.to_string(),
"the Model gpt-7 is not in Codex's catalog: choose one of gpt-6.1-sol, gpt-5.5"
);
}
#[test]
fn an_effort_the_codex_model_does_not_support_fails_naming_those_it_does() {
let error = settled(Some("GPT-5.5"), Some("max")).unwrap_err();
assert_eq!(
error.to_string(),
"the Effort max is not one the Codex Model gpt-5.5 supports: choose one of low, xhigh"
);
}
#[test]
fn with_no_codex_model_the_effort_must_be_one_some_model_supports() {
let error = settled(None, Some("ultra")).unwrap_err();
assert_eq!(
error.to_string(),
"the Effort ultra is not one any Codex Model supports: choose one of low, high, max, \
xhigh, minimal"
);
}
#[test]
fn the_catalog_lists_its_listed_models_by_slug_and_display_name() {
assert_eq!(
catalog().listed(),
["gpt-6.1-sol (GPT-6.1-Sol)", "gpt-5.5 (GPT-5.5 Classic)"]
);
}
#[test]
fn the_efforts_are_the_models_or_with_no_model_any_models_each_once() {
let catalog = catalog();
assert_eq!(catalog.efforts(Some("gpt-5.5")), ["low", "xhigh"]);
assert_eq!(
catalog.efforts(None),
["low", "high", "max", "xhigh", "minimal"]
);
}
#[test]
fn output_that_is_no_catalog_fails() {
for output in ["", "not json", r#"{"models": 3}"#] {
assert!(Catalog::parse(output).is_err(), "{output}");
}
}
}