use std::path::Path;
use crate::core::doctor::{CheckStatus, DoctorCheck};
pub(super) fn check_legacy_instruction_sources(home: &Path) -> DoctorCheck {
let mut findings: Vec<String> = Vec::new();
let bundled_copies = count_legacy_bundled_skills(&home.join(".claude").join("skills"));
if bundled_copies > 0 {
findings.push(format!(
"{bundled_copies} bundled skill copy(ies) in ~/.claude/skills (legacy global \
deploy — delete them; bundled skills deploy only to the tm-managed \
CLAUDE_CONFIG_DIR since #6586, and neither `tm install` nor `tm reinstall` \
writes them here)"
));
}
if findings.is_empty() {
DoctorCheck::new(
"legacy_sources",
CheckStatus::Ok,
"no legacy global instruction sources found",
)
} else {
DoctorCheck::new(
"legacy_sources",
CheckStatus::Warn,
format!(
"legacy instruction source(s) may serve stale guidance: {}",
findings.join("; ")
),
)
}
}
fn count_legacy_bundled_skills(skills_dir: &Path) -> usize {
let bundled = crate::core::manifest::framework::bundled_skill_stems();
let Ok(entries) = std::fs::read_dir(skills_dir) else {
return 0;
};
entries
.flatten()
.filter(|entry| {
entry
.file_name()
.to_str()
.is_some_and(|name| bundled.contains(name.strip_suffix(".md").unwrap_or(name)))
})
.count()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn legacy_sources_ok_when_absent() {
let tmp = tempfile::tempdir().unwrap();
let check = check_legacy_instruction_sources(tmp.path());
assert_eq!(check.status, CheckStatus::Ok);
}
#[test]
fn legacy_sources_warns_on_tm_skills() {
let tmp = tempfile::tempdir().unwrap();
let skills = tmp.path().join(".claude").join("skills");
let skill_dir = skills.join("tm-workflow");
std::fs::create_dir_all(&skill_dir).unwrap();
std::fs::write(skill_dir.join("SKILL.md"), "old attribution").unwrap();
std::fs::create_dir_all(skills.join("my-skill")).unwrap();
let check = check_legacy_instruction_sources(tmp.path());
assert_eq!(check.status, CheckStatus::Warn);
assert!(check.message.contains(".claude/skills"));
assert!(check.message.contains('1'));
}
#[test]
fn legacy_sources_counts_an_unprefixed_bundled_skill() {
let tmp = tempfile::tempdir().unwrap();
let skills = tmp.path().join(".claude").join("skills");
let bundled = skills.join("documentation-style");
std::fs::create_dir_all(&bundled).unwrap();
std::fs::write(bundled.join("SKILL.md"), "stale copy").unwrap();
std::fs::create_dir_all(skills.join("my-own-skill")).unwrap();
let check = check_legacy_instruction_sources(tmp.path());
assert_eq!(check.status, CheckStatus::Warn, "{}", check.message);
assert!(
check.message.contains("1 bundled skill copy(ies)"),
"{}",
check.message
);
}
#[test]
fn legacy_sources_ok_when_the_standalone_config_dir_exists() {
let tmp = tempfile::tempdir().unwrap();
let cfg = tmp.path().join(".trusty-mpm").join("claude-config");
std::fs::create_dir_all(cfg.join("agents")).unwrap();
std::fs::create_dir_all(cfg.join("skills").join("documentation-style")).unwrap();
std::fs::write(
cfg.join("skills")
.join("documentation-style")
.join("SKILL.md"),
"deployed by the standalone driver",
)
.unwrap();
let check = check_legacy_instruction_sources(tmp.path());
assert_eq!(check.status, CheckStatus::Ok, "{}", check.message);
assert!(
!check.message.contains("claude-config"),
"{}",
check.message
);
}
#[test]
fn legacy_sources_ok_after_the_install_deploy() {
let tmp = tempfile::tempdir().unwrap();
let paths = crate::core::paths::FrameworkPaths::under(tmp.path());
let source = paths.skill_source_dir();
std::fs::create_dir_all(&source).unwrap();
std::fs::write(source.join("tm-workflow.md"), "bundled body").unwrap();
crate::core::skill_install_tiers::deploy_install_skill_tiers(&paths).unwrap();
let check = check_legacy_instruction_sources(tmp.path());
assert_eq!(
check.status,
CheckStatus::Ok,
"the installer's own deploy target must not trip legacy_sources: {}",
check.message
);
assert!(
paths
.skill_deploy_dir()
.join("tm-workflow")
.join("SKILL.md")
.is_file(),
"the bundled skill must be on disk in the managed tier"
);
}
}