magi-code 0.96.2

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,
    })
}