holon 0.14.1

A headless, event-driven runtime for long-lived agents
Documentation
use std::path::{Path, PathBuf};

use anyhow::Result;

use crate::types::{AgentsMdKind, AgentsMdScope, AgentsMdSource, LoadedAgentsMd};

const AGENTS_MD_FILENAME: &str = "AGENTS.md";
const CLAUDE_MD_FILENAME: &str = "CLAUDE.md";

pub fn load_agents_md(
    user_home: Option<&Path>,
    agent_home: &Path,
    workspace_anchor: Option<&Path>,
) -> Result<LoadedAgentsMd> {
    Ok(LoadedAgentsMd {
        user_global_source: load_user_global_agents_md(user_home)?,
        agent_source: load_agent_agents_md(agent_home)?,
        workspace_source: load_workspace_agents_md(workspace_anchor)?,
    })
}

fn load_user_global_agents_md(user_home: Option<&Path>) -> Result<Option<AgentsMdSource>> {
    let Some(user_home) = user_home else {
        return Ok(None);
    };
    let path = user_home.join(".agents").join(AGENTS_MD_FILENAME);
    load_source(AgentsMdScope::UserGlobal, AgentsMdKind::AgentsMd, &path)
}

fn load_agent_agents_md(agent_home: &Path) -> Result<Option<AgentsMdSource>> {
    let path = agent_home.join(AGENTS_MD_FILENAME);
    load_source(AgentsMdScope::Agent, AgentsMdKind::AgentsMd, &path)
}

fn load_workspace_agents_md(workspace_anchor: Option<&Path>) -> Result<Option<AgentsMdSource>> {
    let Some(workspace_anchor) = workspace_anchor else {
        return Ok(None);
    };

    let agents_md = workspace_anchor.join(AGENTS_MD_FILENAME);
    if let Some(source) = load_source(AgentsMdScope::Workspace, AgentsMdKind::AgentsMd, &agents_md)?
    {
        return Ok(Some(source));
    }

    let claude_md = workspace_anchor.join(CLAUDE_MD_FILENAME);
    load_source(
        AgentsMdScope::Workspace,
        AgentsMdKind::ClaudeMdFallback,
        &claude_md,
    )
}

fn load_source(
    scope: AgentsMdScope,
    kind: AgentsMdKind,
    path: &Path,
) -> Result<Option<AgentsMdSource>> {
    let content = match std::fs::read_to_string(path) {
        Ok(content) => content,
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
        Err(err) => return Err(err.into()),
    };
    Ok(Some(AgentsMdSource {
        scope,
        kind,
        path: PathBuf::from(path),
        content,
    }))
}

#[cfg(test)]
mod tests {
    use tempfile::tempdir;

    use super::*;

    #[test]
    fn loads_agent_and_workspace_agents_md() {
        let dir = tempdir().unwrap();
        let agent_home = dir.path().join("agent");
        let workspace = dir.path().join("workspace");
        std::fs::create_dir_all(&agent_home).unwrap();
        std::fs::create_dir_all(&workspace).unwrap();
        std::fs::write(agent_home.join(AGENTS_MD_FILENAME), "agent rules\n").unwrap();
        std::fs::write(workspace.join(AGENTS_MD_FILENAME), "workspace rules\n").unwrap();

        let loaded = load_agents_md(None, &agent_home, Some(&workspace)).unwrap();

        assert_eq!(
            loaded
                .agent_source
                .as_ref()
                .map(|source| source.kind.clone()),
            Some(AgentsMdKind::AgentsMd)
        );
        assert_eq!(
            loaded
                .workspace_source
                .as_ref()
                .map(|source| source.kind.clone()),
            Some(AgentsMdKind::AgentsMd)
        );
    }

    #[test]
    fn uses_workspace_claude_md_only_as_fallback() {
        let dir = tempdir().unwrap();
        let agent_home = dir.path().join("agent");
        let workspace = dir.path().join("workspace");
        std::fs::create_dir_all(&agent_home).unwrap();
        std::fs::create_dir_all(&workspace).unwrap();
        std::fs::write(workspace.join(CLAUDE_MD_FILENAME), "legacy rules\n").unwrap();

        let loaded = load_agents_md(None, &agent_home, Some(&workspace)).unwrap();

        assert_eq!(
            loaded
                .workspace_source
                .as_ref()
                .map(|source| source.kind.clone()),
            Some(AgentsMdKind::ClaudeMdFallback)
        );

        std::fs::write(workspace.join(AGENTS_MD_FILENAME), "new rules\n").unwrap();
        let loaded = load_agents_md(None, &agent_home, Some(&workspace)).unwrap();
        assert_eq!(
            loaded
                .workspace_source
                .as_ref()
                .map(|source| source.kind.clone()),
            Some(AgentsMdKind::AgentsMd)
        );
    }

