instruction-files 0.2.2

Discovery, auditing, and sync for AGENTS.md/CLAUDE.md instruction files
Documentation
//! Discovery, auditing, and sync for AGENTS.md/CLAUDE.md instruction files.

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_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};

/// Bundled SKILL.md content for the instruction-files skill.
const BUNDLED_SKILL: &str = include_str!("../.claude/skills/instruction-files/SKILL.md");

/// Initialize instruction-files in a project.
///
/// Installs the bundled SKILL.md and scaffolds default runbooks.
/// Safe to call repeatedly — never overwrites existing files.
///
/// Returns the paths of files written.
pub fn init(root: &Path) -> Result<Vec<PathBuf>> {
    let mut written = Vec::new();

    // Install SKILL.md — always target Claude Code paths since skills are Claude Code only
    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);
    }

    // Scaffold default runbooks
    let n = init_runbooks(root)?;
    if n > 0 {
        let runbooks_dir = root.join(".agent/runbooks");
        written.push(runbooks_dir);
    }

    Ok(written)
}

/// Run the full audit with the given configuration.
///
/// Returns `Ok(())` on success, calls `std::process::exit(1)` on issues found.
///
/// When the `ontology` feature is enabled and `ontology_dir` is provided,
/// instruction files are also scanned for `[term:Name]` annotations and
/// each term is verified against the ontology directory.
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));
            #[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(())
}