use std::path::{Path, PathBuf};
use crate::core::agent_manifest::{atomic_write, checksum};
use crate::core::paths::FrameworkPaths;
use crate::core::skill_deploy_tiers::skill_deploy_tiers;
use crate::core::skill_drift::{
ManifestState, SkillDrift, SkillReference, audit_deployed_skills, deployed_path,
};
use crate::core::skill_manifest::{SkillManifest, SkillManifestEntry};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RepairAction {
Repaired { backup: Option<PathBuf> },
SkippedFrozen,
SkippedUnverifiable(String),
Failed(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RepairOutcome {
pub tier: &'static str,
pub stem: String,
pub action: RepairAction,
}
impl RepairOutcome {
pub fn changed(&self) -> bool {
matches!(self.action, RepairAction::Repaired { .. })
}
}
pub fn repair_skills(
reference: &SkillReference,
paths: &FrameworkPaths,
project_dir: Option<&Path>,
include_frozen: bool,
backup_root: &Path,
) -> Vec<RepairOutcome> {
let mut outcomes = Vec::new();
for tier in skill_deploy_tiers(paths, project_dir) {
let audit = audit_deployed_skills(reference, &tier.dir);
if let ManifestState::Unreadable(why) = &audit.manifest {
outcomes.push(RepairOutcome {
tier: tier.label,
stem: "<manifest>".to_string(),
action: RepairAction::SkippedUnverifiable(format!(
"{why} — refusing to touch this tier; repairing it would rewrite an ownership ledger that cannot be read"
)),
});
continue;
}
let mut manifest = SkillManifest::load(&tier.dir);
let mut manifest_dirty = false;
for finding in audit.findings {
let action = match finding.state {
SkillDrift::Fresh => continue,
SkillDrift::Unverifiable(why) => RepairAction::SkippedUnverifiable(why),
SkillDrift::DriftedFrozen if !include_frozen => RepairAction::SkippedFrozen,
SkillDrift::Drifted | SkillDrift::DriftedFrozen | SkillDrift::Missing => {
let Some(content) = reference.assets.get(&finding.stem) else {
outcomes.push(RepairOutcome {
tier: tier.label,
stem: finding.stem,
action: RepairAction::Failed(
"no bundled asset available to write".to_string(),
),
});
continue;
};
match rewrite_and_verify(
&tier.dir,
tier.label,
&finding.stem,
content,
backup_root,
) {
Ok(backup) => {
manifest.managed.insert(
finding.stem.clone(),
SkillManifestEntry {
checksum: checksum(content),
deployed_at: chrono::Utc::now().to_rfc3339(),
},
);
manifest_dirty = true;
RepairAction::Repaired { backup }
}
Err(e) => RepairAction::Failed(e),
}
}
};
outcomes.push(RepairOutcome {
tier: tier.label,
stem: finding.stem,
action,
});
}
if manifest_dirty && let Err(e) = manifest.save(&tier.dir) {
outcomes.push(RepairOutcome {
tier: tier.label,
stem: "<manifest>".to_string(),
action: RepairAction::Failed(format!("could not save the deploy manifest: {e}")),
});
}
}
outcomes
}
fn rewrite_and_verify(
tier_dir: &Path,
tier_label: &str,
stem: &str,
content: &str,
backup_root: &Path,
) -> Result<Option<PathBuf>, String> {
let target = deployed_path(tier_dir, stem);
let backup = if target.exists() {
let rel = target
.strip_prefix(tier_dir)
.unwrap_or_else(|_| Path::new(stem));
let dest = backup_root.join(tier_label.replace(' ', "-")).join(rel);
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("could not create backup dir {}: {e}", parent.display()))?;
}
std::fs::copy(&target, &dest).map_err(|e| {
format!(
"could not back up {} to {}: {e}",
target.display(),
dest.display()
)
})?;
Some(dest)
} else {
None
};
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("could not create {}: {e}", parent.display()))?;
}
atomic_write(&target, content)
.map_err(|e| format!("could not write {}: {e}", target.display()))?;
let observed = std::fs::read_to_string(&target)
.map_err(|e| format!("wrote {} but could not read it back: {e}", target.display()))?;
if observed != content {
return Err(format!(
"wrote {} but re-reading it returned different content — the repair did NOT take",
target.display()
));
}
Ok(backup)
}
pub fn backup_root_for(home: &Path, now: chrono::DateTime<chrono::Utc>) -> PathBuf {
home.join(".trusty-mpm").join(format!(
"backup-doctor-remediation-{}",
now.format("%Y%m%d-%H%M%S")
))
}
#[cfg(test)]
#[path = "skill_repair_tests.rs"]
mod tests;