    #[test]
    fn loaded_agents_md_does_not_serialize_content() {
        let loaded = LoadedAgentsMd {
            user_global_source: None,
            agent_source: Some(AgentsMdSource {
                scope: AgentsMdScope::Agent,
                kind: AgentsMdKind::AgentsMd,
                path: PathBuf::from("/tmp/agent/AGENTS.md"),
                content: "secret agent content".into(),
            }),
            workspace_source: None,
        };

        let json = serde_json::to_value(&loaded).unwrap();
        assert_eq!(json["agent_source"]["path"], "/tmp/agent/AGENTS.md");
        assert!(json["agent_source"]["content"].is_null());
    }

    #[test]
    fn loads_user_global_agent_and_workspace_guidance_layers() {
        let dir = tempdir().unwrap();
        let user_home = dir.path().join("user");
        let agent_home = dir.path().join("agent");
        let workspace = dir.path().join("workspace");
        std::fs::create_dir_all(user_home.join(".agents")).unwrap();
        std::fs::create_dir_all(&agent_home).unwrap();
        std::fs::create_dir_all(&workspace).unwrap();
        std::fs::write(
            user_home.join(".agents").join(AGENTS_MD_FILENAME),
            "global\n",
        )
        .unwrap();
        std::fs::write(agent_home.join(AGENTS_MD_FILENAME), "agent\n").unwrap();
        std::fs::write(workspace.join(AGENTS_MD_FILENAME), "workspace\n").unwrap();

        let loaded = load_agents_md(Some(&user_home), &agent_home, Some(&workspace)).unwrap();

        assert_eq!(
            loaded
                .user_global_source
                .as_ref()
                .map(|source| source.scope.clone()),
            Some(AgentsMdScope::UserGlobal)
        );
        assert!(loaded.agent_source.is_some());
        assert!(loaded.workspace_source.is_some());
    }

    #[test]
    fn user_global_and_agent_guidance_survive_workspace_switches() {
        let dir = tempdir().unwrap();
        let user_home = dir.path().join("user");
        let agent_home = dir.path().join("agent");
        let workspace_a = dir.path().join("workspace-a");
        let workspace_b = dir.path().join("workspace-b");
        std::fs::create_dir_all(user_home.join(".agents")).unwrap();
        std::fs::create_dir_all(&agent_home).unwrap();
        std::fs::create_dir_all(&workspace_a).unwrap();
        std::fs::create_dir_all(&workspace_b).unwrap();
        let user_global_path = user_home.join(".agents").join(AGENTS_MD_FILENAME);
        let agent_path = agent_home.join(AGENTS_MD_FILENAME);
        let workspace_a_path = workspace_a.join(AGENTS_MD_FILENAME);
        let workspace_b_path = workspace_b.join(AGENTS_MD_FILENAME);
        std::fs::write(&user_global_path, "global\n").unwrap();
        std::fs::write(&agent_path, "agent\n").unwrap();
        std::fs::write(&workspace_a_path, "workspace a\n").unwrap();
        std::fs::write(&workspace_b_path, "workspace b\n").unwrap();

        let loaded_a = load_agents_md(Some(&user_home), &agent_home, Some(&workspace_a)).unwrap();
        let loaded_b = load_agents_md(Some(&user_home), &agent_home, Some(&workspace_b)).unwrap();

        assert_eq!(
            loaded_a
                .user_global_source
                .as_ref()
                .map(|source| &source.path),
            Some(&user_global_path)
        );
        assert_eq!(
            loaded_b
                .user_global_source
                .as_ref()
                .map(|source| &source.path),
            Some(&user_global_path)
        );
        assert_eq!(
            loaded_a.agent_source.as_ref().map(|source| &source.path),
            Some(&agent_path)
        );
        assert_eq!(
            loaded_b.agent_source.as_ref().map(|source| &source.path),
            Some(&agent_path)
        );
        assert_eq!(
            loaded_a
                .workspace_source
                .as_ref()
                .map(|source| &source.path),
            Some(&workspace_a_path)
        );
        assert_eq!(
            loaded_b
                .workspace_source
                .as_ref()
                .map(|source| &source.path),
            Some(&workspace_b_path)
        );
    }
}