rename claude.md to agents.md for agents and the repo
Claude Code (>= 2.1.277) and opencode both read AGENTS.md, but Claude
Code reads it only when no CLAUDE.md exists in its cwd or above. So
this is a plain rename with no CLAUDE.md stub left behind.
- hive-agent/src/agents_md_migrate.rs: new. At harness start, before
the first turn, renames state/CLAUDE.md to state/AGENTS.md when only
the former exists; logs an error naming both paths when both exist
and touches neither; does nothing otherwise. Idempotent.
- hive-agent/src/main.rs: calls it in serve_main; checkpoint prompt
and a comment name AGENTS.md.
- hive-agent/src/claude_md_watch.rs: the size watch reads AGENTS.md;
todo text, doc comments and tests follow. Module name, todo
subsystem key ("claude_md") and metric name
(hyperhive.agent.claude_md.lines) are unchanged so existing todo
rows and dashboard history carry over.
- hive-agent/src/turn.rs, hive-agent/prompts/system.md: prompts tell
the agent its file is AGENTS.md.
- hive-agent/src/{otel_turn_metrics,stats}.rs,
frontend/packages/agent/src/stats.js, hive-subagent-mcp/src/*.rs:
comments and a tool parameter description naming the agent's file.
- nix/host-modules/swarm-grafana/dashboards/agents.json: panel title
and description.
- claude-plugins skills claude-subagents, state-hygiene: they mean the
agent's own state file.
- CLAUDE.md -> AGENTS.md at the repo root; docs/crates/README.md link
(checked by check-doc-refs.sh), hive-c0re/README.md,
.prettierignore and the nix/sources.nix comment follow.
- docs/: every prose mention of the agent's file; persistence.md
documents the boot-time rename, claude-invocation.md the shadowing.
Fixes #5509
This commit is contained in:
parent
484a9fd4c1
commit
a429b30448
1 changed files with 217 additions and 45 deletions
143
hive-agent/src/agents_md_migrate.rs
Normal file
143
hive-agent/src/agents_md_migrate.rs
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
//! 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 `<state_dir>/CLAUDE.md` to `<state_dir>/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<Outcome> {
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue