mod agents;
mod cli_tree;
mod doctor;
mod entry;
mod framework;
mod mcp_tools;
mod skills;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
pub(crate) type GeneratedSet = BTreeMap<&'static str, String>;
pub(crate) fn generate(
roster: &trusty_mpm::core::content_source::AgentRoster,
content: &trusty_mpm::core::content_source::FrameworkContent,
start: &Path,
) -> GeneratedSet {
let mut set = GeneratedSet::new();
set.insert("tm-capabilities.md", entry::render(roster, content));
set.insert("tm-capabilities/references/cli.md", cli_tree::render());
set.insert(
"tm-capabilities/references/mcp-tools.md",
mcp_tools::render(),
);
set.insert(
"tm-capabilities/references/agents.md",
agents::render(roster),
);
set.insert(
"tm-capabilities/references/skills.md",
skills::render(content),
);
set.insert("tm-capabilities/references/doctor.md", doctor::render());
set.insert(
"tm-capabilities/references/framework.md",
framework::render(start),
);
set
}
const SKILLS_ASSET_REL: &str = "content/skills";
pub(crate) fn resolve_skills_asset_dir(start: &Path) -> anyhow::Result<PathBuf> {
let Some(root) = start.ancestors().find(|d| d.join(".git").exists()) else {
anyhow::bail!(
"tm-capabilities: cannot resolve the asset directory: no git root at or above {}. \
Run `tm generate capabilities` from inside a trusty-tools checkout or worktree.",
start.display()
);
};
let dir = root.join(SKILLS_ASSET_REL);
anyhow::ensure!(
dir.is_dir(),
"tm-capabilities: cannot resolve the asset directory: {} does not exist, so the git root \
{} is not a trusty-tools checkout.",
dir.display(),
root.display()
);
Ok(dir)
}
pub(crate) fn write(set: &GeneratedSet, root: &Path) -> anyhow::Result<()> {
for (rel_path, content) in set {
let target = root.join(rel_path);
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&target, content)?;
}
Ok(())
}
pub(crate) fn diff(set: &GeneratedSet, root: &Path) -> Vec<String> {
let mut drifted = Vec::new();
for (rel_path, content) in set {
let target = root.join(rel_path);
match std::fs::read_to_string(&target) {
Ok(existing) if &existing == content => {}
Ok(_) => drifted.push(format!("{rel_path} (content differs)")),
Err(_) => drifted.push(format!("{rel_path} (not found at {})", target.display())),
}
}
drifted
}
pub(crate) fn run_capabilities(check: bool) -> anyhow::Result<()> {
let cwd = std::env::current_dir()
.map_err(|e| anyhow::anyhow!("tm-capabilities: cannot read the current directory: {e}"))?;
run_capabilities_in(&cwd, check)
}
pub(crate) fn run_capabilities_in(start: &Path, check: bool) -> anyhow::Result<()> {
let root = resolve_skills_asset_dir(start)?;
let checkout = root
.ancestors()
.find(|d| d.join(".git").exists())
.unwrap_or(&root);
let resolved = trusty_agents_common::agent_content::checkout_content(checkout)?;
let roster = trusty_mpm::core::content_source::AgentRoster::load(&resolved)?;
let content = trusty_mpm::core::content_source::FrameworkContent::load(&resolved)?;
run_capabilities_at(&root, check, &roster, &content)
}
pub(crate) fn run_capabilities_at(
root: &Path,
check: bool,
roster: &trusty_mpm::core::content_source::AgentRoster,
content: &trusty_mpm::core::content_source::FrameworkContent,
) -> anyhow::Result<()> {
let set = generate(roster, content, root);
if check {
let drifted = diff(&set, root);
if drifted.is_empty() {
println!(
"tm-capabilities: up to date ({} generated files).",
set.len()
);
Ok(())
} else {
eprintln!(
"tm-capabilities: drift detected in {} file(s):",
drifted.len()
);
for d in &drifted {
eprintln!(" - {d}");
}
eprintln!(
"\nRun `tm generate capabilities` (no --check) to regenerate, then commit the diff."
);
anyhow::bail!("tm-capabilities drift check failed");
}
} else {
write(&set, root)?;
println!(
"tm-capabilities: wrote {} generated files under {}/tm-capabilities*",
set.len(),
root.display()
);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_checkout() -> PathBuf {
trusty_common::test_harness::test_repo_root().expect("resolve the checkout")
}
#[test]
fn generated_set_has_seven_entries() {
let set = generate(
crate::commands::install::test_roster_ref(),
crate::commands::install::test_content_ref(),
&test_checkout(),
);
assert_eq!(set.len(), 7);
assert!(set.contains_key("tm-capabilities.md"));
assert!(set.contains_key("tm-capabilities/references/cli.md"));
assert!(set.contains_key("tm-capabilities/references/mcp-tools.md"));
assert!(set.contains_key("tm-capabilities/references/agents.md"));
assert!(set.contains_key("tm-capabilities/references/skills.md"));
assert!(set.contains_key("tm-capabilities/references/doctor.md"));
assert!(set.contains_key("tm-capabilities/references/framework.md"));
}
#[test]
fn generated_set_is_deterministic() {
let a = generate(
crate::commands::install::test_roster_ref(),
crate::commands::install::test_content_ref(),
&test_checkout(),
);
let b = generate(
crate::commands::install::test_roster_ref(),
crate::commands::install::test_content_ref(),
&test_checkout(),
);
assert_eq!(a, b);
}
#[test]
fn generated_set_no_content_is_empty() {
for (path, content) in generate(
crate::commands::install::test_roster_ref(),
crate::commands::install::test_content_ref(),
&test_checkout(),
) {
assert!(!content.trim().is_empty(), "{path} generated empty content");
}
}
#[test]
fn diff_reports_missing_file_against_empty_dir() {
let tmp = tempfile::tempdir().expect("tempdir");
let mut set = GeneratedSet::new();
set.insert("tm-capabilities/whatever.md", "content".to_string());
let drifted = diff(&set, tmp.path());
assert_eq!(drifted.len(), 1);
assert!(drifted[0].contains("not found at"), "{drifted:?}");
}
fn fake_checkout(root: &Path, git_is_file: bool) {
std::fs::create_dir_all(root.join(SKILLS_ASSET_REL)).expect("asset dir");
if git_is_file {
std::fs::write(root.join(".git"), "gitdir: /elsewhere\n").expect(".git file");
} else {
std::fs::create_dir_all(root.join(".git")).expect(".git dir");
}
}
#[test]
fn resolves_the_cwd_git_root_not_the_build_checkout_7776() {
let tmp = tempfile::tempdir().expect("tempdir");
let main = tmp.path().join("trusty-tools");
let worktree = main.join(".claude/worktrees/agent-x");
fake_checkout(&main, false);
fake_checkout(&worktree, true);
let resolved =
resolve_skills_asset_dir(&worktree.join("crates/trusty-mpm/src")).expect("worktree");
assert_eq!(resolved, worktree.join(SKILLS_ASSET_REL));
assert_eq!(
resolve_skills_asset_dir(&main.join("crates")).expect("main checkout"),
main.join(SKILLS_ASSET_REL)
);
}
#[test]
fn check_reads_the_checkout_holding_the_cwd_7776() {
let tmp = tempfile::tempdir().expect("tempdir");
let worktree = tmp.path().join("wt");
fake_checkout(&worktree, true);
let root = resolve_skills_asset_dir(&worktree).expect("worktree");
let roster = crate::commands::install::test_roster_ref();
let content = crate::commands::install::test_content_ref();
let err = run_capabilities_at(&root, true, roster, content)
.expect_err("nothing generated there yet");
assert!(err.to_string().contains("drift check failed"), "{err:#}");
run_capabilities_at(&root, false, roster, content).expect("write into the worktree");
run_capabilities_at(&root, true, roster, content).expect("a clean worktree passes --check");
}
#[test]
fn unresolvable_asset_root_is_a_path_error_7776() {
let tmp = tempfile::tempdir().expect("tempdir");
let bare = tmp.path().join("not-a-repo");
std::fs::create_dir_all(bare.join(".git")).expect(".git");
let err = resolve_skills_asset_dir(&bare).expect_err("no asset dir");
let msg = err.to_string();
assert!(msg.contains("cannot resolve the asset directory"), "{msg}");
assert!(msg.contains(SKILLS_ASSET_REL), "{msg}");
}
#[test]
fn diff_reports_content_mismatch() {
let tmp = tempfile::tempdir().expect("tempdir");
std::fs::write(tmp.path().join("tm-capabilities.md"), "stale content").unwrap();
let mut set = GeneratedSet::new();
set.insert("tm-capabilities.md", "fresh content".to_string());
let drifted = diff(&set, tmp.path());
assert_eq!(drifted.len(), 1);
assert!(drifted[0].contains("content differs"), "{drifted:?}");
}
#[test]
fn write_then_diff_round_trips_clean() {
let tmp = tempfile::tempdir().expect("tempdir");
let set = generate(
crate::commands::install::test_roster_ref(),
crate::commands::install::test_content_ref(),
&test_checkout(),
);
write(&set, tmp.path()).expect("write succeeds");
let drifted = diff(&set, tmp.path());
assert!(
drifted.is_empty(),
"unexpected drift after write: {drifted:?}"
);
}
}