magi-code 0.64.0

Repository-aware CLI coding agent for terminal work
Documentation
pub(crate) mod subdir;

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

const MAX_STARTUP_INSTRUCTION_BYTES: u64 = 256 * 1024;
const MAX_STARTUP_INSTRUCTION_TOTAL_BYTES: usize = 1024 * 1024;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InstructionSourceKind {
    User,
    Repository,
    Configured,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InstructionFile {
    pub kind: InstructionSourceKind,
    pub path: PathBuf,
    pub content: String,
}

/// Discover instruction files in PRD-defined order:
/// 1. user instructions at `~/.magi-code/AGENTS.md`
/// 2. project instructions at the explicit repository/current root `AGENTS.md`
///
/// This intentionally does not scan arbitrary ancestors and does not load
/// `CLAUDE.md`. In repositories where a VCS root is unavailable, callers should
/// pass the active cwd as the project root until explicit root detection exists.
fn discover_agents(
    user_agents: &Path,
    project_root: &Path,
) -> anyhow::Result<Vec<InstructionFile>> {
    let mut files = Vec::new();
    load_optional(user_agents, InstructionSourceKind::User, &mut files)?;
    load_optional(
        &project_root.join("AGENTS.md"),
        InstructionSourceKind::Repository,
        &mut files,
    )?;
    Ok(files)
}

fn load_optional(
    path: &Path,
    kind: InstructionSourceKind,
    files: &mut Vec<InstructionFile>,
) -> anyhow::Result<()> {
    match crate::prompt_file::read_prompt_file(path, MAX_STARTUP_INSTRUCTION_BYTES, false) {
        Ok(file) => append_with_budget(files, kind, path.to_path_buf(), file.text, file.bytes),
        Err(error)
            if error
                .downcast_ref::<std::io::Error>()
                .is_some_and(|e| e.kind() == std::io::ErrorKind::NotFound) =>
        {
            Ok(())
        }
        Err(error) => Err(error)
            .with_context(|| format!("failed to read instruction file: {}", path.display())),
    }
}

fn append_with_budget(
    files: &mut Vec<InstructionFile>,
    kind: InstructionSourceKind,
    path: PathBuf,
    content: String,
    bytes: usize,
) -> anyhow::Result<()> {
    let current = files.iter().map(|file| file.content.len()).sum::<usize>();
    let total = current
        .checked_add(bytes)
        .ok_or_else(|| anyhow::anyhow!("startup instruction byte count overflow"))?;
    if total > MAX_STARTUP_INSTRUCTION_TOTAL_BYTES {
        anyhow::bail!(
            "startup instruction budget exceeded at {}: current total {current} bytes, source {bytes} bytes, ceiling {MAX_STARTUP_INSTRUCTION_TOTAL_BYTES} bytes",
            path.display()
        );
    }
    files.push(InstructionFile {
        kind,
        path,
        content,
    });
    Ok(())
}

pub fn discover_agents_with_additional_markdown(
    user_agents: &Path,
    project_root: &Path,
    additional_paths: &[PathBuf],
) -> anyhow::Result<Vec<InstructionFile>> {
    let mut files = discover_agents(user_agents, project_root)?;
    for (index, path) in additional_paths.iter().enumerate() {
        let file = load_configured_markdown(index, path)?;
        let bytes = file.content.len();
        append_with_budget(&mut files, file.kind, file.path, file.content, bytes)?;
    }
    Ok(files)
}

fn load_configured_markdown(index: usize, path: &Path) -> anyhow::Result<InstructionFile> {
    let key = format!("instructions.additional_markdown_paths[{index}]");
    if !path.is_absolute() {
        anyhow::bail!("{key} error: path must be absolute: {}", path.display());
    }
    if !path
        .extension()
        .is_some_and(|ext| ext.eq_ignore_ascii_case("md"))
    {
        anyhow::bail!("{key} error: expected .md file: {}", path.display());
    }
    let content = crate::prompt_file::read_prompt_file(path, MAX_STARTUP_INSTRUCTION_BYTES, false)
        .with_context(|| {
            format!(
                "{key} error: failed to read UTF-8 markdown file: {}",
                path.display()
            )
        })?;
    Ok(InstructionFile {
        kind: InstructionSourceKind::Configured,
        path: path.to_path_buf(),
        content: content.text,
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::TempDir;

    #[test]
    fn agents_discovery_loads_only_user_and_explicit_project_root() {
        let temp = TempDir::new().unwrap();
        let mc = temp.path().join("mc");
        let repo = temp.path().join("repo");
        let nested = repo.join("a/b");
        fs::create_dir_all(&mc).unwrap();
        fs::create_dir_all(&nested).unwrap();
        fs::write(mc.join("AGENTS.md"), "user").unwrap();
        fs::write(repo.join("AGENTS.md"), "repo agents").unwrap();
        fs::write(repo.join("CLAUDE.md"), "repo claude").unwrap();
        fs::write(repo.join("a").join("AGENTS.md"), "nested agents").unwrap();

        let files = discover_agents(&mc.join("AGENTS.md"), &repo).unwrap();
        assert_eq!(files.len(), 2);
        assert_eq!(files[0].kind, InstructionSourceKind::User);
        assert_eq!(files[0].content, "user");
        assert_eq!(files[1].kind, InstructionSourceKind::Repository);
        assert_eq!(files[1].content, "repo agents");
        assert!(!files.iter().any(|file| file.content == "repo claude"));
        assert!(!files.iter().any(|file| file.content == "nested agents"));
    }

    #[test]
    fn configured_markdown_appends_after_agents_in_configured_order() {
        let temp = TempDir::new().unwrap();
        let mc = temp.path().join("mc");
        let repo = temp.path().join("repo");
        fs::create_dir_all(&mc).unwrap();
        fs::create_dir_all(&repo).unwrap();
        fs::write(mc.join("AGENTS.md"), "user").unwrap();
        fs::write(repo.join("AGENTS.md"), "repo").unwrap();
        let first = temp.path().join("first.md");
        let second = temp.path().join("second.md");
        fs::write(&first, "configured one").unwrap();
        fs::write(&second, "configured two").unwrap();

        let files = discover_agents_with_additional_markdown(
            &mc.join("AGENTS.md"),
            &repo,
            &[first.clone(), second.clone(), first.clone()],
        )
        .unwrap();

        assert_eq!(
            files.iter().map(|file| &file.kind).collect::<Vec<_>>(),
            vec![
                &InstructionSourceKind::User,
                &InstructionSourceKind::Repository,
                &InstructionSourceKind::Configured,
                &InstructionSourceKind::Configured,
                &InstructionSourceKind::Configured,
            ]
        );
        assert_eq!(files[2].path, first);
        assert_eq!(files[2].content, "configured one");
        assert_eq!(files[3].path, second);
        assert_eq!(files[3].content, "configured two");
        assert_eq!(files[4].content, "configured one");
    }

    #[test]
    fn configured_markdown_rejects_invalid_paths_with_setting_index() {
        let temp = TempDir::new().unwrap();
        let non_markdown = temp.path().join("notes.txt");
        fs::write(&non_markdown, "notes").unwrap();
        let missing = temp.path().join("missing.md");
        let invalid_utf8 = temp.path().join("invalid.md");
        fs::write(&invalid_utf8, [0xff, 0xfe]).unwrap();

        for (paths, expected) in [
            (
                vec![PathBuf::from("relative.md")],
                "instructions.additional_markdown_paths[0] error: path must be absolute",
            ),
            (
                vec![non_markdown],
                "instructions.additional_markdown_paths[0] error: expected .md file",
            ),
            (
                vec![missing],
                "instructions.additional_markdown_paths[0] error: failed to read UTF-8 markdown file",
            ),
            (
                vec![invalid_utf8],
                "instructions.additional_markdown_paths[0] error: failed to read UTF-8 markdown file",
            ),
        ] {
            let error = discover_agents_with_additional_markdown(
                &temp.path().join("mc/AGENTS.md"),
                temp.path(),
                &paths,
            )
            .unwrap_err()
            .to_string();
            assert!(error.contains(expected), "{error}");
            assert!(error.contains(&paths[0].display().to_string()), "{error}");
        }
    }
}