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

@ -14,7 +14,7 @@ docs/tools/swarm-metrics-cli.md
# prettier's `proseWrap: "preserve"` prevents prose reflow but not list-item # prettier's `proseWrap: "preserve"` prevents prose reflow but not list-item
# indentation normalisation, so these files need manual re-wrap before # indentation normalisation, so these files need manual re-wrap before
# adding them back to the formatter scope. # adding them back to the formatter scope.
CLAUDE.md AGENTS.md
docs/agent-lifecycle/config-changes.md docs/agent-lifecycle/config-changes.md
docs/process/conventions.md docs/process/conventions.md
docs/networking/gateway.md docs/networking/gateway.md

View file

@ -37,7 +37,7 @@ actually attached, so a `continue` naming a session that isn't there
fails the tool call outright instead of looking like it worked; the error fails the tool call outright instead of looking like it worked; the error
says which directory it searched, which is usually the fix (pass `dir`). says which directory it searched, which is usually the fix (pass `dir`).
`dir` defaults to your work dir, `/agents/<you>/work`, which has no `dir` defaults to your work dir, `/agents/<you>/work`, which has no
`CLAUDE.md` above it, so a subagent started there does not load yours `AGENTS.md` above it, so a subagent started there does not load yours
(until that directory exists, the daemon's cwd is used). (until that directory exists, the daemon's cwd is used).
Use `status` for a Use `status` for a
zero-cost "is it still going" check — for a running subagent it also zero-cost "is it still going" check — for a running subagent it also

View file

@ -5,7 +5,7 @@ description: Enforces safe handling of an agent's own durable state/notes files
# State Hygiene # State Hygiene
Durable notes/state files (your `CLAUDE.md`, `state/notes.md`, `state/TODO.md`, Durable notes/state files (your `AGENTS.md`, `state/notes.md`, `state/TODO.md`,
and anything else under `state/`) are your memory across container resets and and anything else under `state/`) are your memory across container resets and
context compactions. They only work if you actually read them before you context compactions. They only work if you actually read them before you
change them - writing blind is how history gets silently destroyed or change them - writing blind is how history gets silently destroyed or

View file

@ -360,7 +360,7 @@ The system prompt (`hive-agent/prompts/system.md`, rendered by
varies is which MCP tools it surfaces (gated by the agent's permissions, varies is which MCP tools it surfaces (gated by the agent's permissions,
which the swarm sets). No `role:manager` block renders only which the swarm sets). No `role:manager` block renders only
for the root agent. The root agent's role-specific for the root agent. The root agent's role-specific
behaviour comes from its CLAUDE.md / agent-specific instructions, not behaviour comes from its AGENTS.md / agent-specific instructions, not
the system prompt template. the system prompt template.
## Helper events ## Helper events

View file

@ -270,6 +270,13 @@ Under `/var/lib/hyperhive/agents/<name>/`:
- `hyperhive-harness.json` — rate-limited / needs-login flags read - `hyperhive-harness.json` — rate-limited / needs-login flags read
by the dashboard's async container-state fetch. See by the dashboard's async container-state fetch. See
`docs/web-ui/dashboard.md::Container row`. `docs/web-ui/dashboard.md::Container row`.
The agent writes its own instructions to `AGENTS.md` here; claude and
opencode load it automatically from their cwd, `state/`. At boot,
`agents_md_migrate::run` renames a `state/CLAUDE.md` to `AGENTS.md`
when no `AGENTS.md` exists. When both exist it renames neither and
logs an error naming both paths: claude loads only `CLAUDE.md` while
one exists in its cwd or above.
- `harness/` — harness-internal ephemeral state; not intended for - `harness/` — harness-internal ephemeral state; not intended for
agent consumption. Bind-mounted to `/agents/<name>/harness` agent consumption. Bind-mounted to `/agents/<name>/harness`
inside the container (`$HYPERHIVE_HARNESS_DIR`). Contents: inside the container (`$HYPERHIVE_HARNESS_DIR`). Contents:
@ -321,7 +328,7 @@ Under `/var/lib/hyperhive/agents/<name>/`:
and other large working data. Bind-mounted RW to and other large working data. Bind-mounted RW to
`/agents/<name>/work` inside the container and owned by the agent `/agents/<name>/work` inside the container and owned by the agent
user. It sits outside `state/` so a claude or subagent started under user. It sits outside `state/` so a claude or subagent started under
`work/` doesn't walk up into `state/CLAUDE.md` and load the agent's `work/` doesn't walk up into `state/AGENTS.md` and load the agent's
own identity. Survives `destroy`/recreate, gone on `--purge`. Counted own identity. Survives `destroy`/recreate, gone on `--purge`. Counted
in the per-agent disk metric (`measure_agent_disk`) together with in the per-agent disk metric (`measure_agent_disk`) together with
`state/` and the container rootfs. The dashboard doesn't serve it: `state/` and the container rootfs. The dashboard doesn't serve it:

