//! One-time on-boot rename of the agent's instructions file from //! `state/CLAUDE.md` to `state/AGENTS.md`. Runs in `main.rs` before the //! first turn. Idempotent: once the file is renamed, later boots find no //! `CLAUDE.md` and do nothing. //! //! Claude Code reads `AGENTS.md` only when no `CLAUDE.md` exists in its cwd //! or above, so a `CLAUDE.md` left next to an `AGENTS.md` shadows it. When //! both exist this module refuses to choose: it logs an error naming both //! paths and leaves the files alone for the agent or operator to merge. use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; /// What [`rename_claude_md`] found and did. #[derive(Debug, PartialEq, Eq)] pub enum Outcome { /// `CLAUDE.md` existed without an `AGENTS.md`, and is now `AGENTS.md`. Renamed, /// Neither file, or only `AGENTS.md`: nothing to do. NothingToDo, /// Both files exist; neither was touched. BothExist { claude_md: PathBuf, agents_md: PathBuf, }, } /// Rename `/CLAUDE.md` to `/AGENTS.md` if only the /// former exists. Logs the outcome; never fatal to boot. pub fn run(state_dir: &Path) { match rename_claude_md(state_dir) { Ok(Outcome::Renamed) => { tracing::info!(dir = %state_dir.display(), "renamed CLAUDE.md to AGENTS.md"); } Ok(Outcome::NothingToDo) => {} Ok(Outcome::BothExist { claude_md, agents_md, }) => tracing::error!( claude_md = %claude_md.display(), agents_md = %agents_md.display(), "both CLAUDE.md and AGENTS.md exist; claude loads only CLAUDE.md while it exists. \ Merge them into AGENTS.md and delete CLAUDE.md" ), Err(e) => tracing::error!(error = ?e, "CLAUDE.md to AGENTS.md rename failed"), } } /// Decide and apply the rename. /// /// # Errors /// /// Propagates a failed `rename`; both files are left as they were. pub fn rename_claude_md(state_dir: &Path) -> Result { let claude_md = state_dir.join("CLAUDE.md"); let agents_md = state_dir.join("AGENTS.md"); match (claude_md.exists(), agents_md.exists()) { (true, false) => { std::fs::rename(&claude_md, &agents_md).with_context(|| { format!("rename {} -> {}", claude_md.display(), agents_md.display()) })?; Ok(Outcome::Renamed) } (true, true) => Ok(Outcome::BothExist { claude_md, agents_md, }), (false, _) => Ok(Outcome::NothingToDo), } } #[cfg(test)] mod tests { use super::*; #[test] fn claude_md_alone_is_renamed_once() { let dir = tempfile::tempdir().expect("tempdir"); std::fs::write(dir.path().join("CLAUDE.md"), "me\n").expect("write"); assert_eq!(rename_claude_md(dir.path()).expect("run"), Outcome::Renamed); assert!(!dir.path().join("CLAUDE.md").exists()); assert_eq!( std::fs::read_to_string(dir.path().join("AGENTS.md")).expect("read"), "me\n" ); assert_eq!( rename_claude_md(dir.path()).expect("second run"), Outcome::NothingToDo ); assert_eq!( std::fs::read_to_string(dir.path().join("AGENTS.md")).expect("read"), "me\n" ); } #[test] fn both_present_touches_neither() { let dir = tempfile::tempdir().expect("tempdir"); let claude_md = dir.path().join("CLAUDE.md"); let agents_md = dir.path().join("AGENTS.md"); std::fs::write(&claude_md, "old\n").expect("write"); std::fs::write(&agents_md, "new\n").expect("write"); assert_eq!( rename_claude_md(dir.path()).expect("run"), Outcome::BothExist { claude_md: claude_md.clone(), agents_md: agents_md.clone(), } ); assert_eq!(std::fs::read_to_string(&claude_md).expect("read"), "old\n"); assert_eq!(std::fs::read_to_string(&agents_md).expect("read"), "new\n"); } #[test] fn neither_present_creates_nothing() { let dir = tempfile::tempdir().expect("tempdir"); assert_eq!( rename_claude_md(dir.path()).expect("run"), Outcome::NothingToDo ); assert_eq!(std::fs::read_dir(dir.path()).expect("ls").count(), 0); } #[test] fn agents_md_alone_is_left_alone() { let dir = tempfile::tempdir().expect("tempdir"); std::fs::write(dir.path().join("AGENTS.md"), "me\n").expect("write"); assert_eq!( rename_claude_md(dir.path()).expect("run"), Outcome::NothingToDo ); assert_eq!( std::fs::read_to_string(dir.path().join("AGENTS.md")).expect("read"), "me\n" ); } }