use std::path::Path;
const ROOT_CONTROL_PREFIXES: &[&str] = &["steps", "inbox"];
const WORKTREE_PREFIXES: &[&str] = &[
"goal.md",
"soul.md",
"summary",
"messages",
"descriptions",
"skills",
];
const REFS_PREFIXES: &[&str] = &["repo.git/HEAD", "repo.git/refs"];
const BRAZEN_CONFIG_FILE: &str = "config.toml";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RootKind {
Workspace,
NamesRoot,
WorkspacesRoot,
BallsClones,
BrazenConfig,
LernieConfig,
YogState,
}
pub fn is_watched(kind: RootKind, root: &Path, path: &Path) -> bool {
let Ok(rel) = path.strip_prefix(root) else {
return false;
};
let rel = rel.to_string_lossy();
match kind {
RootKind::Workspace => workspace(&rel),
RootKind::NamesRoot | RootKind::WorkspacesRoot => !rel.is_empty(),
RootKind::BallsClones => balls_clones(&rel),
RootKind::BrazenConfig => rel == BRAZEN_CONFIG_FILE,
RootKind::LernieConfig => rel == "models.yaml" || matches_any(&rel, &["workflows"]),
RootKind::YogState => rel == "ui.json" || rel == "ops.jsonl",
}
}
fn workspace(rel: &str) -> bool {
if matches_any(rel, ROOT_CONTROL_PREFIXES) || matches_any(rel, REFS_PREFIXES) {
return true;
}
matches!(
rel.strip_prefix("agents/").and_then(|t| t.split_once('/')),
Some((_agent_id, tail)) if matches_any(tail, WORKTREE_PREFIXES)
)
}
fn balls_clones(rel: &str) -> bool {
let Some((_clone, tail)) = rel.split_once('/') else {
return !rel.is_empty();
};
if matches_any(tail, &["log"]) {
return false;
}
if let Some(name) = tail.strip_prefix("tasks/tasks/") {
return !name.is_empty() && !name.contains('/') && name.ends_with(".md");
}
matches_any(tail, &["config/config"])
}
fn matches_any(rel: &str, prefixes: &[&str]) -> bool {
prefixes
.iter()
.any(|prefix| rel == *prefix || rel.starts_with(&format!("{prefix}/")))
}
#[cfg(test)]
#[path = "roots_tests.rs"]
mod tests;