View file

@ -9,5 +9,5 @@ drift out of sync the way a hand-copied mirror would — edit the crate's
own README to change what shows up here. own README to change what shows up here.
For the one-line-per-crate index (which crate does what, at a glance) For the one-line-per-crate index (which crate does what, at a glance)
see the main [`CLAUDE.md`](../../CLAUDE.md#repo-map) repo map instead; see the main [`AGENTS.md`](../../AGENTS.md#repo-map) repo map instead;
this section is each crate's own, longer word on itself. this section is each crate's own, longer word on itself.

View file

@ -210,7 +210,7 @@ alongside the claude dir.
`/var/lib/hyperhive/agents/<name>/work/` bind-mounts to `/var/lib/hyperhive/agents/<name>/work/` bind-mounts to
`/agents/<name>/work` (RW, owned by the agent user). It's the place for `/agents/<name>/work` (RW, owned by the agent user). It's the place for
git worktrees and build dirs: a claude started under `state/` walks up git worktrees and build dirs: a claude started under `state/` walks up
into `state/CLAUDE.md` and takes on the agent's identity, and one into `state/AGENTS.md` and takes on the agent's identity, and one
started under `work/` doesn't. `/home/<name>` lives in the container started under `work/` doesn't. `/home/<name>` lives in the container
rootfs, which `destroy` deletes; `work/` survives `destroy`/recreate rootfs, which `destroy` deletes; `work/` survives `destroy`/recreate
and goes on `--purge`. The per-agent disk metric counts it; the and goes on `--purge`. The per-agent disk metric counts it; the

View file

@ -383,7 +383,7 @@ of their own — see [ACP-reported cost](#acp-reported-cost).
| `hyperhive.agent.session.count` | — | counter | `model` (incremented once per fresh, non-`--continue`'d session) | | `hyperhive.agent.session.count` | — | counter | `model` (incremented once per fresh, non-`--continue`'d session) |
| `hyperhive.agent.loose_ends.threads` | — | gauge | none | | `hyperhive.agent.loose_ends.threads` | — | gauge | none |
| `hyperhive.agent.loose_ends.reminders` | — | gauge | none | | `hyperhive.agent.loose_ends.reminders` | — | gauge | none |
| `hyperhive.agent.claude_md.lines` | — | gauge | none — recorded from the `CLAUDE.md`-size watch's own ~15-minute tick, **not** per turn like the rows above | | `hyperhive.agent.claude_md.lines` | — | gauge | none — recorded from the `AGENTS.md`-size watch's own ~15-minute tick, **not** per turn like the rows above |
| `hyperhive.agent.subagent_role.lines` | — | gauge | `role` (the file stem) — one series per role file in `subagent_roles/`, on the same ~15-minute tick | | `hyperhive.agent.subagent_role.lines` | — | gauge | `role` (the file stem) — one series per role file in `subagent_roles/`, on the same ~15-minute tick |
| `hyperhive.agent.claude_usage.percent` | `%` | gauge | `window` (`five_hour`, `seven_day`, … as the usage endpoint names them) — polled every 5 minutes, **not** per turn | | `hyperhive.agent.claude_usage.percent` | `%` | gauge | `window` (`five_hour`, `seven_day`, … as the usage endpoint names them) — polled every 5 minutes, **not** per turn |
| `hyperhive.agent.claude_usage.resets_at` | `s` | gauge | `window` — unix seconds at which that window resets; same 5-minute poll | | `hyperhive.agent.claude_usage.resets_at` | `s` | gauge | `window` — unix seconds at which that window resets; same 5-minute poll |

View file

@ -162,7 +162,7 @@ lands in a faithful copy of the agent's environment:
agent's `/home/<name>/.claude` where its OAuth credentials live. choom agent's `/home/<name>/.claude` where its OAuth credentials live. choom
prefixes the machine with `<name>@` (the meta-flake sets the agent's prefixes the machine with `<name>@` (the meta-flake sets the agent's
unix user name to its label). unix user name to its label).
- **from `/agents/<name>/state`**: the session and `CLAUDE.md` (the - **from `/agents/<name>/state`**: the session and `AGENTS.md` (the
persona) resolve against the right project dir. persona) resolve against the right project dir.
- **with the harness flags**: `--settings`, `--mcp-config`, and - **with the harness flags**: `--settings`, `--mcp-config`, and
`--system-prompt-file` from `/run/hive-config/` — the same files the `--system-prompt-file` from `/run/hive-config/` — the same files the

View file

@ -210,9 +210,12 @@ still applies.
The child runs with `cwd = /state` (when the bind exists; falls The child runs with `cwd = /state` (when the bind exists; falls
back to the parent's cwd in dev), so any relative path in a tool back to the parent's cwd in dev), so any relative path in a tool
call (`Read foo.md`, `Bash ls`, `Write notes.md`) lands in the call (`Read foo.md`, `Bash ls`, `Write notes.md`) lands in the
agent's durable bind-mounted dir. CLAUDE.md autoload walks agent's durable bind-mounted dir. AGENTS.md autoload walks
upward from `/state` — drop a per-agent CLAUDE.md there if you upward from `/state` — drop a per-agent AGENTS.md there if you
want long-term hints that survive destroy/recreate. want long-term hints that survive destroy/recreate. A `CLAUDE.md`
in `/state` or above it shadows `AGENTS.md`: claude then loads only
the `CLAUDE.md` (see `docs/agent-lifecycle/persistence.md` for the
boot-time rename).
The wake prompt is intentionally minimal: the popped message's The wake prompt is intentionally minimal: the popped message's
`from`/`body`, prefixed with a `[msg #<id>]` broker-row-id marker `from`/`body`, prefixed with a `[msg #<id>]` broker-row-id marker

View file

@ -427,7 +427,7 @@ hit-rate** (`cache_read` over all input-side tokens) and
scheduled / delivered / pending** for the window. When the scheduled / delivered / pending** for the window. When the
per-session capture has data, a **first-turn ctx** chip shows the per-session capture has data, a **first-turn ctx** chip shows the
input tokens of the most recent fresh claude session's first turn — input tokens of the most recent fresh claude session's first turn —
a proxy for system-prompt + CLAUDE.md sprawl (a fresh session's first a proxy for system-prompt + AGENTS.md sprawl (a fresh session's first
turn pays the full static prefix uncached). It's derived in `stats.rs` turn pays the full static prefix uncached). It's derived in `stats.rs`
(`first_turn_ctx`: the first turn, `ORDER BY started_at LIMIT 1`, of (`first_turn_ctx`: the first turn, `ORDER BY started_at LIMIT 1`, of
the latest `sessions` row in the window) and stays out of the JSON the latest `sessions` row in the window) and stays out of the JSON

View file

@ -147,7 +147,7 @@ window.Chart = Chart;
chips.push(["sessions", fmtInt(s.session_count)]); chips.push(["sessions", fmtInt(s.session_count)]);
} }
// First-turn ctx: input tokens of the most recent fresh session's // First-turn ctx: input tokens of the most recent fresh session's
// first turn — the cold system-prompt + CLAUDE.md cost, a sprawl // first turn — the cold system-prompt + AGENTS.md cost, a sprawl
// proxy. Omitted from the JSON (and so absent here) until the // proxy. Omitted from the JSON (and so absent here) until the
// per-session capture has data. // per-session capture has data.
if (typeof s.first_turn_ctx === "number") { if (typeof s.first_turn_ctx === "number") {

View file

@ -33,7 +33,7 @@ Durable knowledge:
- write to `/agents/{label}/state/notes.md` (free-form) or any other path under `/agents/{label}/state/`. That directory is bind-mounted from the host and persists across container destroy/recreate — claude's `--continue` session only carries short-term context, but `/agents/{label}/state/` is forever. Read it back at the start of relevant turns to remember things across resets. - write to `/agents/{label}/state/notes.md` (free-form) or any other path under `/agents/{label}/state/`. That directory is bind-mounted from the host and persists across container destroy/recreate — claude's `--continue` session only carries short-term context, but `/agents/{label}/state/` is forever. Read it back at the start of relevant turns to remember things across resets.
**Work dir**: `/agents/{label}/work/` is the place for git worktrees, clones, build trees and other bulky scratch. It is bind-mounted from the host, persists across container destroy/recreate and is backed up. Keep `state/` for notes and small durable files. Don't keep work under `/home`, `/tmp` or other container paths — they are outside what's backed up. Start subagents with their `dir` under `work/` (that is also the default): a claude started under `state/` walks up into `state/CLAUDE.md` and takes on your identity, one under `work/` doesn't. **Work dir**: `/agents/{label}/work/` is the place for git worktrees, clones, build trees and other bulky scratch. It is bind-mounted from the host, persists across container destroy/recreate and is backed up. Keep `state/` for notes and small durable files. Don't keep work under `/home`, `/tmp` or other container paths — they are outside what's backed up. Start subagents with their `dir` under `work/` (that is also the default): a claude started under `state/` walks up into `state/AGENTS.md` and takes on your identity, one under `work/` doesn't.
Claude session (OAuth credentials) lives at `/root/.claude/` and persists across restarts. Claude session (OAuth credentials) lives at `/root/.claude/` and persists across restarts.

View 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"
);
}
}

