Skip to main content

instruction_files/
lib.rs

1//! Discovery, auditing, and sync for AGENTS.md/CLAUDE.md instruction files.
2
3mod 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_line_budget, check_staleness,
14    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
26/// Bundled SKILL.md content for the instruction-files skill.
27const BUNDLED_SKILL: &str = include_str!("../.claude/skills/instruction-files/SKILL.md");
28
29/// Initialize instruction-files in a project.
30///
31/// Installs the bundled SKILL.md and scaffolds default runbooks.
32/// Safe to call repeatedly — never overwrites existing files.
33///
34/// Returns the paths of files written.
35pub fn init(root: &Path) -> Result<Vec<PathBuf>> {
36    let mut written = Vec::new();
37
38    // Install SKILL.md
39    let env = agent_kit::detect::Environment::detect();
40    let skill_path = env.skill_path("instruction-files", Some(root));
41    if !skill_path.exists() {
42        if let Some(parent) = skill_path.parent() {
43            std::fs::create_dir_all(parent)
44                .with_context(|| format!("failed to create {}", parent.display()))?;
45        }
46        std::fs::write(&skill_path, BUNDLED_SKILL)
47            .with_context(|| format!("failed to write {}", skill_path.display()))?;
48        written.push(skill_path);
49    }
50
51    // Scaffold default runbooks
52    let n = init_runbooks(root)?;
53    if n > 0 {
54        let runbooks_dir = root.join(".agent/runbooks");
55        written.push(runbooks_dir);
56    }
57
58    Ok(written)
59}
60
61/// Run the full audit with the given configuration.
62///
63/// Returns `Ok(())` on success, calls `std::process::exit(1)` on issues found.
64///
65/// When the `ontology` feature is enabled and `ontology_dir` is provided,
66/// instruction files are also scanned for `[term:Name]` annotations and
67/// each term is verified against the ontology directory.
68pub fn run(
69    config: &AuditConfig,
70    root_override: Option<&Path>,
71    #[cfg(feature = "ontology")] ontology_dir: Option<&Path>,
72) -> Result<()> {
73    println!("Auditing docs...\n");
74
75    let root = match root_override {
76        Some(p) => p.to_path_buf(),
77        None => find_root(config),
78    };
79    let files = find_instruction_files(&root, config);
80    let mut issues: Vec<Issue> = Vec::new();
81
82    for doc in &files {
83        let rel = doc
84            .strip_prefix(&root)
85            .unwrap_or(doc)
86            .to_string_lossy()
87            .to_string();
88        if let Ok(content) = std::fs::read_to_string(doc) {
89            issues.extend(check_tree_paths(&rel, &content, &root));
90            issues.extend(check_actionable(&rel, &content, config));
91            issues.extend(check_context_invariant(&rel, &content, config));
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}