use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use anyhow::Context as _;
use argonauts_conductor::cli_runner::capabilities;
use argonauts_conductor::{CliRunner, boards_with, check_runner, fallback_planner, limits_service};
use argonauts_core::clock::SystemClock;
use argonauts_core::engine::{RepoContext, Services};
use argonauts_core::future::BoxFuture;
use argonauts_core::limits::{CostEstimate, HealthBoard, PlanUsageBoard, TrackRecords};
use argonauts_core::model_catalog::ModelCatalog;
use argonauts_core::names::ModelRef;
use argonauts_core::paths::create_private_dir_all;
use argonauts_core::registry::{ModelEntry, ModelRegistry, RegistryKind};
use argonauts_core::session_runner::{RunnerDirectory, RunnerError, SessionRunner};
use argonauts_core::settings::Settings;
use argonauts_core::steps::SchemaStepRunner;
use argonauts_core::{Adapter, Paths};
use argonauts_endpoints::EndpointRunner;
use argonauts_learning::records::LiveRecords;
use argonauts_policy::{SettingsReloader, approval_policy};
use argonauts_registry::models::{builtin_endpoint, endpoint_settings};
use time::OffsetDateTime;
use tokio::sync::watch;
use super::clis::{self, CliAdapters, RepoModels};
use crate::adapters::{builtin_adapters, opencode};
use crate::app_settings::AppSettings;
use crate::model_memory::ServerMemory;
#[derive(Clone)]
pub(crate) enum SettingsSource {
Fixed {
settings: Arc<Settings>,
turned_on: Vec<String>,
},
Live {
settings: watch::Receiver<Arc<Settings>>,
turned_on: watch::Receiver<Vec<String>>,
},
}
impl SettingsSource {
pub(crate) fn fixed(settings: Arc<Settings>, turned_on: Vec<String>) -> Self {
Self::Fixed {
settings,
turned_on,
}
}
pub(crate) fn live(settings: &AppSettings) -> Self {
Self::Live {
settings: settings.follow(),
turned_on: settings.follow_turned_on(),
}
}
fn current(&self) -> Arc<Settings> {
match self {
Self::Fixed { settings, .. } => Arc::clone(settings),
Self::Live { settings, .. } => Arc::clone(&settings.borrow()),
}
}
fn turned_on(&self) -> Vec<String> {
match self {
Self::Fixed { turned_on, .. } => turned_on.clone(),
Self::Live { turned_on, .. } => turned_on.borrow().clone(),
}
}
}
pub(crate) struct Directory {
settings: SettingsSource,
home: Paths,
repo: Option<RepoModels>,
registry: Arc<dyn ModelRegistry>,
plan_usage: Arc<dyn PlanUsageBoard>,
health: Arc<dyn HealthBoard>,
builtins: Vec<Arc<dyn Adapter>>,
built: Mutex<Built>,
}
struct Built {
settings: Arc<Settings>,
adapters: Vec<Arc<dyn Adapter>>,
clis: CliAdapters,
workers: HashMap<ModelRef, Arc<dyn SessionRunner>>,
}
impl Directory {
fn adapters(&self, settings: &Settings, clis: &mut CliAdapters) -> Vec<Arc<dyn Adapter>> {
let manifests = clis::manifests(
settings,
&self.home,
self.repo.as_ref(),
&self.settings.turned_on(),
);
let mut adapters: Vec<Arc<dyn Adapter>> = self
.builtins
.iter()
.map(|adapter| match adapter.id() {
"opencode" => Arc::new(opencode(settings, self.repo.as_ref())) as Arc<dyn Adapter>,
_ => adapter.clone(),
})
.collect();
adapters.extend(clis.adapters(manifests, &self.builtins, &self.home));
adapters
}
fn current(&self) -> MutexGuard<'_, Built> {
let settings = self.settings.current();
let mut built = self.built.lock().unwrap_or_else(PoisonError::into_inner);
if !Arc::ptr_eq(&built.settings, &settings) {
let built = &mut *built;
built.adapters = self.adapters(&settings, &mut built.clis);
built.settings = settings;
built.workers.clear();
}
built
}
fn provider(&self, worker: &ModelRef, adapter: Option<&Arc<dyn Adapter>>) -> String {
let provider = self.registry.provider(worker);
match adapter {
Some(adapter) if provider.is_empty() || provider == "unknown" => {
adapter.default_provider().to_owned()
}
_ => provider,
}
}
fn build(
&self,
settings: &Arc<Settings>,
adapters: &[Arc<dyn Adapter>],
worker: &ModelRef,
) -> Result<Arc<dyn SessionRunner>, RunnerError> {
if let Some(adapter) = adapters.iter().find(|adapter| adapter.id() == worker.agent) {
if !enabled(settings, &worker.agent) {
return Err(RunnerError::Disabled {
model: worker.clone(),
reason: format!("agents.{}.enabled is false", worker.agent),
});
}
let provider = self.provider(worker, Some(adapter));
let cli = CliRunner::new(
adapter.clone(),
worker.clone(),
provider,
settings.clone(),
self.plan_usage.clone(),
self.health.clone(),
)
.with_registry(self.registry.clone())
.with_home(self.home.clone())
.with_memory(Arc::new(ServerMemory::new(&self.home)));
return Ok(Arc::new(cli));
}
if let Some(endpoint) = endpoint_settings(settings, &worker.agent) {
let provider = self.provider(worker, None);
let runner = EndpointRunner::new(
&worker.agent,
&endpoint,
worker.clone(),
provider,
self.registry.clone(),
self.plan_usage.clone(),
self.health.clone(),
)
.with_home(self.home.clone())
.with_memory(Arc::new(ServerMemory::new(&self.home)));
return Ok(Arc::new(runner));
}
Err(RunnerError::Unknown(worker.clone()))
}
}
fn enabled(settings: &Settings, id: &str) -> bool {
settings.agents.get(id).is_none_or(|agent| agent.enabled)
}
pub(crate) fn installed(settings: &Settings, adapter: &Arc<dyn Adapter>) -> bool {
let configured = settings
.agents
.get(adapter.id())
.and_then(|agent| agent.binary.clone());
match configured {
Some(binary) => binary.is_file(),
None => on_path(adapter.binary()),
}
}
impl RunnerDirectory for Directory {
fn get(&self, worker: &ModelRef) -> Result<Arc<dyn SessionRunner>, RunnerError> {
let mut built = self.current();
if let Some(found) = built.workers.get(worker) {
return Ok(found.clone());
}
let made = self.build(&built.settings, &built.adapters, worker)?;
built.workers.insert(worker.clone(), made.clone());
Ok(made)
}
fn available(&self) -> Vec<ModelRef> {
let (settings, adapters) = {
let built = self.current();
(Arc::clone(&built.settings), built.adapters.clone())
};
let clis = adapters
.iter()
.filter(|adapter| enabled(&settings, adapter.id()) && installed(&settings, adapter))
.map(|adapter| ModelRef::new(adapter.id(), None));
let known = self.registry.list();
let mut endpoint_ids: Vec<&str> = settings.endpoints.keys().map(String::as_str).collect();
for entry in &known {
let id = entry.id.agent.as_str();
if entry.kind == RegistryKind::Endpoint
&& builtin_endpoint(id).is_some()
&& !endpoint_ids.contains(&id)
{
endpoint_ids.push(id);
}
}
let endpoints = endpoint_ids.into_iter().flat_map(|id| {
let models: Vec<ModelRef> = known
.iter()
.filter(|entry| entry.kind == RegistryKind::Endpoint && entry.id.agent == id)
.map(|entry| entry.id.clone())
.collect();
if models.is_empty() {
vec![ModelRef::new(id, None)]
} else {
models
}
});
clis.chain(endpoints).collect()
}
fn enforces_read_only(&self, worker: &ModelRef) -> bool {
self.current()
.adapters
.iter()
.find(|adapter| adapter.id() == worker.agent)
.is_some_and(|adapter| adapter.capabilities().enforces_read_only)
}
fn takes_session_tools(&self, worker: &ModelRef) -> bool {
self.current()
.adapters
.iter()
.find(|adapter| adapter.id() == worker.agent)
.is_some_and(|adapter| capabilities::takes_session_tools(adapter.as_ref()))
}
fn isolates(&self, worker: &ModelRef) -> bool {
let built = self.current();
match built
.adapters
.iter()
.find(|adapter| adapter.id() == worker.agent)
{
Some(adapter) => adapter.capabilities().isolates,
None => endpoint_settings(&built.settings, &worker.agent).is_some(),
}
}
}
pub(crate) struct LiveRegistry {
settings: watch::Receiver<Arc<Settings>>,
build: RegistryFor,
current: Mutex<(Arc<Settings>, Arc<dyn ModelRegistry>)>,
}
type RegistryFor = Box<dyn Fn(&Settings) -> Arc<dyn ModelRegistry> + Send + Sync>;
impl LiveRegistry {
pub(crate) fn new(
settings: watch::Receiver<Arc<Settings>>,
repo: Option<RepoModels>,
cache: Option<PathBuf>,
) -> Self {
Self::built_by(settings, move |now| {
clis::registry(now, repo.as_ref(), cache.as_deref())
})
}
pub(crate) fn built_by(
settings: watch::Receiver<Arc<Settings>>,
build: impl Fn(&Settings) -> Arc<dyn ModelRegistry> + Send + Sync + 'static,
) -> Self {
let now = Arc::clone(&settings.borrow());
let registry = build(&now);
Self {
settings,
build: Box::new(build),
current: Mutex::new((now, registry)),
}
}
fn registry(&self) -> Arc<dyn ModelRegistry> {
let settings = Arc::clone(&self.settings.borrow());
let mut current = self.current.lock().unwrap_or_else(PoisonError::into_inner);
if !Arc::ptr_eq(¤t.0, &settings) {
let registry = (self.build)(&settings);
*current = (settings, registry);
}
Arc::clone(¤t.1)
}
fn caught_up_with(&self, asked: &Arc<dyn ModelRegistry>) {
let mut current = self.current.lock().unwrap_or_else(PoisonError::into_inner);
if !Arc::ptr_eq(¤t.1, asked) {
current.1 = (self.build)(¤t.0);
}
}
}
impl ModelRegistry for LiveRegistry {
fn get(&self, worker: &ModelRef) -> Option<ModelEntry> {
self.registry().get(worker)
}
fn provider(&self, worker: &ModelRef) -> String {
self.registry().provider(worker)
}
fn list(&self) -> Vec<ModelEntry> {
self.registry().list()
}
fn estimate(&self, worker: &ModelRef, input_tokens: u64, output_tokens: u64) -> CostEstimate {
self.registry()
.estimate(worker, input_tokens, output_tokens)
}
fn discover(&self) -> BoxFuture<'_, Vec<ModelEntry>> {
let registry = self.registry();
Box::pin(async move {
let found = registry.discover().await;
self.caught_up_with(®istry);
found
})
}
fn refreshed(&self) -> Option<OffsetDateTime> {
self.registry().refreshed()
}
fn loaded_window(&self, worker: &ModelRef) -> BoxFuture<'_, Option<u64>> {
let registry = self.registry();
let worker = worker.clone();
Box::pin(async move { registry.loaded_window(&worker).await })
}
fn catalog(&self) -> ModelCatalog {
self.registry().catalog()
}
fn models_reported(&self, cli: &str, models: &[argonauts_core::ReportedModel]) {
self.registry().models_reported(cli, models);
}
fn sources(&self) -> Vec<String> {
self.registry().sources()
}
fn discover_source<'a>(&'a self, cli: &'a str) -> BoxFuture<'a, ()> {
let registry = self.registry();
Box::pin(async move {
registry.discover_source(cli).await;
self.caught_up_with(®istry);
})
}
fn source_listed(&self, cli: &str) -> (Option<OffsetDateTime>, bool) {
self.registry().source_listed(cli)
}
fn source_note(&self, cli: &str) -> Option<String> {
self.registry().source_note(cli)
}
}
fn on_path(binary: &str) -> bool {
if binary.contains('/') {
return std::path::Path::new(binary).is_file();
}
std::env::var_os("PATH")
.is_some_and(|path| std::env::split_paths(&path).any(|dir| dir.join(binary).is_file()))
}
pub(crate) fn directory(
settings: SettingsSource,
home: &Paths,
repo: Option<&RepoModels>,
registry: Arc<dyn ModelRegistry>,
plan_usage: Arc<dyn PlanUsageBoard>,
health: Arc<dyn HealthBoard>,
) -> Arc<dyn RunnerDirectory> {
Arc::new(Directory {
settings,
home: home.clone(),
repo: repo.cloned(),
registry,
plan_usage,
health,
builtins: builtin_adapters(),
built: Mutex::new(Built {
settings: Arc::new(Settings::default()),
adapters: Vec::new(),
clis: CliAdapters::default(),
workers: HashMap::new(),
}),
})
}
pub(crate) fn services(
settings: Arc<Settings>,
turned_on: Vec<String>,
home: &Paths,
repo: &RepoContext,
reload: SettingsReloader,
) -> anyhow::Result<Services> {
let state_dir = home.state_dir(&repo.id);
create_private_dir_all(&state_dir)
.with_context(|| format!("creating {}", state_dir.display()))?;
let repo_models = RepoModels::new(&repo.root, &state_dir);
let registry = clis::registry(&settings, Some(&repo_models), Some(&home.models_cache()));
let (plan_usage, health) = boards_with(home, &settings.health, registry.clone());
let workers = directory(
SettingsSource::fixed(settings.clone(), turned_on),
home,
Some(&repo_models),
registry.clone(),
plan_usage.clone(),
health.clone(),
);
let steps = Arc::new(SchemaStepRunner);
let policy = approval_policy(
settings.clone(),
reload,
&state_dir,
workers.clone(),
steps.clone(),
);
let track_records: Arc<dyn TrackRecords> = Arc::new(LiveRecords::new(
state_dir.clone(),
repo.root.clone(),
settings.clone(),
));
let fallback = fallback_planner(
settings.clone(),
registry.clone(),
workers.clone(),
plan_usage.clone(),
health.clone(),
track_records.clone(),
Some(repo.run_dir.clone()),
);
let limits = limits_service(
settings.clone(),
registry.clone(),
workers.clone(),
plan_usage.clone(),
health.clone(),
fallback.clone(),
);
Ok(Services {
workers,
steps,
policy,
limits,
fallback,
plan_usage,
health,
checks: check_runner(settings),
registry,
track_records,
clock: Arc::new(SystemClock),
})
}
#[cfg(test)]
mod tests {
use argonauts_adapter_opencode::LocalUrls;
use argonauts_core::RepoId;
use argonauts_core::layout::project_files;
use argonauts_core::session_runner::RunnerKind;
use argonauts_core::settings::agents::EndpointSettings;
use super::*;
use crate::adapters::opencode_local_urls;
use crate::trust::TrustStore;
const CLIS: &str = r#"
[[cli]]
id = "lines-agent"
display_name = "Lines Agent"
binary = "/opt/lines-agent/bin/lines-agent"
provider = "lines"
read_only_args = ["--read-only"]
build_args = []
[[cli]]
id = "acp-agent"
display_name = "ACP Agent"
binary = "/opt/acp-agent/bin/acp-agent"
provider = "acp"
read_only_args = []
build_args = []
transport = "acp"
[cli.acp]
modes = { read_only = "plan", build = "default" }
model = "legacy-set-model"
effort = "none"
report = "final-prompt"
resume = "load"
steer = "input"
usage = ["end-turn"]
plan_usage = "none"
[[cli]]
id = "codex"
display_name = "Not Codex"
binary = "/opt/fake/codex"
provider = "fake"
read_only_args = []
build_args = []
"#;
fn directory_for(
settings: Settings,
home: &Paths,
repo: Option<&RepoModels>,
) -> Arc<dyn RunnerDirectory> {
directory_turned_on(settings, Vec::new(), home, repo)
}
fn directory_turned_on(
settings: Settings,
turned_on: Vec<String>,
home: &Paths,
repo: Option<&RepoModels>,
) -> Arc<dyn RunnerDirectory> {
let settings = Arc::new(settings);
let registry = clis::registry(&settings, repo, None);
let (plan_usage, health) = boards_with(home, &settings.health, registry.clone());
directory(
SettingsSource::fixed(settings, turned_on),
home,
repo,
registry,
plan_usage,
health,
)
}
#[test]
fn only_claude_code_and_endpoints_take_a_blind_role() {
let dir = tempfile::tempdir().unwrap();
let home = Paths::new(dir.path().join("home"));
std::fs::create_dir_all(home.home()).unwrap();
let mut settings = Settings::default();
settings
.endpoints
.insert("local".to_owned(), EndpointSettings::default());
let workers = directory_for(settings, &home, None);
let isolates = |id: &str| workers.isolates(&ModelRef::new(id, None));
assert!(isolates("claude-code"));
assert!(isolates("local"));
for id in ["codex", "antigravity-cli", "opencode", "unknown-agent"] {
assert!(!isolates(id), "{id}");
}
}
#[test]
fn directory_joins_builtins_manifests_and_endpoints() {
let dir = tempfile::tempdir().unwrap();
let home = Paths::new(dir.path().join("home"));
std::fs::create_dir_all(home.home()).unwrap();
std::fs::write(home.global_models(), CLIS).unwrap();
let mut settings = Settings::default();
settings
.endpoints
.insert("local".to_owned(), EndpointSettings::default());
settings.agents.get_mut("codex").unwrap().enabled = false;
let workers = directory_for(settings, &home, None);
let claude = workers
.get(&ModelRef::new("claude-code", Some("haiku".to_owned())))
.unwrap();
assert_eq!(claude.kind(), RunnerKind::Cli);
assert_eq!(claude.model_ref().model.as_deref(), Some("haiku"));
assert!(Arc::ptr_eq(
&claude,
&workers
.get(&ModelRef::new("claude-code", Some("haiku".to_owned())))
.unwrap()
));
assert_eq!(
workers
.get(&ModelRef::new("lines-agent", None))
.unwrap()
.provider(),
"lines"
);
assert_eq!(
workers
.get(&ModelRef::new("acp-agent", None))
.unwrap()
.provider(),
"acp"
);
let endpoint = workers
.get(&ModelRef::new("local", Some("llama3.2".to_owned())))
.unwrap();
assert_eq!(endpoint.kind(), RunnerKind::Endpoint);
assert!(matches!(
workers.get(&ModelRef::new("codex", None)),
Err(RunnerError::Disabled { .. })
));
assert!(matches!(
workers.get(&ModelRef::new("nobody", None)),
Err(RunnerError::Unknown(_))
));
let available = workers.available();
assert!(available.contains(&ModelRef::new("local", None)));
assert!(!available.iter().any(|worker| worker.agent == "codex"));
assert!(
!available.iter().any(|worker| worker.agent == "lines-agent"),
"its binary isn't installed"
);
}
#[test]
fn a_live_directory_follows_each_settings_change() {
let dir = tempfile::tempdir().unwrap();
let home = Paths::new(dir.path().join("home"));
let mut before = Settings::default();
before.agents.get_mut("codex").unwrap().enabled = false;
let (live, follow) = watch::channel(Arc::new(before.clone()));
let registry: Arc<dyn ModelRegistry> =
Arc::new(LiveRegistry::new(follow.clone(), None, None));
let (plan_usage, health) = boards_with(&home, &before.health, registry.clone());
let (turn_on, turned_on) = watch::channel(Vec::new());
let workers = directory(
SettingsSource::Live {
settings: follow,
turned_on,
},
&home,
None,
registry,
plan_usage,
health,
);
let codex = ModelRef::new("codex", None);
let local = ModelRef::new("local", None);
let claude = ModelRef::new("claude-code", None);
let first_claude = workers.get(&claude).unwrap();
assert!(matches!(
workers.get(&codex),
Err(RunnerError::Disabled { .. })
));
assert!(matches!(workers.get(&local), Err(RunnerError::Unknown(_))));
let mut after = before;
after.agents.get_mut("codex").unwrap().enabled = true;
after
.endpoints
.insert("local".to_owned(), EndpointSettings::default());
let hermes = ModelRef::new("hermes", None);
assert!(matches!(workers.get(&hermes), Err(RunnerError::Unknown(_))));
turn_on.send_replace(vec!["hermes".to_owned()]);
live.send_replace(Arc::new(after));
assert!(workers.get(&codex).is_ok());
assert_eq!(workers.get(&hermes).unwrap().kind(), RunnerKind::Cli);
assert_eq!(workers.get(&local).unwrap().kind(), RunnerKind::Endpoint);
assert!(workers.available().contains(&local));
assert!(
!Arc::ptr_eq(&first_claude, &workers.get(&claude).unwrap()),
"built again with the new settings"
);
}
#[test]
fn the_repos_state_folder_exists_before_the_first_approval() {
let dir = tempfile::tempdir().unwrap();
let home = Paths::new(dir.path().join("home"));
let root = dir.path().join("repo");
std::fs::create_dir_all(&root).unwrap();
let id = RepoId::for_repo(&root).unwrap();
let worktrees = home.home().join("worktrees");
let repo = RepoContext {
run_dir: home.state_dir(&id).join("runs").join("run-01"),
base_worktree: worktrees.join("base"),
worktrees,
root,
id,
};
assert!(!home.state_dir(&repo.id).exists());
let reload: SettingsReloader = Arc::new(|| None);
services(
Arc::new(Settings::default()),
Vec::new(),
&home,
&repo,
reload,
)
.unwrap();
let state = std::fs::metadata(home.state_dir(&repo.id)).unwrap();
assert!(state.is_dir());
assert_eq!(
std::os::unix::fs::PermissionsExt::mode(&state.permissions()) & 0o077,
0
);
}
#[test]
fn repo_clis_need_trust_and_an_outside_binary() {
let dir = tempfile::tempdir().unwrap();
let home = Paths::new(dir.path().join("home"));
let repo = dir.path().join("repo");
std::fs::create_dir_all(repo.join(".argonauts")).unwrap();
let inside = repo.join("tool");
let models = format!(
"[[cli]]\nid = \"outside\"\ndisplay_name = \"Outside\"\nbinary = \"/opt/outside\"\nprovider = \"o\"\nread_only_args = []\nbuild_args = []\n\n\
[[cli]]\nid = \"inside\"\ndisplay_name = \"Inside\"\nbinary = \"{}\"\nprovider = \"i\"\nread_only_args = []\nbuild_args = []\n",
inside.display()
);
let path = repo.join(".argonauts").join(project_files::MODELS);
std::fs::write(&path, models).unwrap();
let state_dir = home.state_dir(&RepoId::for_repo(&repo).unwrap());
let models = RepoModels::new(&repo, &state_dir);
let untrusted = directory_for(Settings::default(), &home, Some(&models));
assert!(untrusted.get(&ModelRef::new("outside", None)).is_err());
std::fs::create_dir_all(&state_dir).unwrap();
TrustStore::load(&state_dir).trust(&path).unwrap();
let trusted = directory_for(Settings::default(), &home, Some(&models));
assert!(trusted.get(&ModelRef::new("outside", None)).is_ok());
assert!(trusted.get(&ModelRef::new("inside", None)).is_err());
}
#[test]
fn a_built_in_endpoint_needs_no_table_and_yours_moves_it() {
let dir = tempfile::tempdir().unwrap();
let home = Paths::new(dir.path().join("home"));
let ollama = ModelRef::new("ollama", Some("llama3.2".to_owned()));
let workflow = directory_for(Settings::default(), &home, None);
let worker = workflow.get(&ollama).unwrap();
assert_eq!(worker.kind(), RunnerKind::Endpoint);
let mut moved = Settings::default();
moved.endpoints.insert(
"ollama".to_owned(),
EndpointSettings {
base_url: Some("http://gpu-box:11434/v1".to_owned()),
..EndpointSettings::default()
},
);
let settings = endpoint_settings(&moved, "ollama").unwrap();
assert_eq!(
settings.base_url.as_deref(),
Some("http://gpu-box:11434/v1")
);
assert!(!settings.local, "a server elsewhere isn't on your machine");
assert_eq!(
opencode_local_urls(&moved),
LocalUrls::new()
.with("ollama", "http://gpu-box:11434/v1")
.with("lmstudio", "http://127.0.0.1:1234/v1")
.with("llamacpp", "http://127.0.0.1:9931/v1")
.with("vllm", "http://localhost:8000/v1"),
"OpenCode is told where your Ollama moved"
);
assert!(directory_for(moved, &home, None).get(&ollama).is_ok());
}
#[test]
fn hermes_runs_only_once_a_layer_turns_it_on() {
let dir = tempfile::tempdir().unwrap();
let home = Paths::new(dir.path().join("home"));
let hermes = ModelRef::new("hermes", None);
let off = directory_for(Settings::default(), &home, None);
assert!(matches!(off.get(&hermes), Err(RunnerError::Unknown(_))));
let on = directory_turned_on(Settings::default(), vec!["hermes".to_owned()], &home, None);
assert_eq!(on.get(&hermes).unwrap().kind(), RunnerKind::Cli);
let template = home
.home()
.join(argonauts_core::layout::user_files::CLIS)
.join("hermes");
assert!(
template.join("config.yaml").is_file(),
"its home template is in place"
);
}
}