use std::collections::BTreeMap;
use std::io::IsTerminal;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use clap::{Args, CommandFactory, Parser, Subcommand};
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
use tokio::process::Command;
mod config;
mod doctor;
mod env;
mod export;
use mira::Host;
use mira::Trial;
use mira::exec::{self, CaseSpec, Concurrency};
use mira::protocol::{
ExecuteResult, InitializeResult, ListResult, RunResult, TranscriptSummary, capabilities,
};
use mira::report::{self, Format};
use mira::run::{RUN_META_FORMAT, RunMeta, RunSummary, new_run_id_at, now_unix};
const REPO_URL: &str = "https://github.com/everruns/mira";
const ISSUES_URL: &str = "https://github.com/everruns/mira/issues";
const DOCS_URL: &str = "https://github.com/everruns/mira/tree/main/docs";
const API_DOCS_URL: &str = "https://docs.rs/mira-eval";
const SKILL_URL: &str = "https://github.com/everruns/mira/tree/main/skills/mira";
const GUIDES: &[(&str, &str)] = &[
(
"how-it-works",
"the core model and moving parts, end to end",
),
("getting-started", "zero to a passing run"),
(
"authoring",
"datasets, the model matrix, axes, metadata, infra-errors vs failures",
),
(
"scorers",
"built-ins, budgets, combinators, closures, LLM-judge",
),
("metrics", "tokens/cost/latency and custom numeric metrics"),
("subjects", "in-process, CLI/polyglot, and runtime sessions"),
(
"extensibility",
"every seam: subjects, scorers, metrics, events, protocol",
),
("protocol", "the normative wire format and its versioning"),
];
const ABOUT: &str = "Run code-first evals for agents and tools across a target matrix — \
the Mira host CLI.";
const HELP_HINT: &str = "Tip: run `mira help --full` for an overview, every flag, examples, \
the doc guides, the agent skill, and links.";
#[derive(Parser)]
#[command(
name = "mira",
version,
about = ABOUT,
after_help = HELP_HINT,
disable_help_subcommand = true,
)]
struct Cli {
#[command(flatten)]
deprecated: DeprecatedLaunch,
#[command(subcommand)]
cmd: Option<Cmd>,
}
#[derive(Args, Default)]
struct StudyArgs {
#[arg(long, value_name = "PATH", group = "study_mode")]
study: Option<String>,
#[arg(long, value_name = "PATH", group = "study_mode")]
study_script: Option<String>,
#[arg(long, value_name = "SCRIPT", group = "study_mode")]
study_uv: Option<String>,
#[arg(long, value_name = "SCRIPT", group = "study_mode")]
study_python: Option<String>,
#[arg(long, value_name = "NAME", group = "study_mode")]
study_bin: Option<String>,
#[arg(long, value_name = "NAME", group = "study_mode")]
study_example: Option<String>,
#[arg(long, value_name = "CMD", group = "study_mode")]
study_cmd: Option<String>,
#[arg(long, value_name = "NAME")]
launcher: Option<String>,
#[arg(long)]
package: Option<String>,
#[arg(long)]
manifest_path: Option<String>,
}
#[derive(Args, Default)]
struct DeprecatedLaunch {
#[arg(long, global = true, hide = true)]
bin: Option<String>,
#[arg(long, global = true, hide = true)]
example: Option<String>,
#[arg(long, global = true, hide = true)]
cmd: Option<String>,
#[arg(long, global = true, hide = true, value_name = "SCRIPT")]
script: Option<String>,
#[arg(long, global = true, hide = true, value_name = "SCRIPT")]
uv: Option<String>,
#[arg(long, global = true, hide = true, value_name = "SCRIPT")]
python: Option<String>,
#[arg(long, global = true, hide = true, value_name = "SCRIPT")]
python3: Option<String>,
}
#[derive(Debug, Default)]
struct Launcher {
launcher: Option<String>,
bin: Option<String>,
example: Option<String>,
cmd: Option<String>,
script: Option<String>,
uv: Option<String>,
python: Option<String>,
python3: Option<String>,
package: Option<String>,
manifest_path: Option<String>,
}
fn merge_launch(study: &StudyArgs, dep: &DeprecatedLaunch) -> Result<Launcher, String> {
let mut l = Launcher {
launcher: study.launcher.clone(),
script: study.study_script.clone(),
uv: study.study_uv.clone(),
python3: study.study_python.clone(),
bin: study.study_bin.clone(),
example: study.study_example.clone(),
cmd: study.study_cmd.clone(),
package: study.package.clone(),
manifest_path: study.manifest_path.clone(),
..Launcher::default()
};
if let Some(path) = &study.study {
match Path::new(path).extension().and_then(|e| e.to_str()) {
Some("rs") => l.script = Some(path.clone()),
Some("py") => l.uv = Some(path.clone()),
_ => {
return Err(format!(
"cannot infer a runner for --study {path:?} (expected .rs or .py); \
use --study-script, --study-uv, --study-python, or --study-cmd"
));
}
}
}
for (flag, value, instead) in [
("--script", &dep.script, "--study PATH (or --study-script)"),
("--uv", &dep.uv, "--study PATH (or --study-uv)"),
("--python", &dep.python, "--study-python SCRIPT"),
("--python3", &dep.python3, "--study-python SCRIPT"),
("--bin", &dep.bin, "--study-bin NAME"),
("--example", &dep.example, "--study-example NAME"),
("--cmd", &dep.cmd, "--study-cmd \"...\""),
] {
if value.is_some() {
eprintln!(
"warning: {flag} is deprecated; use {instead} after the subcommand, \
e.g. `mira run --study study.rs`"
);
}
}
if !cli_sets_mode(&l) {
l.script = dep.script.clone();
l.uv = dep.uv.clone();
l.python = dep.python.clone();
l.python3 = dep.python3.clone();
l.bin = dep.bin.clone();
l.example = dep.example.clone();
l.cmd = dep.cmd.clone();
}
Ok(l)
}
#[derive(Subcommand)]
enum Cmd {
List(ListArgs),
Run(Box<RunArgs>),
Score(ScoreArgs),
Report(ReportArgs),
Export(ExportArgs),
Publish(PublishArgs),
Doctor(DoctorArgs),
Help(HelpArgs),
}
#[derive(Args)]
struct ListArgs {
#[command(flatten)]
study: StudyArgs,
}
#[derive(Args, Clone, Default)]
struct PublishConn {
#[arg(long = "everruns-url")]
api_url: Option<String>,
#[arg(long = "everruns-api-key")]
api_key: Option<String>,
#[arg(long = "everruns-org")]
org_id: Option<String>,
#[arg(long = "everruns-profile")]
profile: Option<String>,
}
impl PublishConn {
fn to_options(&self) -> mira_publish_everruns::PublishOptions {
mira_publish_everruns::PublishOptions {
base_url: self.api_url.clone(),
api_key: self.api_key.clone(),
org_id: self.org_id.clone(),
profile: self.profile.clone(),
}
}
}
#[derive(Args)]
struct PublishArgs {
run_id: String,
filter: Option<String>,
#[arg(long, default_value = "everruns")]
to: String,
#[command(flatten)]
conn: PublishConn,
}
#[derive(Args)]
struct DoctorArgs {
#[arg(long)]
fix: bool,
#[command(flatten)]
study: StudyArgs,
}
#[derive(Args)]
struct HelpArgs {
#[arg(long)]
full: bool,
}
#[derive(Args)]
struct RunArgs {
#[command(flatten)]
study: StudyArgs,
filter: Option<String>,
#[arg(long)]
tag: Option<String>,
#[arg(long)]
targets: Option<String>,
#[arg(long)]
samples: Option<String>,
#[arg(long)]
evals: Option<String>,
#[arg(long = "axis", value_name = "NAME=V1,V2")]
axes: Vec<String>,
#[arg(long)]
preset: Option<String>,
#[arg(long)]
trials: Option<usize>,
#[arg(long)]
seed: Option<u64>,
#[arg(long)]
group_by: Option<String>,
#[arg(long)]
out: Option<String>,
#[arg(long, default_value = "json")]
format: String,
#[arg(long)]
dry_run: bool,
#[arg(long, value_name = "RUN_ID", conflicts_with = "dry_run")]
resume: Option<String>,
#[arg(long, short = 'j', default_value_t = 8)]
max_concurrent: usize,
#[arg(long)]
provider_concurrency: Option<String>,
#[arg(long)]
no_adaptive: bool,
#[arg(long, default_value_t = 4)]
max_retries: u32,
#[arg(long, value_name = "SECONDS")]
timeout: Option<u64>,
#[arg(long, requires = "artifacts", conflicts_with_all = ["out", "resume"])]
execute_only: bool,
#[arg(long)]
artifacts: Option<String>,
#[arg(long, value_name = "TARGET", conflicts_with_all = ["dry_run", "execute_only"])]
publish: Option<String>,
#[command(flatten)]
publish_conn: PublishConn,
}
#[derive(Args)]
struct ScoreArgs {
#[command(flatten)]
study: StudyArgs,
filter: Option<String>,
#[arg(long)]
artifacts: String,
#[arg(long)]
group_by: Option<String>,
#[arg(long)]
out: Option<String>,
#[arg(long)]
dry_run: bool,
#[arg(long, default_value = "json")]
format: String,
}
#[derive(Args)]
struct ReportArgs {
run_id: String,
filter: Option<String>,
#[arg(long)]
group_by: Option<String>,
#[arg(long)]
out: Option<String>,
#[arg(long, default_value = "json")]
format: String,
}
#[derive(Args)]
struct ExportArgs {
run_id: String,
filter: Option<String>,
#[arg(long, default_value = export::FORMAT_ATIF)]
format: String,
#[arg(long)]
out: Option<String>,
#[arg(long)]
artifacts: Option<String>,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
match &cli.cmd {
None => {
Cli::command().print_help()?;
return Ok(());
}
Some(Cmd::Help(args)) => {
if args.full {
print_full_help()?;
} else {
Cli::command().print_help()?;
}
return Ok(());
}
Some(Cmd::Report(args)) => return report(args),
Some(Cmd::Export(args)) => {
return export::run(
&args.run_id,
args.filter.as_deref(),
&args.format,
args.out.as_deref(),
args.artifacts.as_deref(),
);
}
Some(Cmd::Publish(args)) => return publish_cmd(args).await,
Some(Cmd::Doctor(args)) => {
doctor::doctor(build_launch_command(&args.study, &cli.deprecated), args.fix).await
}
_ => {}
}
let study = match &cli.cmd {
Some(Cmd::List(a)) => &a.study,
Some(Cmd::Run(a)) => &a.study,
Some(Cmd::Score(a)) => &a.study,
_ => unreachable!("handled above"),
};
let progress = Arc::new(ProgressBar::hidden());
let progress_evt = progress.clone();
let command =
build_launch_command(study, &cli.deprecated).map_err(Box::<dyn std::error::Error>::from)?;
let host = Host::spawn(command).await?.on_event(move |n| {
if let Some(log) = n.as_log() {
progress_evt.suspend(|| eprintln!(" study: {}", log.message));
}
});
let info = host.initialize("mira-cli").await?;
eprintln!(
"study {} · protocol {} · {} evals",
info.study, info.protocol_version, info.evals
);
let listing = host.list_complete().await?;
match cli.cmd {
Some(Cmd::List(_)) => {
print_listing(&listing);
host.shutdown().await?;
Ok(())
}
Some(Cmd::Run(args)) => run(host, info, listing, *args, progress).await,
Some(Cmd::Score(args)) => score(host, info, listing, args).await,
None
| Some(Cmd::Help(_))
| Some(Cmd::Report(_))
| Some(Cmd::Export(_))
| Some(Cmd::Publish(_))
| Some(Cmd::Doctor(_)) => {
unreachable!("handled before host spawn")
}
}
}
fn print_full_help() -> std::io::Result<()> {
use std::io::Write;
let overview = "\
OVERVIEW
Mira is a Rust-first, code-first evaluation framework for agents and tools —
built for multi-turn, tool-using, long-running trajectories.
You write evals as code (in Rust, or any language that speaks the protocol);
this binary is the HOST. It owns the run end to end: it launches your eval
program (the `study`), enumerates what it advertises, plans the grid
(selection x target matrix x axes), executes each case over the protocol,
scores the results, then aggregates, reports, and saves the run. Every run is
saved by default under the results dir as `<run_id>/` (per-case results +
report + meta), so it can be resumed (`run --resume <run_id>`) and re-rendered
(`report <run_id>`); `--dry-run` opts out. Execution and scoring can be split
for long runs (`run --execute-only` then `score`).
Point it at any study, verb first: `mira run --study study.rs` resolves the
runner by extension — `.rs` is a single-file Rust study (cargo-script
frontmatter, shimmed onto stable), `.py` runs via `uv run`. Specific runners:
`--study-script` / `--study-uv` / `--study-python SCRIPT`, a crate via
`--study-bin NAME` / `--study-example NAME` (plus `--package` /
`--manifest-path`), or an arbitrary `--study-cmd \"...\"`. Save a repo's
invocation as `[launchers.NAME]` in mira.toml and select it with
`--launcher NAME` (or a `default_launcher`).";
let examples = "\
EXAMPLES
mira list --study study.rs # what the study advertises
mira run --study study.rs # run the whole matrix
mira run --study study.rs greet # selective (substring), like cargo test
mira run --study study.rs --tag smoke # only samples carrying a tag
mira run --study study.rs --targets sim --format junit --out results.xml
mira run --study study.rs --format html --out report.html # standalone viewer file
mira run --study study.rs --dry-run # don't save a run folder
mira run --study study.rs --resume <run_id> # finish an interrupted run
mira report <run_id> # re-render a saved run
mira export <run_id> --format atif # emit ATIF trajectory docs
mira run --study study.rs --execute-only --artifacts art/ # capture transcripts
mira score --study study.rs --artifacts art/ # score (or re-score) them
mira doctor --study study.rs # check config/study/saved runs (--fix repairs)
mira run --study-bin NAME # drive a crate study (workspace bin)
mira run --study study.py # drive a non-Rust study via uv (PEP 723)
mira run --study-python study.py # …or with a bare python3
mira run --launcher greet # use [launchers.greet] from mira.toml
mira run # use mira.toml's default_launcher";
let width = GUIDES.iter().map(|(name, _)| name.len()).max().unwrap_or(0);
let mut guides = format!("GUIDES ({DOCS_URL})\n");
for (name, desc) in GUIDES {
guides.push_str(&format!(" {name:<width$} {desc}\n"));
}
let guides = guides.trim_end();
let links = format!(
"\
LINKS
Repository: {REPO_URL}
Issues: {ISSUES_URL}
Docs: {DOCS_URL}
API docs: {API_DOCS_URL}
Agent skill: {SKILL_URL} (`mira` — teaches an agent to author/run evals)"
);
let mut root = Cli::command().about(None).after_help(None);
let flags = root.render_long_help();
let sub_help: Vec<(String, clap::builder::StyledStr)> = root
.get_subcommands_mut()
.filter(|c| c.get_name() != "help")
.map(|c| (c.get_name().to_string(), c.render_long_help()))
.collect();
let mut out = std::io::stdout().lock();
writeln!(out, "{ABOUT}\n")?;
writeln!(out, "{overview}\n")?;
write!(out, "{flags}")?;
for (name, help) in sub_help {
writeln!(out, "\nmira {name}")?;
write!(out, "{help}")?;
}
writeln!(out, "\n{examples}\n")?;
writeln!(out, "{guides}\n")?;
writeln!(out, "{links}")?;
Ok(())
}
fn build_launch_command(study: &StudyArgs, dep: &DeprecatedLaunch) -> Result<Command, String> {
let cli = &merge_launch(study, dep)?;
let needs_config = cli.launcher.is_some() || !cli_sets_mode(cli);
let cfg = if needs_config {
config::Config::load()
} else {
config::Config::default()
};
build_command(&resolve_launcher(cli, &cfg)?)
}
fn cli_sets_mode(cli: &Launcher) -> bool {
cli.cmd.is_some()
|| cli.script.is_some()
|| cli.bin.is_some()
|| cli.example.is_some()
|| cli.uv.is_some()
|| cli.python.is_some()
|| cli.python3.is_some()
}
fn resolve_launcher(
cli: &Launcher,
cfg: &config::Config,
) -> Result<config::LauncherConfig, String> {
let mut base = match &cli.launcher {
Some(name) => cfg.launcher(name)?,
None if !cli_sets_mode(cli) => match &cfg.default_launcher {
Some(name) => cfg.launcher(name)?,
None => config::LauncherConfig::default(),
},
None => config::LauncherConfig::default(),
};
if cli_sets_mode(cli) {
base.cmd = cli.cmd.clone();
base.script = cli.script.clone();
base.bin = cli.bin.clone();
base.example = cli.example.clone();
base.uv = cli.uv.clone();
base.python = cli.python.clone();
base.python3 = cli.python3.clone();
}
base.package = cli.package.clone().or(base.package);
base.manifest_path = cli.manifest_path.clone().or(base.manifest_path);
Ok(base)
}
fn build_command(launcher: &config::LauncherConfig) -> Result<Command, String> {
if let Some(raw) = &launcher.cmd {
let mut parts = raw.split_whitespace();
let program = parts.next().unwrap_or("false");
let mut command = Command::new(program);
command.args(parts);
return Ok(command);
}
if let Some(script) = &launcher.script {
let path = script.split_whitespace().next().unwrap_or(script);
if std::env::var_os("MIRA_SCRIPT_NATIVE").is_some() {
let mut command = Command::new("cargo");
command.arg("-Zscript").arg(path);
return Ok(command);
}
let (manifest, target_dir) = materialize_script(Path::new(path))?;
let mut command = Command::new("cargo");
command
.arg("run")
.arg("-q")
.arg("--manifest-path")
.arg(manifest)
.arg("--target-dir")
.arg(target_dir);
return Ok(command);
}
if let Some(script) = &launcher.uv {
let mut command = Command::new("uv");
command.arg("run").args(script.split_whitespace());
return Ok(command);
}
if let Some(script) = &launcher.python {
let mut command = Command::new("python");
command.args(script.split_whitespace());
return Ok(command);
}
if let Some(script) = &launcher.python3 {
let mut command = Command::new("python3");
command.args(script.split_whitespace());
return Ok(command);
}
let mut command = Command::new("cargo");
command.arg("run").arg("-q");
if let Some(pkg) = &launcher.package {
command.arg("-p").arg(pkg);
}
if let Some(bin) = &launcher.bin {
command.arg("--bin").arg(bin);
} else if let Some(example) = &launcher.example {
command.arg("--example").arg(example);
} else {
command.arg("--bin").arg("greet");
}
if let Some(manifest) = &launcher.manifest_path {
command.arg("--manifest-path").arg(manifest);
}
Ok(command)
}
fn materialize_script(script: &Path) -> Result<(PathBuf, PathBuf), String> {
let src = std::fs::read_to_string(script)
.map_err(|e| format!("cannot read study script {}: {e}", script.display()))?;
let (manifest_toml, body, skipped_lines) = parse_script(&src)
.map_err(|e| format!("invalid study script {}: {e}", script.display()))?;
let script_dir = script
.parent()
.filter(|p| !p.as_os_str().is_empty())
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
let script_dir = std::fs::canonicalize(&script_dir)
.map_err(|e| format!("cannot resolve script directory: {e}"))?;
let stem = script
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("study");
let crate_name = sanitize_crate_name(stem);
let manifest = render_manifest(&manifest_toml, &crate_name, &script_dir)?;
let main_rs = format!("{}{}", "\n".repeat(skipped_lines), body);
let hash = short_hash(&[&manifest, &main_rs, script_dir.to_string_lossy().as_ref()]);
let base = std::env::temp_dir().join("mira-script");
let crate_dir = base.join(format!("{crate_name}-{hash}"));
let src_dir = crate_dir.join("src");
std::fs::create_dir_all(&src_dir)
.map_err(|e| format!("cannot create script cache {}: {e}", src_dir.display()))?;
write_if_changed(&crate_dir.join("Cargo.toml"), &manifest)?;
write_if_changed(&src_dir.join("main.rs"), &main_rs)?;
Ok((crate_dir.join("Cargo.toml"), base.join("target")))
}
fn parse_script(src: &str) -> Result<(String, String, usize), String> {
let mut lines = src.lines().peekable();
let mut skipped = 0usize;
if lines.peek().is_some_and(|l| l.starts_with("#!")) {
lines.next();
skipped += 1;
}
while lines.peek().is_some_and(|l| l.trim().is_empty()) {
lines.next();
skipped += 1;
}
if lines.peek().map(|l| l.trim_end()) != Some("---") {
let body = src.lines().skip(skipped).collect::<Vec<_>>().join("\n");
return Ok((String::new(), body, skipped));
}
lines.next();
skipped += 1;
let mut manifest = String::new();
let mut closed = false;
for line in lines.by_ref() {
skipped += 1;
if line.trim_end() == "---" {
closed = true;
break;
}
manifest.push_str(line);
manifest.push('\n');
}
if !closed {
return Err("frontmatter opened with `---` but never closed".into());
}
let body = src.lines().skip(skipped).collect::<Vec<_>>().join("\n");
Ok((manifest, body, skipped))
}
fn render_manifest(
frontmatter: &str,
crate_name: &str,
script_dir: &Path,
) -> Result<String, String> {
let mut doc: toml::Table =
toml::from_str(frontmatter).map_err(|e| format!("frontmatter is not valid TOML: {e}"))?;
let pkg = doc
.entry("package".to_string())
.or_insert_with(|| toml::Value::Table(Default::default()));
if let Some(pkg) = pkg.as_table_mut() {
pkg.entry("name".to_string())
.or_insert_with(|| toml::Value::String(crate_name.to_string()));
pkg.entry("version".to_string())
.or_insert_with(|| toml::Value::String("0.0.0".to_string()));
pkg.entry("edition".to_string())
.or_insert_with(|| toml::Value::String("2024".to_string()));
pkg.insert("publish".to_string(), toml::Value::Boolean(false));
}
for key in ["dependencies", "dev-dependencies", "build-dependencies"] {
if let Some(table) = doc.get_mut(key).and_then(|v| v.as_table_mut()) {
for (_name, dep) in table.iter_mut() {
if let Some(dep) = dep.as_table_mut()
&& let Some(toml::Value::String(p)) = dep.get("path").cloned()
{
let anchored = script_dir.join(&p);
let abs = std::fs::canonicalize(&anchored).unwrap_or(anchored);
dep.insert(
"path".to_string(),
toml::Value::String(abs.to_string_lossy().into_owned()),
);
}
}
}
}
let mut manifest =
toml::to_string(&doc).map_err(|e| format!("cannot serialize generated manifest: {e}"))?;
manifest.push_str(&format!(
"\n[[bin]]\nname = \"{crate_name}\"\npath = \"src/main.rs\"\n\n[workspace]\n"
));
Ok(manifest)
}
fn sanitize_crate_name(stem: &str) -> String {
let mut name: String = stem
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '-' {
c
} else {
'_'
}
})
.collect();
if name.is_empty() || name.chars().next().is_some_and(|c| c.is_ascii_digit()) {
name.insert_str(0, "study-");
}
name
}
fn short_hash(parts: &[&str]) -> String {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
for p in parts {
p.hash(&mut h);
0u8.hash(&mut h); }
format!("{:016x}", h.finish())
}
fn write_if_changed(path: &Path, content: &str) -> Result<(), String> {
if std::fs::read_to_string(path).ok().as_deref() == Some(content) {
return Ok(());
}
std::fs::write(path, content).map_err(|e| format!("cannot write {}: {e}", path.display()))
}
async fn run(
host: Host,
info: InitializeResult,
listing: ListResult,
args: RunArgs,
progress: Arc<ProgressBar>,
) -> Result<(), Box<dyn std::error::Error>> {
let format = Format::from_str(&args.format)?;
let started_now = now_unix();
let selection = resolve_selection(&args).map_err(Box::<dyn std::error::Error>::from)?;
validate_selection(&selection, &listing).map_err(Box::<dyn std::error::Error>::from)?;
let timeouts = resolve_timeouts(&args, &selection, &listing);
let plan = plan_grid(&listing, &args, &selection, &timeouts);
if plan.is_empty() {
eprintln!("no cases matched the selection");
}
if args.execute_only {
require_capability(&info, capabilities::EXECUTE, "--execute-only")?;
let dir = args.artifacts.as_ref().expect("clap requires artifacts");
return execute_only(host, &plan, dir).await;
}
let run_store: Option<(String, PathBuf, u64)> = if args.dry_run {
None
} else {
let base = config::Config::load().results_dir();
match &args.resume {
Some(run_id) => {
let dir = config::run_dir(&base, run_id);
let started = config::load_meta(&dir)
.map(|m| m.started_unix)
.unwrap_or(started_now);
Some((run_id.clone(), dir, started))
}
None => {
let run_id = new_run_id_at(started_now);
let dir = config::run_dir(&base, &run_id);
Some((run_id, dir, started_now))
}
}
};
let mut done: BTreeMap<String, RunResult> = BTreeMap::new();
let environment = collect_environment();
if let Some((run_id, dir, started)) = &run_store {
for r in config::load_case_results(dir) {
done.insert(r.key(), r);
}
if args.resume.is_some() {
let have = plan.iter().filter(|c| done.contains_key(&c.key())).count();
eprintln!(
"resuming run {run_id}: {have}/{} case(s) already done",
plan.len()
);
}
let header = RunMeta {
format: RUN_META_FORMAT,
run_id: run_id.clone(),
study: info.study.clone(),
study_version: info.study_version.clone(),
started_unix: *started,
finished_unix: 0,
environment: environment.clone(),
summary: RunSummary::default(),
};
config::init_run(dir, &header)?;
}
let resumable = plan.iter().filter(|c| done.contains_key(&c.key())).count();
if !plan.is_empty() && std::io::stderr().is_terminal() {
progress.set_draw_target(ProgressDrawTarget::stderr());
progress.set_style(
ProgressStyle::with_template(
"{spinner:.green} [{elapsed_precise}] [{bar:30.cyan/blue}] \
{pos}/{len} (eta {eta}) {msg}",
)
.unwrap()
.progress_chars("=>-"),
);
}
progress.set_length(plan.len() as u64);
progress.set_position(resumable as u64);
let todo: Vec<CaseSpec> = plan
.iter()
.filter(|case| !done.contains_key(&case.key()))
.cloned()
.collect();
let cfg = concurrency(&args);
{
let handle = host.handle();
let write_dir = run_store.as_ref().map(|(_, dir, _)| dir.clone());
exec::run_cases(
todo,
&cfg,
|case| {
let handle = handle.clone();
async move {
handle
.run(
&case.eval,
&case.sample,
&case.target,
&case.params,
case.trial,
)
.await
}
},
|case, result| {
let key = case.key();
progress.set_message(key.clone());
if let Some(dir) = &write_dir
&& let Err(e) = config::write_case_result(dir, &key, &result)
{
progress.suspend(|| eprintln!("warning: failed to write case result: {e}"));
}
done.insert(key, result);
progress.inc(1);
},
)
.await;
}
progress.finish_and_clear();
host.shutdown().await?;
let results: Vec<RunResult> = plan
.iter()
.filter_map(|case| done.get(&case.key()).cloned())
.collect();
report::print_results(&results);
let group_vals = args
.group_by
.as_deref()
.map(|key| group_values(&results, key, &listing));
let group = match (args.group_by.as_deref(), group_vals.as_deref()) {
(Some(key), Some(values)) => {
report::print_group_breakdown(&results, key, values);
Some(report::Group { key, values })
}
_ => None,
};
if let Some(path) = &args.out {
std::fs::write(path, report::render_with_group(&results, format, group))?;
eprintln!("\nwrote {path} ({:?})", format);
}
if let Some((run_id, dir, started)) = &run_store {
let meta = RunMeta {
format: RUN_META_FORMAT,
run_id: run_id.clone(),
study: info.study.clone(),
study_version: info.study_version.clone(),
started_unix: *started,
finished_unix: now_unix(),
environment,
summary: RunSummary::of(&results),
};
config::finalize_run(dir, &meta, &results, group)?;
eprintln!("\nsaved run {run_id} to {}", dir.display());
if let Some(target) = &args.publish {
publish_results(target, &args.publish_conn, &meta, &results).await?;
}
}
let failed = results
.iter()
.any(|r| !r.skipped && !report::is_na(r) && !r.passed);
std::process::exit(if failed { 1 } else { 0 });
}
fn collect_environment() -> Option<mira::run::Environment> {
let cfg = config::Config::load();
cfg.environment
.enabled
.then(|| env::collect(&cfg.environment.labels))
.flatten()
}
async fn publish_cmd(args: &PublishArgs) -> Result<(), Box<dyn std::error::Error>> {
let base = config::Config::load().results_dir();
let dir = config::run_dir(&base, &args.run_id);
let meta = config::load_meta(&dir).ok_or_else(|| {
format!(
"no saved run '{}' found under {}",
args.run_id,
dir.display()
)
})?;
let mut results = config::load_case_results(&dir);
if let Some(f) = &args.filter {
results.retain(|r| r.key().contains(f.as_str()));
}
if results.is_empty() {
return Err(format!("no case results to publish for run {}", args.run_id).into());
}
publish_results(&args.to, &args.conn, &meta, &results).await
}
async fn publish_results(
target: &str,
conn: &PublishConn,
meta: &RunMeta,
results: &[RunResult],
) -> Result<(), Box<dyn std::error::Error>> {
if target != "everruns" {
return Err(
format!("unknown publish target '{target}': only 'everruns' is supported").into(),
);
}
let outcome = mira_publish_everruns::publish(meta, results, &conn.to_options()).await?;
eprintln!(
"published {} eval(s), {} case(s) to everruns",
outcome.evals, outcome.cases
);
for id in &outcome.run_ids {
eprintln!(" run {id}");
}
Ok(())
}
fn report(args: &ReportArgs) -> Result<(), Box<dyn std::error::Error>> {
let format = Format::from_str(&args.format)?;
let base = config::Config::load().results_dir();
let dir = config::run_dir(&base, &args.run_id);
let mut results = config::load_case_results(&dir);
if let Some(f) = &args.filter {
results.retain(|r| r.key().contains(f.as_str()));
}
if results.is_empty() {
eprintln!(
"no case results for run {} (looked in {}/cases)",
args.run_id,
dir.display()
);
}
report::print_results(&results);
let empty = ListResult { evals: Vec::new() };
let group_vals = args
.group_by
.as_deref()
.map(|key| group_values(&results, key, &empty));
let group = match (args.group_by.as_deref(), group_vals.as_deref()) {
(Some(key), Some(values)) => {
report::print_group_breakdown(&results, key, values);
Some(report::Group { key, values })
}
_ => None,
};
if let Some(path) = &args.out {
std::fs::write(path, report::render_with_group(&results, format, group))?;
eprintln!("\nwrote {path} ({:?})", format);
}
if args.filter.is_none()
&& let Some(mut meta) = config::load_meta(&dir)
{
meta.summary = RunSummary::of(&results);
config::finalize_run(&dir, &meta, &results, group)?;
}
Ok(())
}
fn resolve_timeouts(args: &RunArgs, sel: &Selection, listing: &ListResult) -> Timeouts {
use std::collections::BTreeSet;
let cfg = config::Config::load();
let known: BTreeSet<&str> = listing
.evals
.iter()
.flat_map(|e| e.targets.iter().map(|t| t.label.as_str()))
.collect();
let mut per_target = BTreeMap::new();
for (label, tcfg) in &cfg.targets {
let Some(secs) = tcfg.timeout else { continue };
if !known.contains(label.as_str()) {
eprintln!(
"warning: mira.toml [targets.{label:?}] timeout set, but no target {label:?} is declared"
);
}
per_target.insert(label.clone(), secs);
}
Timeouts {
cli: args.timeout,
preset: sel.timeout,
per_target,
}
}
fn concurrency(args: &RunArgs) -> Concurrency {
let mut cfg = Concurrency::new(args.max_concurrent);
cfg.adaptive = !args.no_adaptive;
cfg.max_retries = args.max_retries;
if let Some(spec) = &args.provider_concurrency {
for entry in spec.split(',') {
let entry = entry.trim();
if entry.is_empty() {
continue;
}
if let Some((provider, n)) = entry.split_once('=')
&& let Ok(limit) = n.trim().parse::<usize>()
{
cfg = cfg.provider(provider.trim(), limit);
} else {
eprintln!("ignoring malformed --provider-concurrency entry: {entry:?}");
}
}
}
cfg
}
fn require_capability(
info: &InitializeResult,
cap: &str,
feature: &str,
) -> Result<(), Box<dyn std::error::Error>> {
if info.capabilities.iter().any(|c| c == cap) {
return Ok(());
}
Err(format!(
"study {} doesn't support {feature}: it doesn't advertise the `{cap}` capability",
info.study
)
.into())
}
async fn execute_only(
host: Host,
plan: &[CaseSpec],
dir: &str,
) -> Result<(), Box<dyn std::error::Error>> {
std::fs::create_dir_all(dir)?;
let mut wrote = 0usize;
for case in plan {
let path = artifact_path(dir, &case.key());
if path.exists() {
continue;
}
let result = host
.execute(
&case.eval,
&case.sample,
&case.target,
&case.params,
case.trial,
)
.await?;
std::fs::write(&path, serde_json::to_string_pretty(&result)?)?;
wrote += 1;
}
host.shutdown().await?;
eprintln!("executed {wrote} case(s); artifacts in {dir}");
eprintln!("score them with: mira score --artifacts {dir}");
Ok(())
}
async fn score(
host: Host,
info: InitializeResult,
listing: ListResult,
args: ScoreArgs,
) -> Result<(), Box<dyn std::error::Error>> {
require_capability(&info, capabilities::SCORE, "mira score")?;
let format = Format::from_str(&args.format)?;
let started_unix = now_unix();
let mut artifacts = load_artifacts(&args.artifacts);
if let Some(f) = &args.filter {
artifacts.retain(|a| a.key().contains(f.as_str()));
}
if artifacts.is_empty() {
eprintln!("no artifacts in {}", args.artifacts);
}
let mut results = Vec::with_capacity(artifacts.len());
for artifact in &artifacts {
if artifact.skipped {
results.push(skipped_result(artifact));
} else {
results.push(host.score(artifact).await?);
}
}
host.shutdown().await?;
report::print_results(&results);
let group_vals = args
.group_by
.as_deref()
.map(|key| group_values(&results, key, &listing));
let group = match (args.group_by.as_deref(), group_vals.as_deref()) {
(Some(key), Some(values)) => {
report::print_group_breakdown(&results, key, values);
Some(report::Group { key, values })
}
_ => None,
};
if let Some(path) = &args.out {
std::fs::write(path, report::render_with_group(&results, format, group))?;
eprintln!("\nwrote {path} ({:?})", format);
}
if !args.dry_run {
let base = config::Config::load().results_dir();
let run_id = new_run_id_at(started_unix);
let dir = config::run_dir(&base, &run_id);
for r in &results {
config::write_case_result(&dir, &r.key(), r)?;
}
let meta = RunMeta {
format: RUN_META_FORMAT,
run_id: run_id.clone(),
study: info.study.clone(),
study_version: info.study_version.clone(),
started_unix,
finished_unix: now_unix(),
environment: collect_environment(),
summary: RunSummary::of(&results),
};
config::finalize_run(&dir, &meta, &results, group)?;
eprintln!("\nsaved run {run_id} to {}", dir.display());
}
let failed = results
.iter()
.any(|r| !r.skipped && !report::is_na(r) && !r.passed);
std::process::exit(if failed { 1 } else { 0 });
}
fn skipped_result(a: &ExecuteResult) -> RunResult {
RunResult {
eval: a.eval.clone(),
sample: a.sample.clone(),
target: a.target.clone(),
params: a.params.clone(),
trial: a.trial,
trials: a.trials,
seed: a.seed,
input: Vec::new(),
expected: None,
passed: false,
aggregate: 0.0,
scores: Vec::new(),
transcript: TranscriptSummary::of(&a.transcript),
skipped: true,
}
}
fn artifact_path(dir: &str, key: &str) -> std::path::PathBuf {
Path::new(dir).join(format!("{}.json", config::encode_key(key)))
}
pub(crate) fn load_artifacts(dir: &str) -> Vec<ExecuteResult> {
let mut out = Vec::new();
let entries = match std::fs::read_dir(dir) {
Ok(entries) => entries,
Err(e) => {
eprintln!("warning: cannot read artifacts dir {dir}: {e}");
return out;
}
};
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("json") {
continue;
}
match std::fs::read_to_string(&path) {
Ok(text) => match serde_json::from_str::<ExecuteResult>(&text) {
Ok(result) => out.push(result),
Err(e) => {
eprintln!(
"warning: skipping {}: invalid artifact JSON: {e}",
path.display()
)
}
},
Err(e) => eprintln!("warning: skipping {}: {e}", path.display()),
}
}
out.sort_by_key(|a| a.key());
out
}
fn axis_combinations(eval: &mira::protocol::EvalInfo) -> Vec<BTreeMap<String, String>> {
let mut combos = vec![BTreeMap::new()];
for axis in &eval.axes {
let mut next = Vec::new();
for combo in &combos {
for value in &axis.values {
let mut c = combo.clone();
c.insert(axis.name.clone(), value.clone());
next.push(c);
}
}
if !next.is_empty() {
combos = next;
}
}
combos
}
struct Selection {
filter: Option<String>,
tag: Option<String>,
evals: Option<Vec<String>>,
samples: Option<Vec<String>>,
axes: BTreeMap<String, Vec<String>>,
timeout: Option<u64>,
}
#[derive(Default)]
struct Timeouts {
cli: Option<u64>,
preset: Option<u64>,
per_target: BTreeMap<String, u64>,
}
impl Timeouts {
fn for_target(&self, label: &str) -> Option<Duration> {
self.cli
.or_else(|| self.per_target.get(label).copied())
.or(self.preset)
.map(Duration::from_secs)
}
}
fn split_csv(s: &str) -> Vec<String> {
s.split(',')
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty())
.collect()
}
fn resolve_selection(args: &RunArgs) -> Result<Selection, String> {
let preset = match &args.preset {
Some(name) => config::Config::load().preset(name)?,
None => config::Preset::default(),
};
let filter = args.filter.clone();
let tag = args.tag.clone().or(preset.tag);
let evals = match &args.evals {
Some(s) => Some(split_csv(s)),
None => (!preset.evals.is_empty()).then(|| preset.evals.clone()),
};
let samples = match &args.samples {
Some(s) => Some(split_csv(s)),
None => (!preset.samples.is_empty()).then(|| preset.samples.clone()),
};
let mut axes: BTreeMap<String, Vec<String>> = preset.axes.clone();
let targets = match &args.targets {
Some(s) => split_csv(s),
None => preset.targets.clone(),
};
if !targets.is_empty() {
axes.insert("target".to_string(), targets);
}
for spec in &args.axes {
let (name, vals) = spec
.split_once('=')
.ok_or_else(|| format!("--axis expects NAME=V1,V2, got {spec:?}"))?;
let values = split_csv(vals);
if values.is_empty() {
return Err(format!("--axis {name}= lists no values"));
}
axes.insert(name.trim().to_string(), values);
}
Ok(Selection {
filter,
tag,
evals,
samples,
axes,
timeout: preset.timeout,
})
}
fn validate_selection(sel: &Selection, listing: &ListResult) -> Result<(), String> {
use std::collections::BTreeSet;
let mut declared: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
for eval in &listing.evals {
let t = declared.entry("target".to_string()).or_default();
for m in &eval.targets {
t.insert(m.label.clone());
}
for a in &eval.axes {
let e = declared.entry(a.name.clone()).or_default();
for v in &a.values {
e.insert(v.clone());
}
}
}
let listed = |set: &BTreeSet<String>| {
let mut v: Vec<&str> = set.iter().map(String::as_str).collect();
v.sort_unstable();
v.join(", ")
};
for (name, allowed) in &sel.axes {
let Some(valid) = declared.get(name) else {
let names: BTreeSet<String> = declared.keys().cloned().collect();
return Err(format!(
"unknown axis {name:?} (declared: {})",
listed(&names)
));
};
for v in allowed {
if !valid.iter().any(|d| mira::glob_match(v, d)) {
return Err(format!(
"axis {name:?} has no value matching {v:?} (declared: {})",
listed(valid)
));
}
}
}
if let Some(evals) = &sel.evals {
let known: BTreeSet<String> = listing.evals.iter().map(|e| e.name.clone()).collect();
for e in evals {
if !known.iter().any(|k| mira::glob_match(e, k)) {
return Err(format!(
"no eval matching {e:?} (declared: {})",
listed(&known)
));
}
}
}
Ok(())
}
fn axes_allowed(sel: &Selection, params: &mira::Params) -> bool {
params.iter().all(|(name, value)| {
sel.axes
.get(name)
.is_none_or(|allowed| allowed.iter().any(|p| mira::glob_match(p, value)))
})
}
fn plan_grid(
listing: &ListResult,
args: &RunArgs,
sel: &Selection,
timeouts: &Timeouts,
) -> Vec<CaseSpec> {
let mut plan = Vec::new();
for eval in &listing.evals {
if let Some(evals) = &sel.evals
&& !evals.iter().any(|p| mira::glob_match(p, &eval.name))
{
continue;
}
let combos = axis_combinations(eval);
let trials = args.trials.unwrap_or(eval.trials).max(1);
let seed_base = args.seed.or(eval.seed);
for sample in &eval.samples {
if let Some(tag) = &sel.tag
&& !sample.tags.contains(tag)
{
continue;
}
if let Some(allow) = &sel.samples
&& !allow.iter().any(|p| mira::glob_match(p, &sample.id))
{
continue;
}
for target in &eval.targets {
if let Some(allow) = sel.axes.get("target")
&& !allow.iter().any(|p| mira::glob_match(p, &target.label))
{
continue;
}
for params in &combos {
if !axes_allowed(sel, params) {
continue;
}
let key = mira::case_key(&eval.name, &sample.id, &target.label, params);
if let Some(f) = &sel.filter
&& !key.contains(f.as_str())
{
continue;
}
let timeout = timeouts.for_target(&target.label);
for index in 0..trials {
plan.push(CaseSpec {
eval: eval.name.clone(),
sample: sample.id.clone(),
target: target.label.clone(),
provider: target.provider.clone(),
params: params.clone(),
timeout,
trial: Trial {
index,
count: trials,
seed: seed_base.map(|s| s.wrapping_add(index as u64)),
},
});
}
}
}
}
}
plan
}
type MetaIndex = BTreeMap<(String, String), mira::Metadata>;
fn meta_indexes(listing: &ListResult) -> (MetaIndex, MetaIndex) {
let mut sample_meta = MetaIndex::new();
let mut model_meta = MetaIndex::new();
for eval in &listing.evals {
for s in &eval.samples {
if !s.metadata.is_empty() {
sample_meta.insert((eval.name.clone(), s.id.clone()), s.metadata.clone());
}
}
for m in &eval.targets {
if !m.metadata.is_empty() {
model_meta.insert((eval.name.clone(), m.label.clone()), m.metadata.clone());
}
}
}
(sample_meta, model_meta)
}
fn group_value(
r: &RunResult,
key: &str,
sample_meta: &MetaIndex,
model_meta: &MetaIndex,
) -> Option<String> {
if let Some(v) = r.params.get(key) {
return Some(v.clone());
}
if let Some(v) = sample_meta
.get(&(r.eval.clone(), r.sample.clone()))
.and_then(|m| m.get(key))
{
return Some(mira::metadata_display(v));
}
if let Some(v) = model_meta
.get(&(r.eval.clone(), r.target.clone()))
.and_then(|m| m.get(key))
{
return Some(mira::metadata_display(v));
}
r.transcript.metadata.get(key).map(mira::metadata_display)
}
fn group_values(results: &[RunResult], key: &str, listing: &ListResult) -> Vec<Option<String>> {
let (sample_meta, model_meta) = meta_indexes(listing);
results
.iter()
.map(|r| group_value(r, key, &sample_meta, &model_meta))
.collect()
}
fn print_listing(listing: &ListResult) {
for eval in &listing.evals {
let desc = if eval.description.is_empty() {
String::new()
} else {
format!(" — {}", eval.description)
};
let trials = if eval.trials > 1 {
let seed = eval.seed.map(|s| format!(", seed={s}")).unwrap_or_default();
format!(", trials={}{seed}", eval.trials)
} else {
String::new()
};
println!(
"{}{desc} (max_turns={}{trials})",
eval.name, eval.max_turns
);
println!(
" samples: {}",
eval.samples
.iter()
.map(|s| {
let mut label = s.id.clone();
if !s.tags.is_empty() {
label.push_str(&format!(" [{}]", s.tags.join(",")));
}
if !s.metadata.is_empty() {
label.push_str(&format!(" {{{}}}", fmt_meta(&s.metadata)));
}
label
})
.collect::<Vec<_>>()
.join(", ")
);
println!(" scorers: {}", eval.scorers.join(", "));
println!(
" targets: {}",
eval.targets
.iter()
.map(|m| {
let mut label = m.label.clone();
if !m.available {
label.push_str(" (unavailable)");
}
if !m.metadata.is_empty() {
label.push_str(&format!(" {{{}}}", fmt_meta(&m.metadata)));
}
label
})
.collect::<Vec<_>>()
.join(", ")
);
if !eval.axes.is_empty() {
let axes: Vec<String> = eval
.axes
.iter()
.map(|a| format!("{}=[{}]", a.name, a.values.join(",")))
.collect();
println!(" axes: {}", axes.join(", "));
}
if !eval.metadata.is_empty() {
println!(" meta: {}", fmt_meta(&eval.metadata));
}
}
}
fn fmt_meta(meta: &mira::Metadata) -> String {
meta.iter()
.map(|(k, v)| format!("{k}={}", mira::metadata_display(v)))
.collect::<Vec<_>>()
.join(", ")
}
#[cfg(test)]
mod tests {
use super::*;
fn empty_launcher() -> Launcher {
Launcher {
launcher: None,
bin: None,
example: None,
cmd: None,
script: None,
uv: None,
python: None,
python3: None,
package: None,
manifest_path: None,
}
}
fn cfg(text: &str) -> config::Config {
config::Config::parse(text).unwrap()
}
fn run_args() -> RunArgs {
RunArgs {
study: StudyArgs::default(),
filter: None,
tag: None,
targets: None,
samples: None,
evals: None,
axes: Vec::new(),
preset: None,
trials: None,
seed: None,
group_by: None,
out: None,
format: "json".into(),
dry_run: false,
resume: None,
max_concurrent: 8,
provider_concurrency: None,
no_adaptive: false,
max_retries: 4,
execute_only: false,
artifacts: None,
timeout: None,
publish: None,
publish_conn: PublishConn::default(),
}
}
fn sample(id: &str) -> mira::protocol::SampleInfo {
mira::protocol::SampleInfo {
id: id.into(),
tags: Vec::new(),
metadata: Default::default(),
}
}
fn target(label: &str) -> mira::protocol::TargetInfo {
mira::protocol::TargetInfo {
label: label.into(),
provider: "sim".into(),
available: true,
metadata: Default::default(),
}
}
fn listing() -> ListResult {
let eval = |name: &str| mira::protocol::EvalInfo {
name: name.into(),
description: String::new(),
samples: vec![sample("france"), sample("spain")],
next_cursor: None,
scorers: vec!["contains".into()],
targets: vec![target("sim"), target("anthropic/opus")],
axes: Vec::new(),
max_turns: 0,
trials: 0,
seed: None,
metadata: Default::default(),
};
ListResult {
evals: vec![eval("greet"), eval("coding")],
}
}
fn keys(plan: &[CaseSpec]) -> std::collections::BTreeSet<String> {
plan.iter()
.map(|c| format!("{}/{}@{}", c.eval, c.sample, c.target))
.collect()
}
#[test]
fn glob_targets_select_by_pattern() {
let args = RunArgs {
targets: Some("anthropic/*".into()),
..run_args()
};
let sel = resolve_selection(&args).unwrap();
let plan = plan_grid(&listing(), &args, &sel, &Timeouts::default());
assert!(keys(&plan).iter().all(|k| k.ends_with("@anthropic/opus")));
assert_eq!(plan.len(), 4); }
#[test]
fn glob_samples_and_evals_narrow_the_grid() {
let args = RunArgs {
samples: Some("s*".into()), evals: Some("greet".into()),
..run_args()
};
let sel = resolve_selection(&args).unwrap();
assert_eq!(
keys(&plan_grid(&listing(), &args, &sel, &Timeouts::default())),
["greet/spain@anthropic/opus", "greet/spain@sim"]
.into_iter()
.map(String::from)
.collect()
);
}
#[test]
fn preset_samples_apply_and_flags_override() {
let cfg = cfg("[presets.fr]\nsamples = \"france\"\n");
let preset = cfg.preset("fr").unwrap();
assert_eq!(preset.samples, vec!["france"]);
let args = RunArgs {
targets: Some("openai/*".into()),
..run_args()
};
let sel = resolve_selection(&args).unwrap();
let err = validate_selection(&sel, &listing()).unwrap_err();
assert!(err.contains("no value matching"), "{err}");
}
fn parts(cmd: &Command) -> (String, Vec<String>) {
let std = cmd.as_std();
let program = std.get_program().to_string_lossy().into_owned();
let args = std
.get_args()
.map(|a| a.to_string_lossy().into_owned())
.collect();
(program, args)
}
#[test]
fn guides_match_docs_readme() {
let readme = include_str!("../../../docs/README.md");
for (name, _) in GUIDES {
assert!(
readme.contains(&format!("]({name}.md)")),
"GUIDES entry `{name}` has no matching link in docs/README.md"
);
}
}
#[test]
fn root_readme_diagram_urls_render_off_repo() {
let readme = include_str!("../../../README.md");
assert!(
!readme.contains("src=\"docs/assets/"),
"root README diagrams need absolute URLs so crates.io and other off-repo renderers can load them"
);
}
#[test]
fn timeout_precedence_cli_then_per_target_then_preset() {
let per_target = BTreeMap::from([("anthropic/opus".to_string(), 300u64)]);
let t = Timeouts {
cli: Some(10),
preset: Some(120),
per_target: per_target.clone(),
};
assert_eq!(
t.for_target("anthropic/opus"),
Some(Duration::from_secs(10))
);
assert_eq!(t.for_target("sim"), Some(Duration::from_secs(10)));
let t = Timeouts {
cli: None,
preset: Some(120),
per_target,
};
assert_eq!(
t.for_target("anthropic/opus"),
Some(Duration::from_secs(300))
);
assert_eq!(t.for_target("sim"), Some(Duration::from_secs(120)));
let t = Timeouts {
cli: None,
preset: None,
per_target: BTreeMap::new(),
};
assert_eq!(t.for_target("sim"), None);
}
#[test]
fn uv_launcher_prepends_run() {
let l = config::LauncherConfig {
uv: Some("study.py".into()),
..Default::default()
};
let (program, args) = parts(&build_command(&l).unwrap());
assert_eq!(program, "uv");
assert_eq!(args, ["run", "study.py"]);
}
#[test]
fn python_launchers_take_script_directly() {
let l = config::LauncherConfig {
python: Some("study.py --flag".into()),
..Default::default()
};
let (program, args) = parts(&build_command(&l).unwrap());
assert_eq!(program, "python");
assert_eq!(args, ["study.py", "--flag"]);
let l = config::LauncherConfig {
python3: Some("examples/greet-python/study.py".into()),
..Default::default()
};
let (program, args) = parts(&build_command(&l).unwrap());
assert_eq!(program, "python3");
assert_eq!(args, ["examples/greet-python/study.py"]);
}
#[test]
fn cmd_wins_over_python_launchers() {
let l = config::LauncherConfig {
cmd: Some("echo hi".into()),
python3: Some("study.py".into()),
..Default::default()
};
let (program, args) = parts(&build_command(&l).unwrap());
assert_eq!(program, "echo");
assert_eq!(args, ["hi"]);
}
#[test]
fn defaults_to_greet_bin() {
let (program, args) = parts(&build_command(&config::LauncherConfig::default()).unwrap());
assert_eq!(program, "cargo");
assert_eq!(args, ["run", "-q", "--bin", "greet"]);
}
#[test]
fn named_launcher_supports_polyglot_modes() {
let cfg = cfg("default_launcher = \"py\"\n[launchers.py]\npython3 = \"study.py\"\n");
let l = resolve_launcher(&empty_launcher(), &cfg).unwrap();
assert_eq!(l.python3.as_deref(), Some("study.py"));
let (program, args) = parts(&build_command(&l).unwrap());
assert_eq!(program, "python3");
assert_eq!(args, ["study.py"]);
}
#[test]
fn named_launcher_resolves_from_config() {
let cfg = cfg("[launchers.greet]\nbin = \"greet\"\npackage = \"myapp\"\n");
let cli = Launcher {
launcher: Some("greet".into()),
..empty_launcher()
};
let l = resolve_launcher(&cli, &cfg).unwrap();
assert_eq!(l.bin.as_deref(), Some("greet"));
assert_eq!(l.package.as_deref(), Some("myapp"));
}
#[test]
fn default_launcher_applies_when_no_flag() {
let cfg = cfg("default_launcher = \"py\"\n[launchers.py]\ncmd = \"python s.py\"\n");
let l = resolve_launcher(&empty_launcher(), &cfg).unwrap();
assert_eq!(l.cmd.as_deref(), Some("python s.py"));
}
#[test]
fn explicit_mode_overrides_named_launcher() {
let cfg = cfg("[launchers.py]\ncmd = \"python s.py\"\n");
let cli = Launcher {
launcher: Some("py".into()),
bin: Some("other".into()),
..empty_launcher()
};
let l = resolve_launcher(&cli, &cfg).unwrap();
assert_eq!(l.bin.as_deref(), Some("other"));
assert!(
l.cmd.is_none(),
"explicit --bin must drop the named cmd mode"
);
}
#[test]
fn explicit_mode_suppresses_default_launcher() {
let cfg = cfg("default_launcher = \"py\"\n[launchers.py]\ncmd = \"python s.py\"\n");
let cli = Launcher {
bin: Some("greet".into()),
..empty_launcher()
};
let l = resolve_launcher(&cli, &cfg).unwrap();
assert_eq!(l.bin.as_deref(), Some("greet"));
assert!(l.cmd.is_none());
}
#[test]
fn package_overlays_named_launcher() {
let cfg = cfg("[launchers.greet]\nbin = \"greet\"\npackage = \"a\"\n");
let cli = Launcher {
launcher: Some("greet".into()),
package: Some("b".into()),
..empty_launcher()
};
let l = resolve_launcher(&cli, &cfg).unwrap();
assert_eq!(l.bin.as_deref(), Some("greet"));
assert_eq!(l.package.as_deref(), Some("b"));
}
#[test]
fn unknown_launcher_errors() {
let cfg = cfg("[launchers.greet]\nbin = \"greet\"\n");
let cli = Launcher {
launcher: Some("nope".into()),
..empty_launcher()
};
assert!(resolve_launcher(&cli, &cfg).is_err());
}
#[test]
fn bare_cli_with_no_config_defaults_to_greet() {
let l = resolve_launcher(&empty_launcher(), &config::Config::default()).unwrap();
assert!(l.bin.is_none() && l.cmd.is_none() && l.example.is_none());
let cmd = build_command(&l).unwrap();
let argv: Vec<_> = cmd
.as_std()
.get_args()
.map(|a| a.to_string_lossy().into_owned())
.collect();
assert_eq!(argv, vec!["run", "-q", "--bin", "greet"]);
}
#[test]
fn study_flag_infers_runner_by_extension() {
let study = |p: &str| StudyArgs {
study: Some(p.into()),
..Default::default()
};
let l = merge_launch(&study("examples/greet.rs"), &Default::default()).unwrap();
assert_eq!(l.script.as_deref(), Some("examples/greet.rs"));
let l = merge_launch(&study("study.py"), &Default::default()).unwrap();
assert_eq!(l.uv.as_deref(), Some("study.py"));
let err = merge_launch(&study("study.sh"), &Default::default()).unwrap_err();
assert!(err.contains("--study-cmd"), "{err}");
}
#[test]
fn study_flags_are_verb_scoped() {
assert!(Cli::try_parse_from(["mira", "run", "--study", "s.rs"]).is_ok());
assert!(Cli::try_parse_from(["mira", "--study", "s.rs", "run"]).is_err());
assert!(Cli::try_parse_from(["mira", "list", "--study-bin", "greet"]).is_ok());
assert!(Cli::try_parse_from(["mira", "doctor", "--study", "s.rs", "--fix"]).is_ok());
assert!(Cli::try_parse_from(["mira", "report", "id", "--study", "s.rs"]).is_err());
assert!(
Cli::try_parse_from(["mira", "run", "--study", "s.rs", "--study-bin", "x"]).is_err()
);
}
#[test]
fn deprecated_aliases_parse_in_both_positions() {
for argv in [
["mira", "--script", "s.rs", "run"],
["mira", "run", "--script", "s.rs"],
] {
let cli = Cli::try_parse_from(argv).unwrap();
assert_eq!(cli.deprecated.script.as_deref(), Some("s.rs"));
}
let cli = Cli::try_parse_from(["mira", "run", "--python3", "s.py"]).unwrap();
let l = merge_launch(&StudyArgs::default(), &cli.deprecated).unwrap();
assert_eq!(l.python3.as_deref(), Some("s.py"));
}
#[test]
fn merge_launch_passes_launcher_name_through() {
let study = StudyArgs {
launcher: Some("greet".into()),
..Default::default()
};
let l = merge_launch(&study, &Default::default()).unwrap();
assert_eq!(l.launcher.as_deref(), Some("greet"));
assert!(l.script.is_none() && l.bin.is_none());
}
#[test]
fn build_launch_command_end_to_end_from_study_flag() {
let script = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap() .parent()
.unwrap() .join("examples/greet.rs");
let study = StudyArgs {
study: Some(script.to_string_lossy().into_owned()),
..Default::default()
};
let cmd = build_launch_command(&study, &Default::default()).unwrap();
let (program, args) = parts(&cmd);
assert_eq!(program, "cargo");
assert!(args.contains(&"run".to_string()));
assert!(args.iter().any(|a| a.ends_with("Cargo.toml")));
}
#[test]
fn new_flags_beat_deprecated_aliases() {
let study = StudyArgs {
study_bin: Some("new".into()),
..Default::default()
};
let dep = DeprecatedLaunch {
script: Some("old.rs".into()),
..Default::default()
};
let l = merge_launch(&study, &dep).unwrap();
assert_eq!(l.bin.as_deref(), Some("new"));
assert!(
l.script.is_none(),
"deprecated --script must lose to --study-bin"
);
}
#[test]
fn deprecated_aliases_hidden_from_help() {
let mut root = Cli::command();
let run_help = root
.find_subcommand_mut("run")
.unwrap()
.render_long_help()
.to_string();
assert!(run_help.contains("--study"), "{run_help}");
assert!(run_help.contains("--study-python"), "{run_help}");
for old in ["--script", "--python", "--python3", "--uv"] {
assert!(
!run_help.contains(&format!("{old} ")) && !run_help.contains(&format!("{old}\n")),
"deprecated {old} leaked into help:\n{run_help}"
);
}
}
#[test]
fn parse_script_strips_shebang_and_frontmatter() {
let src = "#!/usr/bin/env -S cargo -Zscript\n\
---\n\
[dependencies]\n\
foo = \"1\"\n\
---\n\
fn main() {}\n";
let (manifest, body, skipped) = parse_script(src).unwrap();
assert_eq!(manifest, "[dependencies]\nfoo = \"1\"\n");
assert_eq!(body, "fn main() {}");
assert_eq!(skipped, 5);
}
#[test]
fn parse_script_allows_no_frontmatter() {
let (manifest, body, skipped) = parse_script("fn main() {}\n").unwrap();
assert!(manifest.is_empty());
assert_eq!(body, "fn main() {}");
assert_eq!(skipped, 0);
}
#[test]
fn parse_script_rejects_unterminated_frontmatter() {
let err = parse_script("---\n[dependencies]\nfn main() {}\n").unwrap_err();
assert!(err.contains("never closed"), "got: {err}");
}
#[test]
fn render_manifest_anchors_paths_and_adds_bin_workspace() {
let fm = "[dependencies]\n\
mira-eval = { path = \"../crates/mira-eval\" }\n\
tokio = { version = \"1\" }\n";
let script_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap() .parent()
.unwrap() .join("examples");
let out = render_manifest(fm, "greet", &script_dir).unwrap();
assert!(out.contains("[[bin]]"));
assert!(out.contains("name = \"greet\""));
assert!(out.contains("[workspace]"));
assert!(out.contains("edition = \"2024\""));
assert!(out.contains("crates/mira-eval"));
assert!(!out.contains("../crates/mira-eval"));
assert!(out.contains("version = \"1\""));
}
#[test]
fn sanitize_crate_name_handles_odd_stems() {
assert_eq!(sanitize_crate_name("greet"), "greet");
assert_eq!(sanitize_crate_name("my study.rs"), "my_study_rs");
assert_eq!(sanitize_crate_name("123"), "study-123");
}
#[test]
fn script_launcher_native_mode_uses_zscript() {
let l = config::LauncherConfig {
script: Some("study.rs".into()),
..Default::default()
};
unsafe { std::env::set_var("MIRA_SCRIPT_NATIVE", "1") };
let (program, args) = parts(&build_command(&l).unwrap());
unsafe { std::env::remove_var("MIRA_SCRIPT_NATIVE") };
assert_eq!(program, "cargo");
assert_eq!(args, ["-Zscript", "study.rs"]);
}
}