use std::path::PathBuf;
use super::schema::{ContentSource, HarnessManifest, selection_matches};
use crate::core::paths::FrameworkPaths;
#[derive(Debug, Clone)]
pub struct HarnessPlan {
pub agent_source: PathBuf,
pub agent_include: Vec<String>,
pub agent_exclude: Vec<String>,
pub skill_source: PathBuf,
pub skill_include: Vec<String>,
pub skill_exclude: Vec<String>,
pub inject_trusty_memory: bool,
pub inject_trusty_search: bool,
pub style: Option<String>,
}
impl HarnessPlan {
pub fn from_manifest(
manifest: &HarnessManifest,
fw: &FrameworkPaths,
catalog_root: &std::path::Path,
) -> Self {
let catalog_agents = catalog_root.join("repo").join(".claude").join("agents");
let catalog_skills = catalog_root.join("repo").join(".claude").join("skills");
let (agent_source, agent_include, agent_exclude) = match &manifest.agents {
Some(set) => {
let source = match set.source {
ContentSource::Bundled => fw.agent_source_dir(),
ContentSource::Catalog => catalog_agents,
};
(source, set.include.clone(), set.exclude.clone())
}
None => (fw.agent_source_dir(), Vec::new(), Vec::new()),
};
let (skill_source, skill_include, skill_exclude) = match &manifest.skills {
Some(set) => {
let source = match set.source {
ContentSource::Bundled => fw.skill_source_dir(),
ContentSource::Catalog => catalog_skills,
};
(source, set.include.clone(), set.exclude.clone())
}
None => (fw.skill_source_dir(), Vec::new(), Vec::new()),
};
let (inject_trusty_memory, inject_trusty_search) = match &manifest.mcp {
Some(mcp) => (
mcp.trusty_memory.unwrap_or(true),
mcp.trusty_search.unwrap_or(true),
),
None => (true, true),
};
let style = manifest.style.as_ref().and_then(|s| s.active.clone());
Self {
agent_source,
agent_include,
agent_exclude,
skill_source,
skill_include,
skill_exclude,
inject_trusty_memory,
inject_trusty_search,
style,
}
}
pub fn agent_selected(&self, stem: &str) -> bool {
selection_matches(stem, &self.agent_include, &self.agent_exclude)
}
pub fn skill_selected(&self, stem: &str) -> bool {
selection_matches(stem, &self.skill_include, &self.skill_exclude)
}
}
#[cfg(test)]
mod tests {
use super::super::default::default_manifest;
use super::super::schema::{AgentSet, McpServers, SkillSet, StyleSelection};
use super::*;
#[test]
fn plan_default_uses_bundled_sources() {
let fw = FrameworkPaths::under("/base");
let plan = HarnessPlan::from_manifest(
&default_manifest(),
&fw,
std::path::Path::new("/home/.trusty-mpm/catalog"),
);
assert_eq!(plan.agent_source, fw.agent_source_dir());
assert_eq!(plan.skill_source, fw.skill_source_dir());
assert!(plan.agent_include.is_empty());
assert!(plan.agent_selected("anything"));
assert!(plan.skill_selected("anything"));
assert!(plan.inject_trusty_memory);
assert!(plan.inject_trusty_search);
assert_eq!(plan.style.as_deref(), Some("trusty-mpm"));
}
#[test]
fn plan_catalog_source_paths() {
let fw = FrameworkPaths::under("/base");
let manifest = HarnessManifest {
agents: Some(AgentSet {
include: vec![],
exclude: vec![],
source: ContentSource::Catalog,
}),
..default_manifest()
};
let plan = HarnessPlan::from_manifest(
&manifest,
&fw,
std::path::Path::new("/home/.trusty-mpm/catalog"),
);
assert_eq!(
plan.agent_source,
PathBuf::from("/home/.trusty-mpm/catalog/repo/.claude/agents")
);
assert_eq!(plan.skill_source, fw.skill_source_dir());
}
#[test]
fn plan_selection_filters() {
let fw = FrameworkPaths::under("/base");
let manifest = HarnessManifest {
agents: Some(AgentSet {
include: vec!["*-engineer".into()],
exclude: vec!["php-engineer".into()],
source: ContentSource::Bundled,
}),
skills: Some(SkillSet {
include: vec!["example-skill".into()],
exclude: vec![],
source: ContentSource::Bundled,
}),
..default_manifest()
};
let plan = HarnessPlan::from_manifest(&manifest, &fw, std::path::Path::new("/c"));
assert!(plan.agent_selected("rust-engineer"));
assert!(!plan.agent_selected("php-engineer"), "exclude wins");
assert!(!plan.agent_selected("qa"), "not in include");
assert!(plan.skill_selected("example-skill"));
assert!(!plan.skill_selected("tm-doctor"));
}
#[test]
fn plan_mcp_toggles() {
let fw = FrameworkPaths::under("/base");
let manifest = HarnessManifest {
mcp: Some(McpServers {
trusty_memory: Some(false),
trusty_search: Some(true),
}),
..default_manifest()
};
let plan = HarnessPlan::from_manifest(&manifest, &fw, std::path::Path::new("/c"));
assert!(!plan.inject_trusty_memory);
assert!(plan.inject_trusty_search);
}
#[test]
fn plan_style_default_lowest_precedence() {
let fw = FrameworkPaths::under("/base");
let manifest = HarnessManifest {
style: Some(StyleSelection {
active: Some("trusty-mpm-research".into()),
}),
..HarnessManifest::default()
};
let plan = HarnessPlan::from_manifest(&manifest, &fw, std::path::Path::new("/c"));
assert_eq!(plan.style.as_deref(), Some("trusty-mpm-research"));
let none_plan = HarnessPlan::from_manifest(
&HarnessManifest::default(),
&fw,
std::path::Path::new("/c"),
);
assert_eq!(none_plan.style, None);
}
}