Watch
0
0
Fork
You've already forked hyperhive
0

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:
atlas 2026-10-18 01:06:47 +02:00 • committed by mara
commit a429b30448
25 changed files with 217 additions and 45 deletions

View file

@ -1,5 +1,5 @@
//! `CLAUDE.md` size watch: raises a loose-ends-v2 todo when the agent's
//! own `CLAUDE.md` grows past 500 lines, and feeds the same line count
//! `AGENTS.md` size watch: raises a loose-ends-v2 todo when the agent's
//! own `AGENTS.md` grows past 500 lines, and feeds the same line count
//! into an OTEL gauge every tick regardless of threshold. Per the
//! operator's own framing of the ask — "claude md is personality and
//! how you organize yourself, not a work log" — the prompt is asked to
@ -11,7 +11,7 @@
//! summary) — a third instance of the same anti-nag pattern rather than
//! a new one.
//!
//! `CLAUDE.md` lives at the top of the agent's own state dir — the same
//! `AGENTS.md` lives at the top of the agent's own state dir — the same
//! directory `claude --print` runs with as its cwd (`turn::session_cwd`),
//! which is how the CLI's own auto-load discovers it.
//!
@ -40,20 +40,24 @@ const WARN_LINES: u64 = 500;
const BUCKET: u64 = 50;
/// Producing subsystem marker for the todo row.
const SUBSYSTEM: &str = "claude_md";
/// Fixed dedup key — there is only ever one `CLAUDE.md`-size todo per
/// Fixed dedup key — there is only ever one `AGENTS.md`-size todo per
/// agent.
const TODO_KEY: &str = "size";
/// Background loop: periodically reconcile the `CLAUDE.md`-size todo and
/// Background loop: periodically reconcile the `AGENTS.md`-size todo and
/// feed the OTEL gauge. Detached task — runs for the harness's lifetime;
/// errors are logged, never fatal.
pub async fn run(todos: Arc<Todos>, wake: Arc<Notify>) {
loop {
tokio::time::sleep(CHECK_INTERVAL).await;
let lines = match tokio::task::spawn_blocking(|| line_count(&claude_md_path())).await {
let lines = match tokio::task::spawn_blocking(|| {
line_count(&agents_md_path(&crate::paths::state_dir()))
})
.await
{
Ok(l) => l,
Err(e) => {
tracing::warn!(error = %e, "claude.md watch probe panicked");
tracing::warn!(error = %e, "agents.md watch probe panicked");
continue;
}
};
@ -72,25 +76,25 @@ fn reconcile(todos: &Todos, wake: &Notify, summary: Option<&str>) {
Some(summary) => match todos.upsert(SUBSYSTEM, Some(TODO_KEY), summary, None, false) {
Ok((_, true)) => wake.notify_one(),
Ok((_, false)) => {}
Err(e) => tracing::warn!(error = ?e, "claude.md todo upsert failed"),
Err(e) => tracing::warn!(error = ?e, "agents.md todo upsert failed"),
},
None => {
if let Err(e) = todos.clear(SUBSYSTEM, Some(TODO_KEY)) {
tracing::warn!(error = ?e, "claude.md todo clear failed");
tracing::warn!(error = ?e, "agents.md todo clear failed");
}
}
}
}
/// `CLAUDE.md`'s path: the top of the agent's own state dir, matching
/// `AGENTS.md`'s path: the top of the agent's own state dir, matching
/// where `claude --print` runs (`turn::session_cwd`) and therefore where
/// its own auto-load discovers the file.
fn claude_md_path() -> std::path::PathBuf {
crate::paths::state_dir().join("CLAUDE.md")
fn agents_md_path(state_dir: &Path) -> std::path::PathBuf {
state_dir.join("AGENTS.md")
}
/// Line count of the file at `path`. `None` when it can't be read — no
/// `CLAUDE.md` yet is not a warning, and a transient read failure isn't
/// `AGENTS.md` yet is not a warning, and a transient read failure isn't
/// either.
fn line_count(path: &Path) -> Option<u64> {
let contents = std::fs::read_to_string(path).ok()?;
@ -108,9 +112,9 @@ fn summary_for(lines: u64) -> Option<String> {
}
let bucket = lines / BUCKET * BUCKET;
Some(format!(
"your CLAUDE.md is {bucket}+ lines. CLAUDE.md is personality and how you organize \
"your AGENTS.md is {bucket}+ lines. AGENTS.md is personality and how you organize \
yourself, not a work log — if it's grown past that, moving detail into separate files \
(linked from CLAUDE.md) or an archive is what actually shrinks this count. It \
(linked from AGENTS.md) or an archive is what actually shrinks this count. It \
auto-loads into every turn, so a smaller file is a fixed cost saved on every turn. \
Nothing here is a hard limit, and deleting stale content outright is fine too if \
that's genuinely what it is — no obligation either way. No need to act on this now — \
@ -175,16 +179,28 @@ mod tests {
#[test]
fn line_count_matches_editor_expectations() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("CLAUDE.md");
let path = dir.path().join("AGENTS.md");
// Three lines, trailing newline — an editor shows 3 lines, not 4.
std::fs::write(&path, "one\ntwo\nthree\n").expect("write");
assert_eq!(line_count(&path), Some(3));
}
/// The watch counts `AGENTS.md`, and a leftover `CLAUDE.md` beside it
/// is not what it reads.
#[test]
fn watch_reads_agents_md_not_claude_md() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join("CLAUDE.md"), "one\n").expect("write");
assert_eq!(line_count(&agents_md_path(dir.path())), None);
std::fs::write(dir.path().join("AGENTS.md"), "one\ntwo\n").expect("write");
assert_eq!(line_count(&agents_md_path(dir.path())), Some(2));
}
#[test]
fn missing_file_is_silent_not_an_error() {
assert_eq!(
line_count(Path::new("/definitely/does/not/exist/CLAUDE.md")),
line_count(Path::new("/definitely/does/not/exist/AGENTS.md")),
None
);
}