mod audit;
mod discovery;
pub use agent_runbooks as runbooks;
#[cfg(feature = "ontology")]
pub mod ontology;
#[cfg(feature = "spec-audit")]
pub mod spec_audit;
mod types;
pub use audit::{
check_actionable, check_context_invariant, check_library_context_policy, check_line_budget,
check_staleness, check_tree_paths,
};
pub use discovery::{find_instruction_files, find_root};
#[cfg(feature = "ontology")]
pub use ontology::check_ontology_terms;
pub use runbooks::init_runbooks;
pub use types::{AuditConfig, Issue, is_agent_file};
use agent_kit::audit_common::LINE_BUDGET;
use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
const BUNDLED_SKILL: &str = include_str!("../.claude/skills/instruction-files/SKILL.md");
pub fn init(root: &Path) -> Result<Vec<PathBuf>> {
let mut written = Vec::new();
let skill_path = agent_kit::detect::Environment::ClaudeCode.skill_path("instruction-files", Some(root));
if !skill_path.exists() {
if let Some(parent) = skill_path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("failed to create {}", parent.display()))?;
}
std::fs::write(&skill_path, BUNDLED_SKILL)
.with_context(|| format!("failed to write {}", skill_path.display()))?;
written.push(skill_path);
}
let n = init_runbooks(root)?;
if n > 0 {
let runbooks_dir = root.join(".agent/runbooks");
written.push(runbooks_dir);
}
Ok(written)
}
pub fn run(
config: &AuditConfig,
root_override: Option<&Path>,
#[cfg(feature = "ontology")] ontology_dir: Option<&Path>,
) -> Result<()> {
println!("Auditing docs...\n");
let root = match root_override {
Some(p) => p.to_path_buf(),
None => find_root(config),
};
let files = find_instruction_files(&root, config);
let mut issues: Vec<Issue> = Vec::new();
for doc in &files {
let rel = doc
.strip_prefix(&root)
.unwrap_or(doc)
.to_string_lossy()
.to_string();
if let Ok(content) = std::fs::read_to_string(doc) {
issues.extend(check_tree_paths(&rel, &content, &root));
issues.extend(check_actionable(&rel, &content, config));
issues.extend(check_context_invariant(&rel, &content, config));
issues.extend(check_library_context_policy(&rel, &content, &root));
#[cfg(feature = "ontology")]
if let Some(onto_dir) = ontology_dir {
issues.extend(check_ontology_terms(&rel, &content, onto_dir));
}
}
}
let (budget_issues, counts, total) = check_line_budget(&files, &root, config);
issues.extend(budget_issues);
issues.extend(check_staleness(&files, &root, config));
for issue in &issues {
let mut loc = format!(" {}", issue.file);
if issue.line > 0 {
if issue.end_line > issue.line {
loc.push_str(&format!(":{}-{}", issue.line, issue.end_line));
} else {
loc.push_str(&format!(":{}", issue.line));
}
}
let marker = if issue.warning { "\u{26a0}" } else { "\u{2717}" };
println!("{:<50} {} {}", loc, marker, issue.message);
}
let mark = if total <= LINE_BUDGET {
"\u{2713}"
} else {
"\u{2717}"
};
println!(
"\nCombined instruction files: {} lines (budget: {}) {}",
total, LINE_BUDGET, mark
);
for (name, n) in &counts {
println!(" {}: {}", name, n);
}
let n = issues.len();
if n > 0 {
println!("\nFound {} issue(s)", n);
std::process::exit(1);
} else {
println!("\nNo issues found \u{2713}");
}
Ok(())
}