use std::path::Path;
use crate::core::agent_manifest::AgentManifest;
use crate::core::manifest::{HarnessPlan, ManifestSources, resolve_manifest};
use crate::core::paths::FrameworkPaths;
use crate::core::skill_manifest::SkillManifest;
fn reconstruct_plan(fw: &FrameworkPaths) -> HarnessPlan {
let project_dir = fw.claude_home_dir();
let catalog_root = crate::content::catalog_root_for(&fw.root);
let sources = ManifestSources::resolve(&project_dir, &catalog_root);
let manifest = resolve_manifest(&sources);
HarnessPlan::from_manifest(&manifest, fw, &catalog_root)
}
fn canonical_stems(dir: &Path) -> Vec<String> {
let Ok(entries) = std::fs::read_dir(dir) else {
return Vec::new();
};
let mut names: Vec<String> = entries
.flatten()
.filter_map(|entry| {
let path = entry.path();
let is_md = path
.extension()
.and_then(|x| x.to_str())
.map(|x| x.eq_ignore_ascii_case("md"))
.unwrap_or(false);
if !is_md {
return None;
}
path.file_stem().and_then(|s| s.to_str()).map(str::to_owned)
})
.collect();
names.sort_unstable();
names
}
pub(super) fn expected_agent_stems(
fw: &FrameworkPaths,
manifest: Option<&AgentManifest>,
) -> Vec<String> {
let plan = reconstruct_plan(fw);
let plan_stems: Vec<String> = canonical_stems(&plan.agent_source)
.into_iter()
.filter(|name| plan.agent_selected(name))
.collect();
if !plan_stems.is_empty() {
return plan_stems;
}
if let Some(m) = manifest {
let manifest_stems: Vec<String> = m
.managed
.keys()
.filter_map(|f| f.strip_suffix(".md").map(str::to_owned))
.collect();
if !manifest_stems.is_empty() {
return manifest_stems;
}
}
canonical_stems(&fw.agent_source_dir())
}
pub(super) fn expected_skill_stems(
fw: &FrameworkPaths,
manifest: Option<&SkillManifest>,
) -> Vec<String> {
let plan = reconstruct_plan(fw);
let plan_stems: Vec<String> = canonical_stems(&plan.skill_source)
.into_iter()
.filter(|name| plan.skill_selected(name))
.collect();
if !plan_stems.is_empty() {
return plan_stems;
}
if let Some(m) = manifest {
let manifest_stems: Vec<String> = m.managed.keys().cloned().collect();
if !manifest_stems.is_empty() {
return manifest_stems;
}
}
canonical_stems(&fw.skill_source_dir())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn seed_agent_source(fw: &FrameworkPaths, names: &[&str]) {
std::fs::create_dir_all(&fw.agents).unwrap();
for name in names {
std::fs::write(
fw.agents.join(format!("{name}.md")),
format!("---\nname: {name}\ndescription: d\n---\n\nBody.\n"),
)
.unwrap();
}
}
fn write_project_manifest(fw: &FrameworkPaths, project_dir: &Path, toml: &str) {
let dir = project_dir.join(".trusty-mpm").join("framework");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("manifest.toml"), toml).unwrap();
let _ = fw;
}
#[test]
fn plan_filters_expected_agents() {
let tmp = TempDir::new().unwrap();
let workspace = tmp.path().join("workspace");
std::fs::create_dir_all(&workspace).unwrap();
let fw = FrameworkPaths::for_managed_project(tmp.path(), &workspace);
let mut fw = fw;
fw.trusty_mpm_root = None;
seed_agent_source(&fw, &["engineer", "rust-engineer"]);
let mut expected = expected_agent_stems(&fw, None);
expected.sort();
assert_eq!(
expected,
vec!["engineer".to_string(), "rust-engineer".to_string()]
);
}
#[test]
fn plan_selection_excludes_generic_engineer() {
let tmp = TempDir::new().unwrap();
let workspace = tmp.path().join("workspace");
std::fs::create_dir_all(&workspace).unwrap();
let mut fw = FrameworkPaths::for_managed_project(tmp.path(), &workspace);
fw.trusty_mpm_root = None;
seed_agent_source(&fw, &["engineer", "rust-engineer", "python-engineer"]);
write_project_manifest(&fw, &workspace, "[agents]\nexclude = [\"engineer\"]\n");
let mut expected = expected_agent_stems(&fw, None);
expected.sort();
assert_eq!(
expected,
vec!["python-engineer".to_string(), "rust-engineer".to_string()],
"excluded generic engineer must not be in the expected set"
);
}
#[test]
fn user_level_manifest_is_ignored() {
let tmp = TempDir::new().unwrap();
let workspace = tmp.path().join("workspace");
std::fs::create_dir_all(&workspace).unwrap();
let mut fw = FrameworkPaths::for_managed_project(tmp.path(), &workspace);
fw.trusty_mpm_root = None;
seed_agent_source(&fw, &["engineer", "rust-engineer", "python-engineer"]);
std::fs::create_dir_all(&fw.root).unwrap();
std::fs::write(
fw.root.join("manifest.toml"),
"[agents]\nexclude = [\"engineer\"]\n",
)
.unwrap();
let mut expected = expected_agent_stems(&fw, None);
expected.sort();
assert!(
expected.contains(&"engineer".to_string()),
"a user-level manifest must no longer be read: {expected:?}"
);
}
#[test]
fn empty_plan_source_falls_back_to_manifest() {
let tmp = TempDir::new().unwrap();
let workspace = tmp.path().join("workspace");
std::fs::create_dir_all(&workspace).unwrap();
let mut fw = FrameworkPaths::for_managed_project(tmp.path(), &workspace);
fw.trusty_mpm_root = None;
let mut manifest = AgentManifest::default();
manifest.managed.insert(
"engineer.md".to_string(),
crate::core::agent_manifest::ManifestEntry {
source_chain: vec!["engineer".to_string()],
checksum: "abc".to_string(),
deployed_at: "2026-01-01T00:00:00Z".to_string(),
origin: crate::core::agent_manifest::Origin::Bundled,
},
);
let expected = expected_agent_stems(&fw, Some(&manifest));
assert_eq!(expected, vec!["engineer".to_string()]);
}
#[test]
fn manifest_and_plan_both_empty_falls_back_to_canonical_roster() {
let tmp = TempDir::new().unwrap();
let workspace = tmp.path().join("workspace");
std::fs::create_dir_all(&workspace).unwrap();
let mut fw = FrameworkPaths::for_managed_project(tmp.path(), &workspace);
fw.trusty_mpm_root = None;
seed_agent_source(&fw, &["engineer"]);
let expected = expected_agent_stems(&fw, None);
assert_eq!(expected, vec!["engineer".to_string()]);
}
#[test]
fn expected_skill_stems_mirrors_agent_fallback_order() {
let tmp = TempDir::new().unwrap();
let workspace = tmp.path().join("workspace");
std::fs::create_dir_all(&workspace).unwrap();
let mut fw = FrameworkPaths::for_managed_project(tmp.path(), &workspace);
fw.trusty_mpm_root = None;
std::fs::create_dir_all(&fw.skills).unwrap();
std::fs::write(fw.skills.join("tm-doctor.md"), "skill body").unwrap();
let expected = expected_skill_stems(&fw, None);
assert_eq!(expected, vec!["tm-doctor".to_string()]);
}
}