supercode-harness 0.4.19

The optional native Supercode agent and tool harness
Documentation
//! BP-5 (catalog D2 "Path-scoped rules": *rule files activated only when
//! matching files are touched*; cc§2 "`.claude/rules/*.md` — modular
//! instruction files; optional `paths:` frontmatter scopes a rule to file
//! globs so it loads only when Claude touches matching files").
//!
//! **A prompt-assembly input, not a module.** A rule file is an instruction
//! file that happens to carry a selector. There are exactly two ways it can
//! reach a prompt, and both are doors that already existed:
//!
//! * **No `paths:`** — it joins the instruction blob at construction, beside
//!   `CLAUDE.md`/`AGENTS.md`, under the same `core.project_context` byte
//!   budget (`agent::assemble_project_instructions`).
//! * **With `paths:`** — it is held back, and injected as a tool-result
//!   notice the first time a tool touches a matching file. That is the exact
//!   mechanism `core.nested_instructions` already uses for a subdirectory's
//!   own CLAUDE.md (`tools::builtins::nested_instructions_notice`), with the
//!   selector swapped from "the directory you touched" to "a glob this rule
//!   declares". Each rule is injected at most once per session, deduped by
//!   path, exactly as nested instructions are.
//!
//! **Roots.** `<CLAUDE_CONFIG_DIR>/rules/` (the user tier) and
//! `.claude/rules/` in each directory of the instruction walk
//! (`docs:memory#organize-rules-with-claude-rules`). Read only when
//! `[core.path_rules]` is on; a config that does not set it opens no
//! directory at all.

use std::path::{Path, PathBuf};

use crate::config::Config;

/// Ceiling on the bytes of one rule file that reach a prompt.
const MAX_RULE_BYTES: usize = 32 * 1024;

/// Ceiling on the number of rule files one config may load, so a rules
/// directory cannot make agent construction unbounded.
const MAX_RULE_FILES: usize = 64;

/// One `.claude/rules/*.md` file.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RuleFile {
    /// Frontmatter `name`, else the file stem.
    pub name: String,
    /// The file itself.
    pub path: PathBuf,
    /// `paths:` frontmatter globs. Empty = unscoped (always loaded).
    pub paths: Vec<String>,
    /// Everything after the frontmatter, trimmed and capped.
    pub body: String,
}

impl RuleFile {
    /// Whether this rule waits for a matching file to be touched.
    pub fn is_scoped(&self) -> bool {
        !self.paths.is_empty()
    }

    /// Does this rule's selector match `touched`? Each glob is tried
    /// against the path relative to `root`, the full path, and the bare
    /// file name — the same three spellings instruction-file excludes are
    /// matched in (`agent::instruction_file_excluded`), so one glob
    /// spelling means one thing across the product.
    pub fn matches(&self, touched: &Path, root: &Path) -> bool {
        let full = touched.to_string_lossy().to_string();
        let name = touched
            .file_name()
            .map(|n| n.to_string_lossy().to_string())
            .unwrap_or_default();
        let rel = touched
            .strip_prefix(root)
            .ok()
            .map(|p| p.to_string_lossy().to_string());
        self.paths.iter().any(|pattern| {
            // gitignore-spec `**/` matches ZERO OR MORE directories, so
            // `src/**/*.rs` covers `src/lib.rs` as well as `src/a/b.rs`.
            // The product's `*` matcher has no `**` concept (its `*` already
            // crosses `/`), so the zero-directory reading is supplied here as
            // a second candidate spelling rather than by a second matcher.
            let collapsed = pattern.replace("/**/", "/");
            let mut candidates = vec![pattern.as_str()];
            if collapsed != *pattern {
                candidates.push(collapsed.as_str());
            }
            candidates.iter().any(|pattern| {
                crate::config::glob_match(pattern, &full)
                    || crate::config::glob_match(pattern, &name)
                    || rel
                        .as_deref()
                        .is_some_and(|r| crate::config::glob_match(pattern, r))
            })
        })
    }

    /// How this rule renders wherever it is injected — one shape, so a rule
    /// read at startup and the same rule injected on a tool result are
    /// recognizably the same thing.
    pub fn render(&self) -> String {
        format!("[rule: {}]\n{}", self.name, self.body)
    }
}

/// Every rule file `config` loads, user tier first, then the instruction
/// walk from the outermost root down to `cwd` — the same root→cwd ordering
/// instruction files use, so the nearest rule is read last. Empty (and free
/// of any filesystem work) when `[core.path_rules]` is off.
pub fn load(config: &Config) -> Vec<RuleFile> {
    if !config.path_rules {
        return Vec::new();
    }
    let mut out = Vec::new();
    let mut seen: std::collections::BTreeSet<PathBuf> = std::collections::BTreeSet::new();
    for root in rule_roots(config) {
        let Ok(entries) = std::fs::read_dir(&root) else {
            continue;
        };
        let mut files: Vec<PathBuf> = entries
            .flatten()
            .map(|e| e.path())
            .filter(|p| p.extension().and_then(|e| e.to_str()) == Some("md"))
            .collect();
        files.sort();
        for file in files {
            if out.len() >= MAX_RULE_FILES {
                return out;
            }
            let canonical = std::fs::canonicalize(&file).unwrap_or_else(|_| file.clone());
            if !seen.insert(canonical) {
                continue;
            }
            if let Some(rule) = read_rule(&file) {
                out.push(rule);
            }
        }
    }
    out
}

/// The rule directories, in load order.
fn rule_roots(config: &Config) -> Vec<PathBuf> {
    let mut roots = vec![crate::skills::SkillHomes::default()
        .claude_code
        .join("rules")];
    for dir in crate::agent::instruction_walk_roots(config) {
        roots.push(dir.join(".claude").join("rules"));
    }
    roots
}

/// Parse one rule file. `None` when it has no body worth injecting.
fn read_rule(path: &Path) -> Option<RuleFile> {
    let text = std::fs::read_to_string(path).ok()?;
    let front = crate::skills::read_frontmatter(path);
    let mut body = crate::skills::strip_frontmatter(&text);
    if body.is_empty() {
        return None;
    }
    if body.len() > MAX_RULE_BYTES {
        let mut cut = MAX_RULE_BYTES;
        while cut > 0 && !body.is_char_boundary(cut) {
            cut -= 1;
        }
        body.truncate(cut);
        body.push_str("\n[rule truncated]");
    }
    let name = front.get("name").cloned().unwrap_or_else(|| {
        path.file_stem()
            .map(|s| s.to_string_lossy().to_string())
            .unwrap_or_default()
    });
    Some(RuleFile {
        name,
        path: path.to_path_buf(),
        paths: front
            .get("paths")
            .map(|v| crate::skills::frontmatter_list(v))
            .unwrap_or_default(),
        body,
    })
}

/// The unscoped rules' contribution to the instruction blob, in load order.
/// Empty when nothing is unscoped.
pub fn always_on_text(rules: &[RuleFile]) -> String {
    let mut out = String::new();
    for rule in rules.iter().filter(|r| !r.is_scoped()) {
        out.push_str("\n\n");
        out.push_str(&rule.render());
    }
    out
}