use std::path::PathBuf;
use super::default::{DEFAULT_DIVERT_MIN_LINES, DEFAULT_DIVERT_WORKER_MODEL};
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 agent_ignore_staleness: 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>,
pub divert_enabled: bool,
pub divert_min_lines: u32,
pub divert_worker_model: 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, agent_ignore_staleness) =
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(),
set.ignore_staleness.clone(),
)
}
None => (fw.agent_source_dir(), Vec::new(), 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());
let (divert_enabled, divert_min_lines, divert_worker_model) = match &manifest.divert {
Some(divert) => (
divert.enabled.unwrap_or(false),
divert.min_lines.unwrap_or(DEFAULT_DIVERT_MIN_LINES),
divert
.worker_model
.clone()
.filter(|m| !m.trim().is_empty())
.unwrap_or_else(|| DEFAULT_DIVERT_WORKER_MODEL.to_string()),
),
None => (
false,
DEFAULT_DIVERT_MIN_LINES,
DEFAULT_DIVERT_WORKER_MODEL.to_string(),
),
};
Self {
agent_source,
agent_include,
agent_exclude,
agent_ignore_staleness,
skill_source,
skill_include,
skill_exclude,
inject_trusty_memory,
inject_trusty_search,
style,
divert_enabled,
divert_min_lines,
divert_worker_model,
}
}
pub fn agent_selected(&self, stem: &str) -> bool {
selection_matches(stem, &self.agent_include, &self.agent_exclude)
}
pub fn agent_staleness_ignored(&self, stem: &str) -> bool {
super::schema::matches_any(stem, &self.agent_ignore_staleness)
}
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,
..AgentSet::default()
}),
..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,
..AgentSet::default()
}),
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_ignore_staleness_does_not_affect_agent_selected() {
let fw = FrameworkPaths::under("/base");
let manifest = HarnessManifest {
agents: Some(AgentSet {
include: vec![],
exclude: vec![],
ignore_staleness: vec!["engineer".into()],
source: ContentSource::Bundled,
}),
..default_manifest()
};
let plan = HarnessPlan::from_manifest(&manifest, &fw, std::path::Path::new("/c"));
assert!(
plan.agent_selected("engineer"),
"ignore_staleness must not remove an agent from the deploy roster"
);
assert!(plan.agent_staleness_ignored("engineer"));
assert!(
!plan.agent_staleness_ignored("qa"),
"unrelated agent unaffected"
);
}
#[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_divert_toggles() {
use super::super::schema::DivertConfig;
let fw = FrameworkPaths::under("/base");
let catalog = std::path::Path::new("/c");
let absent = HarnessManifest {
divert: None,
..HarnessManifest::default()
};
let plan = HarnessPlan::from_manifest(&absent, &fw, catalog);
assert!(!plan.divert_enabled, "an absent section must not enable it");
assert_eq!(plan.divert_min_lines, 350);
assert_eq!(plan.divert_worker_model, "claude-haiku-4-5");
let partial = HarnessManifest {
divert: Some(DivertConfig {
enabled: Some(true),
..DivertConfig::default()
}),
..HarnessManifest::default()
};
let plan = HarnessPlan::from_manifest(&partial, &fw, catalog);
assert!(plan.divert_enabled);
assert_eq!(plan.divert_min_lines, 350);
assert_eq!(plan.divert_worker_model, "claude-haiku-4-5");
let plan = HarnessPlan::from_manifest(&default_manifest(), &fw, catalog);
assert!(!plan.divert_enabled);
assert_eq!(plan.divert_worker_model, "claude-haiku-4-5");
let full = HarnessManifest {
divert: Some(DivertConfig {
enabled: Some(true),
min_lines: Some(120),
worker_model: Some("claude-haiku-4-5-20251001".to_string()),
}),
..HarnessManifest::default()
};
let plan = HarnessPlan::from_manifest(&full, &fw, catalog);
assert!(plan.divert_enabled);
assert_eq!(plan.divert_min_lines, 120);
assert_eq!(plan.divert_worker_model, "claude-haiku-4-5-20251001");
}
#[test]
fn a_custom_mcp_table_reaches_no_part_of_the_plan() {
let fw = FrameworkPaths::under("/base");
let manifest = HarnessManifest::from_toml(
"[mcp.custom.duetto-memory]\ntype = \"http\"\nurl = \"https://mcp.example/memory\"\n",
)
.expect("a manifest declaring [mcp.custom] must still parse");
let plan = HarnessPlan::from_manifest(&manifest, &fw, std::path::Path::new("/c"));
assert!(
!format!("{plan:?}").contains("duetto-memory"),
"no part of the plan may carry a [mcp.custom] entry: {plan:?}"
);
}
#[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);
}
}