View file

@ -1,5 +1,5 @@
//! `CLAUDE.md` size watch: raises a loose-ends-v2 todo when the agent's //! `AGENTS.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 //! own `AGENTS.md` grows past 500 lines, and feeds the same line count
//! into an OTEL gauge every tick regardless of threshold. Per the //! into an OTEL gauge every tick regardless of threshold. Per the
//! operator's own framing of the ask — "claude md is personality and //! 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 //! 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 //! summary) — a third instance of the same anti-nag pattern rather than
//! a new one. //! 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`), //! directory `claude --print` runs with as its cwd (`turn::session_cwd`),
//! which is how the CLI's own auto-load discovers it. //! which is how the CLI's own auto-load discovers it.
//! //!
@ -40,20 +40,24 @@ const WARN_LINES: u64 = 500;
const BUCKET: u64 = 50; const BUCKET: u64 = 50;
/// Producing subsystem marker for the todo row. /// Producing subsystem marker for the todo row.
const SUBSYSTEM: &str = "claude_md"; 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. /// agent.
const TODO_KEY: &str = "size"; 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; /// feed the OTEL gauge. Detached task — runs for the harness's lifetime;
/// errors are logged, never fatal. /// errors are logged, never fatal.
pub async fn run(todos: Arc<Todos>, wake: Arc<Notify>) { pub async fn run(todos: Arc<Todos>, wake: Arc<Notify>) {
loop { loop {
tokio::time::sleep(CHECK_INTERVAL).await; 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, Ok(l) => l,
Err(e) => { Err(e) => {
tracing::warn!(error = %e, "claude.md watch probe panicked"); tracing::warn!(error = %e, "agents.md watch probe panicked");
continue; 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) { Some(summary) => match todos.upsert(SUBSYSTEM, Some(TODO_KEY), summary, None, false) {
Ok((_, true)) => wake.notify_one(), Ok((_, true)) => wake.notify_one(),
Ok((_, false)) => {} 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 => { None => {
if let Err(e) = todos.clear(SUBSYSTEM, Some(TODO_KEY)) { 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 /// where `claude --print` runs (`turn::session_cwd`) and therefore where
/// its own auto-load discovers the file. /// its own auto-load discovers the file.
fn claude_md_path() -> std::path::PathBuf { fn agents_md_path(state_dir: &Path) -> std::path::PathBuf {
crate::paths::state_dir().join("CLAUDE.md") state_dir.join("AGENTS.md")
} }
/// Line count of the file at `path`. `None` when it can't be read — no /// 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. /// either.
fn line_count(path: &Path) -> Option<u64> { fn line_count(path: &Path) -> Option<u64> {
let contents = std::fs::read_to_string(path).ok()?; 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; let bucket = lines / BUCKET * BUCKET;
Some(format!( 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 \ 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. \ 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 \ 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 — \ that's genuinely what it is — no obligation either way. No need to act on this now — \
@ -175,16 +179,28 @@ mod tests {
#[test] #[test]
fn line_count_matches_editor_expectations() { fn line_count_matches_editor_expectations() {
let dir = tempfile::tempdir().expect("tempdir"); 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. // Three lines, trailing newline — an editor shows 3 lines, not 4.
std::fs::write(&path, "one\ntwo\nthree\n").expect("write"); std::fs::write(&path, "one\ntwo\nthree\n").expect("write");
assert_eq!(line_count(&path), Some(3)); 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] #[test]
fn missing_file_is_silent_not_an_error() { fn missing_file_is_silent_not_an_error() {
assert_eq!( assert_eq!(
line_count(Path::new("/definitely/does/not/exist/CLAUDE.md")), line_count(Path::new("/definitely/does/not/exist/AGENTS.md")),
None None
); );
} }

View file

@ -9,6 +9,7 @@
//! Single bin crate: the module tree below (formerly this crate's `lib.rs`, //! Single bin crate: the module tree below (formerly this crate's `lib.rs`,
//! before lib + bin were collapsed into one) plus the serve loop. //! before lib + bin were collapsed into one) plus the serve loop.
mod agents_md_migrate;
mod boot_announce; mod boot_announce;
mod claude_credentials; mod claude_credentials;
mod claude_md_watch; mod claude_md_watch;
@ -215,7 +216,7 @@ fn graceful_stop_message() -> hive_sh4re::inbox::DeliveredMessage {
body: "You are being gracefully stopped — the container will shut down after this turn, \ body: "You are being gracefully stopped — the container will shut down after this turn, \
and new inbound messages are already fenced. This is your one checkpoint turn: \ and new inbound messages are already fenced. This is your one checkpoint turn: \
flush anything worth keeping into your durable /state files now — update your \ flush anything worth keeping into your durable /state files now — update your \
notes / CLAUDE.md / TODO.md with in-flight task state, decisions made, important \ notes / AGENTS.md / TODO.md with in-flight task state, decisions made, important \
file paths, and whatever you'd need to resume cleanly later with only a summary \ file paths, and whatever you'd need to resume cleanly later with only a summary \
of this conversation to go on. Do not start new work or reply to anyone; just \ of this conversation to go on. Do not start new work or reply to anyone; just \
write your notes and end your turn. The session may be compacted after this turn \ write your notes and end your turn. The session may be compacted after this turn \
@ -450,7 +451,7 @@ fn spawn_todo_socket(
// top level instead of disk pressure. // top level instead of disk pressure.
tokio::spawn(state_entry_watch::run(store.clone(), todo_wake.clone())); tokio::spawn(state_entry_watch::run(store.clone(), todo_wake.clone()));
// Same shape again: nudge (and feed an OTEL gauge) on a // Same shape again: nudge (and feed an OTEL gauge) on a
// grown CLAUDE.md instead of a grown state dir. // grown AGENTS.md instead of a grown state dir.
tokio::spawn(claude_md_watch::run(store.clone(), todo_wake.clone())); tokio::spawn(claude_md_watch::run(store.clone(), todo_wake.clone()));
// Swarm-scheduled prompts fire as todos too, from the list the // Swarm-scheduled prompts fire as todos too, from the list the
// swarm keeps for this agent. // swarm keeps for this agent.
@ -553,6 +554,8 @@ async fn serve_main<S: Surface>(socket: &Path, poll_ms: u64) -> Result<()> {
tracing::error!(error = %e, "web_ui::serve exited with error"); tracing::error!(error = %e, "web_ui::serve exited with error");
} }
}); });
// Before the first turn, so claude's own auto-load finds `AGENTS.md`.
agents_md_migrate::run(&paths::state_dir());
// Harness-local reminders: a due reminder fires // Harness-local reminders: a due reminder fires
// straight into an mpsc channel the serve loop races against the // straight into an mpsc channel the serve loop races against the
// broker long-poll (unlike todos, a fire carries real per-row data, // broker long-poll (unlike todos, a fire carries real per-row data,

View file

@ -53,7 +53,7 @@ struct Instruments {
open_threads: Gauge<u64>, open_threads: Gauge<u64>,
open_reminders: Gauge<u64>, open_reminders: Gauge<u64>,
/// Recorded from [`crate::claude_md_watch`]'s own tick, not from /// Recorded from [`crate::claude_md_watch`]'s own tick, not from
/// [`record`] — `CLAUDE.md` size is a continuously-live value (like /// [`record`] — `AGENTS.md` size is a continuously-live value (like
/// hive-c0re's container gauges), not a once-per-turn one, so it has /// hive-c0re's container gauges), not a once-per-turn one, so it has
/// its own entry point rather than riding `record`'s per-turn call. /// its own entry point rather than riding `record`'s per-turn call.
claude_md_lines: Gauge<u64>, claude_md_lines: Gauge<u64>,
@ -110,7 +110,7 @@ pub fn record(row: &TurnStatRow, fresh_session: bool) {
} }
} }
/// Record the current `CLAUDE.md` line count. Called from /// Record the current `AGENTS.md` line count. Called from
/// [`crate::claude_md_watch`]'s periodic tick (every ~15 minutes, not /// [`crate::claude_md_watch`]'s periodic tick (every ~15 minutes, not
/// per turn) — see the field doc on [`Instruments::claude_md_lines`] for /// per turn) — see the field doc on [`Instruments::claude_md_lines`] for
/// why this doesn't ride [`record`]. No-op when OTEL isn't configured, /// why this doesn't ride [`record`]. No-op when OTEL isn't configured,

View file

@ -142,7 +142,7 @@ pub struct Snapshot {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub reminder_stats: Option<ReminderStats>, pub reminder_stats: Option<ReminderStats>,
/// First-turn input tokens of the most recent fresh claude session /// First-turn input tokens of the most recent fresh claude session
/// that started in the window — a proxy for system-prompt + CLAUDE.md /// that started in the window — a proxy for system-prompt + AGENTS.md
/// sprawl (a fresh session's first turn pays the full static prefix /// sprawl (a fresh session's first turn pays the full static prefix
/// uncached, so this is the current "cold context" cost). `None` until /// uncached, so this is the current "cold context" cost). `None` until
/// the sessions capture (per-session `session_id`) has data; every /// the sessions capture (per-session `session_id`) has data; every
@ -379,7 +379,7 @@ fn snapshot(path: &Path, window: Window) -> Result<Snapshot> {
} }
/// First-turn input tokens of the most recent fresh claude session that /// First-turn input tokens of the most recent fresh claude session that
/// started in `[from, now]`. Tracks system-prompt + CLAUDE.md sprawl: /// started in `[from, now]`. Tracks system-prompt + AGENTS.md sprawl:
/// the first turn of a fresh session (`--continue` suppressed) pays the /// the first turn of a fresh session (`--continue` suppressed) pays the
/// full static prefix as uncached input, so watching this over time /// full static prefix as uncached input, so watching this over time
/// surfaces creep. Uses the agreed per-session derive — the first turn /// surfaces creep. Uses the agreed per-session derive — the first turn

View file

@ -81,7 +81,7 @@ Your conversation context has grown large and the harness is about to run `/comp
which collapses the detailed turn-by-turn history into a short summary. Anything you \ which collapses the detailed turn-by-turn history into a short summary. Anything you \
do not persist now is effectively lost after the next turn.\n\n\ do not persist now is effectively lost after the next turn.\n\n\
Use THIS turn to flush anything worth keeping into your durable `/state` files: update \ Use THIS turn to flush anything worth keeping into your durable `/state` files: update \
your notes / CLAUDE.md / TODO.md with in-flight task state, decisions made, important \ your notes / AGENTS.md / TODO.md with in-flight task state, decisions made, important \
file paths, and whatever you would need to resume cleanly with only a summary of this \ file paths, and whatever you would need to resume cleanly with only a summary of this \
conversation to go on. Do not start new work or reply to anyone — just write your notes \ conversation to go on. Do not start new work or reply to anyone — just write your notes \
and end the turn."; and end the turn.";

View file

@ -47,5 +47,5 @@ to track it:
matrix provisioning, gateway vhosts, and the `hive-priv` client matrix provisioning, gateway vhosts, and the `hive-priv` client
respectively. respectively.
See the top-level `CLAUDE.md`/`docs/` index for the full reading-path See the top-level `AGENTS.md`/`docs/` index for the full reading-path
map. map.

View file

@ -80,7 +80,7 @@ struct StartArgs {
/// Working directory for the subagent's session — e.g. a git worktree /// Working directory for the subagent's session — e.g. a git worktree
/// you've already prepared for it, so a parallel batch of subagents /// you've already prepared for it, so a parallel batch of subagents
/// never race on the same working tree. Must exist. Omit to use your /// never race on the same working tree. Must exist. Omit to use your
/// work dir, `/agents/<you>/work` (it has no `CLAUDE.md` above it, so the /// work dir, `/agents/<you>/work` (it has no `AGENTS.md` above it, so the
/// subagent does not load yours); until that directory exists (a container /// subagent does not load yours); until that directory exists (a container
/// that predates its bind) the daemon's cwd is used. The daemon /// that predates its bind) the daemon's cwd is used. The daemon
/// remembers whichever `dir` you give here against `name`, so a later /// remembers whichever `dir` you give here against `name`, so a later

View file

@ -31,7 +31,7 @@ pub fn state_dir() -> PathBuf {
} }
/// The agent's persistent work dir, a sibling of [`state_dir`] /// The agent's persistent work dir, a sibling of [`state_dir`]
/// (`/agents/<name>/work`). It has no `CLAUDE.md` above it, so a subagent /// (`/agents/<name>/work`). It has no `AGENTS.md` above it, so a subagent
/// started here does not load the agent's own. /// started here does not load the agent's own.
#[must_use] #[must_use]
pub fn work_dir() -> PathBuf { pub fn work_dir() -> PathBuf {

View file

@ -853,8 +853,8 @@
"kind": "Panel", "kind": "Panel",
"spec": { "spec": {
"id": 33, "id": 33,
"title": "CLAUDE.md size by agent", "title": "AGENTS.md size by agent",
"description": "Lines in each agent's CLAUDE.md file over time.", "description": "Lines in each agent's AGENTS.md file over time.",
"links": [], "links": [],
"data": { "data": {
"kind": "QueryGroup", "kind": "QueryGroup",

View file

@ -10,7 +10,7 @@
# does NOT change the store path and therefore does NOT force a # does NOT change the store path and therefore does NOT force a
# rebuild of every agent container. # rebuild of every agent container.
# #
# Dropped: scripts/, docs/, root-level *.md (README/CLAUDE/TODO/…). # Dropped: scripts/, docs/, root-level *.md (README/AGENTS/TODO/…).
# Kept (build needs them): .nix, .rs, Cargo.*, branding/, # Kept (build needs them): .nix, .rs, Cargo.*, branding/,
# frontend/, prompts/, flake.lock. docs/ is deliberately dropped: # frontend/, prompts/, flake.lock. docs/ is deliberately dropped:
# it is shipped to agent containers as its OWN narrow meta-flake # it is shipped to agent containers as its OWN narrow meta-flake