use std::path::Path;
use crate::core::agent_deployer::deploy_agents;
use crate::core::paths::FrameworkPaths;
use crate::core::skill_tiers::deploy_all_skill_tiers;
use crate::core::standalone::global_config::ensure_global_config_dir;
pub fn ensure_managed_config_dir(config_dir: &Path) -> anyhow::Result<()> {
ensure_managed_config_dir_with_root(&FrameworkPaths::default(), config_dir)
}
pub fn ensure_managed_config_dir_with_root(
fw: &FrameworkPaths,
config_dir: &Path,
) -> anyhow::Result<()> {
ensure_global_config_dir(&fw.root, config_dir)?;
if let Err(err) = crate::core::skill_source::ensure_skill_source_fresh(fw) {
tracing::warn!("managed config dir: skill source refresh failed (non-fatal): {err}");
}
let agents_dest = config_dir.join("agents");
deploy_agents(&fw.agent_source_dir(), &agents_dest).map_err(|e| {
anyhow::anyhow!("failed to deploy full agent roster into managed config dir: {e}")
})?;
let skills_dest = config_dir.join("skills");
deploy_all_skill_tiers(
&fw.skill_source_dir(),
&fw.user_skill_source_dir(),
&skills_dest,
|_| true,
)
.map_err(|e| anyhow::anyhow!("failed to deploy full skill set into managed config dir: {e}"))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn seed_framework(base: &Path) -> FrameworkPaths {
let fw = FrameworkPaths::under(base);
std::fs::create_dir_all(&fw.agents).unwrap();
std::fs::create_dir_all(&fw.skills).unwrap();
let agents: &[(&str, &str)] = &[
(
"BASE-AGENT.md",
"---\nname: BASE-AGENT\ndescription: base\n---\n\nBase.\n",
),
(
"engineer.md",
"---\nname: engineer\ndescription: implementation specialist\n---\n\nEngineer.\n",
),
(
"rust-engineer.md",
"---\nname: rust-engineer\ndescription: rust specialist\n---\n\nRust.\n",
),
(
"research.md",
"---\nname: research\ndescription: research specialist\n---\n\nResearch.\n",
),
(
"qa.md",
"---\nname: qa\ndescription: quality specialist\n---\n\nQA.\n",
),
(
"version-control.md",
"---\nname: version-control\ndescription: git specialist\n---\n\nVCS.\n",
),
(
"ticketing.md",
"---\nname: ticketing\ndescription: ticketing specialist\n---\n\nTickets.\n",
),
];
for (name, body) in agents {
std::fs::write(fw.agents.join(name), body).unwrap();
}
std::fs::write(
fw.skills.join("tm-doctor.md"),
"---\nname: tm-doctor\ndescription: doctor\n---\n\nDoctor.\n",
)
.unwrap();
fw
}
#[test]
fn ensure_managed_config_dir_deploys_full_roster() {
let tmp = TempDir::new().unwrap();
let fw = seed_framework(tmp.path());
let config_dir = tmp.path().join(".trusty-tools/trusty-mpm/claude-config");
ensure_managed_config_dir_with_root(&fw, &config_dir).unwrap();
assert!(config_dir.join("settings.json").exists());
assert!(config_dir.join(".mcp.json").exists());
let agents_dir = config_dir.join("agents");
for name in [
"engineer",
"rust-engineer",
"research",
"qa",
"version-control",
"ticketing",
] {
let path = agents_dir.join(format!("{name}.md"));
assert!(path.exists(), "agent {name} must be deployed to config dir");
let body = std::fs::read_to_string(&path).unwrap();
assert!(
body.contains("description:"),
"deployed agent {name} must carry a description (spawnable)"
);
}
assert!(
config_dir.join("skills/tm-doctor/SKILL.md").exists(),
"tm-* skills must be deployed to config dir"
);
}
#[test]
fn ensure_managed_config_dir_is_idempotent() {
let tmp = TempDir::new().unwrap();
let fw = seed_framework(tmp.path());
let config_dir = tmp.path().join(".trusty-tools/trusty-mpm/claude-config");
ensure_managed_config_dir_with_root(&fw, &config_dir).unwrap();
let first = std::fs::read_to_string(config_dir.join("agents/engineer.md")).unwrap();
ensure_managed_config_dir_with_root(&fw, &config_dir).unwrap();
let second = std::fs::read_to_string(config_dir.join("agents/engineer.md")).unwrap();
assert_eq!(first, second, "re-provisioning must be idempotent");
}
#[test]
fn ensure_managed_config_dir_deploys_user_tier_skill() {
let tmp = TempDir::new().unwrap();
let fw = seed_framework(tmp.path());
std::fs::create_dir_all(&fw.user_skills).unwrap();
std::fs::write(
fw.user_skills.join("my-custom-skill.md"),
"---\nname: my-custom-skill\n---\n\nUSER CUSTOM.\n",
)
.unwrap();
let config_dir = tmp.path().join(".trusty-tools/trusty-mpm/claude-config");
ensure_managed_config_dir_with_root(&fw, &config_dir).unwrap();
assert!(
config_dir.join("skills/my-custom-skill/SKILL.md").exists(),
"user-custom skill must be deployed to the tm-global config dir"
);
}
#[test]
#[ignore = "static verification — composes the full real roster; run explicitly"]
fn manual_static_verify_full_roster() {
let tmp = TempDir::new().unwrap();
let fw = FrameworkPaths::under(tmp.path());
std::fs::create_dir_all(&fw.agents).unwrap();
std::fs::create_dir_all(&fw.skills).unwrap();
let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
for (src, dst) in [
(manifest.join("src/assets/agents"), &fw.agents),
(manifest.join("src/assets/skills"), &fw.skills),
] {
for entry in std::fs::read_dir(&src).unwrap() {
let entry = entry.unwrap();
if entry.path().extension().and_then(|e| e.to_str()) == Some("md") {
std::fs::copy(entry.path(), dst.join(entry.file_name())).unwrap();
}
}
}
let config_dir = tmp.path().join(".trusty-tools/trusty-mpm/claude-config");
ensure_managed_config_dir_with_root(&fw, &config_dir).unwrap();
let agents_dir = config_dir.join("agents");
let mut deployed: Vec<String> = std::fs::read_dir(&agents_dir)
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().and_then(|x| x.to_str()) == Some("md"))
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect();
deployed.sort();
println!("\n=== deployed agents ({}) ===", deployed.len());
let mut missing_desc = Vec::new();
for name in &deployed {
let body = std::fs::read_to_string(agents_dir.join(name)).unwrap();
let has_desc = body.contains("description:");
println!(" {name} spawnable={has_desc}");
if !has_desc && !name.starts_with("BASE-") {
missing_desc.push(name.clone());
}
}
for required in [
"engineer.md",
"rust-engineer.md",
"research.md",
"qa.md",
"local-ops.md",
"version-control.md",
"ticketing.md",
"documentation.md",
"security.md",
] {
assert!(
deployed.iter().any(|d| d == required),
"required agent {required} missing from deployed roster: {deployed:?}"
);
}
assert!(
missing_desc.is_empty(),
"these deployed agents are not spawnable (no description): {missing_desc:?}"
);
}
}