1mod audit;
4mod discovery;
5pub use agent_runbooks as runbooks;
6#[cfg(feature = "ontology")]
7pub mod ontology;
8#[cfg(feature = "spec-audit")]
9pub mod spec_audit;
10mod types;
11
12pub use audit::{
13 check_actionable, check_context_invariant, check_library_context_policy, check_line_budget,
14 check_staleness, check_tree_paths,
15};
16pub use discovery::{find_instruction_files, find_root};
17#[cfg(feature = "ontology")]
18pub use ontology::check_ontology_terms;
19pub use runbooks::init_runbooks;
20pub use types::{AuditConfig, Issue, is_agent_file};
21
22use agent_kit::audit_common::LINE_BUDGET;
23use anyhow::{Context, Result};
24use std::path::{Path, PathBuf};
25
26const BUNDLED_SKILL: &str = include_str!("../.claude/skills/instruction-files/SKILL.md");
28
29pub fn init(root: &Path) -> Result<Vec<PathBuf>> {
36 let mut written = Vec::new();
37
38 let skill_path = agent_kit::detect::Environment::ClaudeCode.skill_path("instruction-files", Some(root));
40 if !skill_path.exists() {
41 if let Some(parent) = skill_path.parent() {
42 std::fs::create_dir_all(parent)
43 .with_context(|| format!("failed to create {}", parent.display()))?;
44 }
45 std::fs::write(&skill_path, BUNDLED_SKILL)
46 .with_context(|| format!("failed to write {}", skill_path.display()))?;
47 written.push(skill_path);
48 }
49
50 let n = init_runbooks(root)?;
52 if n > 0 {
53 let runbooks_dir = root.join(".agent/runbooks");
54 written.push(runbooks_dir);
55 }
56
57 Ok(written)
58}
59
60pub fn run(
68 config: &AuditConfig,
69 root_override: Option<&Path>,
70 #[cfg(feature = "ontology")] ontology_dir: Option<&Path>,
71) -> Result<()> {
72 println!("Auditing docs...\n");
73
74 let root = match root_override {
75 Some(p) => p.to_path_buf(),
76 None => find_root(config),
77 };
78 let files = find_instruction_files(&root, config);
79 let mut issues: Vec<Issue> = Vec::new();
80
81 for doc in &files {
82 let rel = doc
83 .strip_prefix(&root)
84 .unwrap_or(doc)
85 .to_string_lossy()
86 .to_string();
87 if let Ok(content) = std::fs::read_to_string(doc) {
88 issues.extend(check_tree_paths(&rel, &content, &root));
89 issues.extend(check_actionable(&rel, &content, config));
90 issues.extend(check_context_invariant(&rel, &content, config));
91 issues.extend(check_library_context_policy(&rel, &content, &root));
92 #[cfg(feature = "ontology")]
93 if let Some(onto_dir) = ontology_dir {
94 issues.extend(check_ontology_terms(&rel, &content, onto_dir));
95 }
96 }
97 }
98
99 let (budget_issues, counts, total) = check_line_budget(&files, &root, config);
100 issues.extend(budget_issues);
101 issues.extend(check_staleness(&files, &root, config));
102
103 for issue in &issues {
104 let mut loc = format!(" {}", issue.file);
105 if issue.line > 0 {
106 if issue.end_line > issue.line {
107 loc.push_str(&format!(":{}-{}", issue.line, issue.end_line));
108 } else {
109 loc.push_str(&format!(":{}", issue.line));
110 }
111 }
112 let marker = if issue.warning { "\u{26a0}" } else { "\u{2717}" };
113 println!("{:<50} {} {}", loc, marker, issue.message);
114 }
115
116 let mark = if total <= LINE_BUDGET {
117 "\u{2713}"
118 } else {
119 "\u{2717}"
120 };
121 println!(
122 "\nCombined instruction files: {} lines (budget: {}) {}",
123 total, LINE_BUDGET, mark
124 );
125 for (name, n) in &counts {
126 println!(" {}: {}", name, n);
127 }
128
129 let n = issues.len();
130 if n > 0 {
131 println!("\nFound {} issue(s)", n);
132 std::process::exit(1);
133 } else {
134 println!("\nNo issues found \u{2713}");
135 }
136
137 Ok(())
138}