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
25 changed files with 217 additions and 45 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
219
AGENTS.md
Normal file
219
AGENTS.md
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
# hyperhive — claude entry point
|
||||||
|
|
||||||
|
Hey claude. This is your starting page. The detailed docs live in
|
||||||
|
[`docs/`](docs/) and are written for humans + you both — read them
|
||||||
|
when you need depth on a subsystem. This file is the index.
|
||||||
|
|
||||||
|
- High-level project intro: **[README.md](README.md)**.
|
||||||
|
- Open work + backlog: the forge issue tracker at
|
||||||
|
**`$HIVE_FORGE_URL/hyperhive/hyperhive/issues`**. Read the host out of
|
||||||
|
the env var — inside an agent container `localhost` is the *agent*, not
|
||||||
|
the forge, so a hardcoded loopback address fails to connect.
|
||||||
|
- Operator/agent trust-boundary design:
|
||||||
|
**[docs/trust-boundary/boundary.md](docs/trust-boundary/boundary.md)** (`area/ops` issues
|
||||||
|
for the deployment/gateway/privsep work — the separator is a **slash**,
|
||||||
|
and `--label area:ops` now fails with `did you mean "area/ops"?`).
|
||||||
|
- Agent trust model (trust boundary, prompt-injection threat model,
|
||||||
|
capability = accepted risk), credential isolation + sandbox threat model:
|
||||||
|
**[docs/trust-boundary/security.md](docs/trust-boundary/security.md)**.
|
||||||
|
- Odd-looking nspawn / container plumbing usually has a story:
|
||||||
|
**[docs/process/gotchas.md](docs/process/gotchas.md)**.
|
||||||
|
|
||||||
|
## Build & check
|
||||||
|
|
||||||
|
Always through the devshell — a bare `cargo` fails (`failed to find tool
|
||||||
|
"cc"`, `cannot find -lsqlite3`). Details:
|
||||||
|
[`docs/process/conventions.md`](docs/process/conventions.md#building--local-checks).
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nix develop -c cargo clippy --all-targets -- -D warnings
|
||||||
|
nix develop -c cargo test
|
||||||
|
nix fmt # treefmt is the CI gate, NOT bare cargo fmt
|
||||||
|
```
|
||||||
|
|
||||||
|
Changed a `hivectl` / `swarmctl` verb or flag? Regenerate its doc — only
|
||||||
|
CI's `nix flake check` catches the drift:
|
||||||
|
`nix develop -c cargo run --bin hivectl -- markdown-docs > docs/tools/hivectl-cli.md`
|
||||||
|
(same for `swarmctl` → `docs/tools/swarmctl-cli.md`). CI red but local
|
||||||
|
green → build the suspect check (e.g. `nix build
|
||||||
|
.#checks.x86_64-linux.hivectl-docs`) before blaming the builder.
|
||||||
|
|
||||||
|
## Repo map
|
||||||
|
|
||||||
|
One line per crate / top-level dir. **Each module's authoritative,
|
||||||
|
always-current description lives in its own `//!` doc-comment** —
|
||||||
|
`grep`/read the module when you need detail. This index is kept
|
||||||
|
deliberately lean: it auto-loads into every turn's context, and a
|
||||||
|
hand-maintained per-file tree drifts out of sync with the code.
|
||||||
|
|
||||||
|
### Rust workspace (`Cargo.toml` members)
|
||||||
|
|
||||||
|
Per-hive (host + in-container):
|
||||||
|
|
||||||
|
- **`hive-c0re/`** — host daemon (runs as the unprivileged `hive-core`
|
||||||
|
user), **daemon-only** — the operator CLI is `hivectl`. Owns the sqlite
|
||||||
|
broker, reminder queue, meta flake, lifecycle (`nixos-container`
|
||||||
|
shellouts), gateway wiring, per-container stats and the axum operator
|
||||||
|
dashboard. Agent forge/matrix accounts are `swarm-controller`'s. Largest
|
||||||
|
crate.
|
||||||
|
- **`hivectl/`** — operator CLI; talks to `hive-c0re` over the host admin
|
||||||
|
socket, does NOT link it. Verb reference (CI-enforced):
|
||||||
|
[`docs/tools/hivectl-cli.md`](docs/tools/hivectl-cli.md).
|
||||||
|
- **`hive-priv/`** — minimal root helper, socket-activated at
|
||||||
|
`/run/hive/priv.sock`; the few root ops (bind-mount edits, nsenter)
|
||||||
|
`hive-c0re` delegates. See `docs/trust-boundary/boundary.md`.
|
||||||
|
- **`hive-agent/`** — the in-container serve loop: turn-loop *policy*
|
||||||
|
(`turn.rs`) over `hive-runtime`, per-agent web UI, event + turn-stats
|
||||||
|
sinks, system-prompt renderer, claude launch config (`mcp_config.rs`).
|
||||||
|
With `hive-agent-mcp` it's what "hive-ag3nt" refers to — there is no
|
||||||
|
such directory.
|
||||||
|
- **`hive-agent-mcp/`** — the embedded MCP server (`hive-mcp-http` unit).
|
||||||
|
- **`hive-runtime/`** — the `Runtime` interface an agent's turns run on:
|
||||||
|
a `claude` backend (over `hive-claude`) and an `acp` backend whose stream
|
||||||
|
is translated into claude's `stream-json` shape. Links no hyperhive
|
||||||
|
binary crate, so the subagent daemon uses it too.
|
||||||
|
- **`hive-bash-mcp/`**, **`hive-matrix-mcp/`**, **`hive-subagent-mcp/`** —
|
||||||
|
per-agent MCP daemons serving streamable-http directly (no stdio
|
||||||
|
bridge): background bash tasks, matrix-sdk tools, nested subagent
|
||||||
|
sessions (optionally goal-driven over several turns; daemon state is
|
||||||
|
in-memory, the claude session survives a restart).
|
||||||
|
- **`hive-screen-mcp/`** — stdio MCP bridge for GUI agents: screenshot,
|
||||||
|
keyboard, mouse. All userspace.
|
||||||
|
- **`hive-forge/`** — `hive-forge` Forgejo CLI; one module per verb.
|
||||||
|
- **`hive-forge-notify/`** — notification pollers → todos on the in-agent
|
||||||
|
socket. Two binaries: `hive-forge-notify`, `hive-github-notify`.
|
||||||
|
- **`hive-jobq/`** — the job-DAG scheduler library. **Runtime-only —
|
||||||
|
nothing writes the graph to disk**, so node ids are stable within a run,
|
||||||
|
not across restarts. `hive-c0re`'s `job_queue/` is being removed in its
|
||||||
|
favour — new scheduler-shaped code goes here.
|
||||||
|
- **`hive-jobq-wire/`** — presentation of a `hive-jobq` graph to viewers
|
||||||
|
(`WireNode` / `WireResource`); kept out of `hive-jobq` on purpose.
|
||||||
|
- **`hive-jobq-metrics/`** — OTEL export of a graph's state rollup on a
|
||||||
|
timer.
|
||||||
|
- **`hive-log/`** — the one log-sink chooser: journald *or* stdout, never
|
||||||
|
both (under a unit, stdout already is the journal). Do not grow it into a
|
||||||
|
utility crate.
|
||||||
|
- **`hive-metric/`** — one-shot CLI pushing a labeled metric over OTLP.
|
||||||
|
|
||||||
|
Wire types + socket plumbing:
|
||||||
|
|
||||||
|
- **`hive-types/`** — leaf newtypes, chiefly the validated `Ident`.
|
||||||
|
- **`hive-sh4re/`** — shared payload types (`Message`, `HelperEvent`, …).
|
||||||
|
- **`hive-host-sock/`** (hivectl ↔ c0re), **`hive-priv-sock/`** (c0re ↔
|
||||||
|
priv), **`hive-core-agent-sock/`** (harness ↔ c0re, `/run/hive/mcp.sock`)
|
||||||
|
— per-socket wire types, split out so each client links only its protocol.
|
||||||
|
- **`hive-agent-sock/`** — the *in-agent* socket the harness serves to
|
||||||
|
in-container producers (todos, reminders); also holds the `extra_mcp`
|
||||||
|
spec. ⚠️ **This socket never leaves the container** — `hive-c0re` is not
|
||||||
|
in the path, unlike `hive-core-agent-sock`.
|
||||||
|
- **`hive-sock-client/`** — the shared JSON-line unix-socket client; retry
|
||||||
|
is a policy value (`Retry::None` / `Retry::RideOutRestart`). Also holds
|
||||||
|
the bounded request-line reader (`read_request_line`) the socket servers
|
||||||
|
use.
|
||||||
|
|
||||||
|
Swarm-level:
|
||||||
|
|
||||||
|
- **`swarm-controller/`** — owns what is true **across** hives; opt-in,
|
||||||
|
one per swarm. Serves HTTP over a unix socket nginx proxies to — ⚠️
|
||||||
|
**the socket's directory is its access control**; the crate README has
|
||||||
|
the constraint and a unit test pins the path.
|
||||||
|
- **`swarm-controller-api/`** — serde-only request and response bodies of
|
||||||
|
the controller's HTTP API, shared with `swarmctl` (forge, schedules and
|
||||||
|
the agent-create and forge-token responses).
|
||||||
|
- **`swarmctl/`** — swarm operator CLI; runs as **root and acts directly**
|
||||||
|
(README says why). Doesn't link `swarm-controller`: the forge and
|
||||||
|
schedule bodies and the agent responses come from
|
||||||
|
`swarm-controller-api`; the agent-create request, inference and
|
||||||
|
permissions ones are a local mirror. ⚠️ Edits authelia's
|
||||||
|
**`users.yml` in place**, shared with `swarm-authelia-bridge`; that
|
||||||
|
crate's README says what both writers must uphold. Verb reference
|
||||||
|
(CI-enforced):
|
||||||
|
[`docs/tools/swarmctl-cli.md`](docs/tools/swarmctl-cli.md).
|
||||||
|
- **`swarm-authelia-bridge/`** (+ **`swarm-authelia-bridge-sock/`** wire
|
||||||
|
types) — the other writer of authelia's users DB; runs inside
|
||||||
|
`swarm-authelia`.
|
||||||
|
- **`swarm-secret-client/`** — OpenBao client over `vaultrs`. ⚠️ Reads
|
||||||
|
the **`BAO_`** env spellings explicitly — `vaultrs`'s `VAULT_*` defaults
|
||||||
|
are set by no unit here, and falling through to them yields a client
|
||||||
|
with **no identity** and a TLS failure that names no cause.
|
||||||
|
- **`swarm-queue-client/`** — the one authenticated connect to the swarm
|
||||||
|
NATS queue.
|
||||||
|
- **`swarm-nats-auth/`** — NATS auth-callout responder; without it the
|
||||||
|
queue is fail-closed and admits nobody.
|
||||||
|
- **`swarm-agent-verify/`** — checks a `swarm-agent.<agent>.<secret>`
|
||||||
|
token against the store; shared by `swarm-nats-auth` and
|
||||||
|
`swarm-logs-auth`.
|
||||||
|
- **`swarm-logs-auth/`** — nginx `auth_request` responder for agent access
|
||||||
|
to the log store, and to the metrics store as `swarm-metrics-agent-auth`.
|
||||||
|
- **`swarm-metrics-auth/`** — nginx `auth_request` responder for Grafana's
|
||||||
|
bearer on the metrics store's query API.
|
||||||
|
- **`swarm-logs/`** — agent CLI: a LogsQL query in, log lines out.
|
||||||
|
- **`swarm-metrics/`** — agent CLI: a MetricsQL query in, the store's JSON out.
|
||||||
|
- **`swarm-matrix-client/`** — appservice calls against the swarm
|
||||||
|
homeserver, shared by `swarm-controller` and `swarm-matrix-ctl`.
|
||||||
|
- **`swarm-matrix-ctl/`** — the one binary run inside
|
||||||
|
`containers.hive-matrix`; new in-container jobs are verbs here, not
|
||||||
|
crates.
|
||||||
|
- **`swarm-builder/`** — builds agent config PR heads for commit status,
|
||||||
|
and evaluates the agent archetypes the create form lists.
|
||||||
|
Design: [`docs/swarm/builder.md`](docs/swarm/builder.md).
|
||||||
|
- **`swarm-ci-metrics/`** — polls the forge's Actions runs and pushes each
|
||||||
|
finished run's duration and queue wait to the metrics store. Design:
|
||||||
|
[`docs/swarm/ci-metrics.md`](docs/swarm/ci-metrics.md).
|
||||||
|
- **`swarm-claude-login/`** — runs `claude auth login` for one agent and
|
||||||
|
stores the credentials at `swarm/agents/<agent>/claude-credentials`.
|
||||||
|
Design: [`docs/swarm/claude-login.md`](docs/swarm/claude-login.md).
|
||||||
|
- **`swarm-review-classifier/`** — one `claude --print` per PR
|
||||||
|
classification, no tools, no forge credential. Design:
|
||||||
|
[`docs/swarm/review-classifier.md`](docs/swarm/review-classifier.md).
|
||||||
|
|
||||||
|
### External dependencies with no directory here
|
||||||
|
|
||||||
|
- **`hive-claude`** — app-agnostic driver for headless `claude --print`
|
||||||
|
(stream-json, `Telemetry`, the self-compacting `InfiniteSession`).
|
||||||
|
⚠️ **Its own repo, not a workspace member** — there is no
|
||||||
|
`hive-claude/` directory and `grep` here will not find it.
|
||||||
|
|
||||||
|
### Other top-level dirs
|
||||||
|
|
||||||
|
- **`frontend/`** — npm workspaces built hermetically by
|
||||||
|
`nix/packages/frontend.nix`: `shared`, `dashboard` (operator SPA),
|
||||||
|
`agent` (per-container UI), `swarm-ui` (Preact; packaged separately by
|
||||||
|
`nix/packages/swarm-ui.nix`, not in `packages.default`'s closure).
|
||||||
|
- **`nix/`** — `host-modules/` (the host stack), `agent-modules/`
|
||||||
|
(per-agent harness features), `container-modules/` (guest side of
|
||||||
|
service containers), `templates/agent.nix` (the container entry
|
||||||
|
point), `archetypes/` (the built-in agent archetypes), `packages/`,
|
||||||
|
`docs/` (options doc), `module-eval/` (NixOS
|
||||||
|
module-eval tests), `builder/` (`check-flake`), `script-tests/`, plus
|
||||||
|
`sources.nix` / `rust.nix` / `checks.nix` / `devshell.nix` /
|
||||||
|
`treefmt.nix` behind the thin `flake.nix`.
|
||||||
|
- **`docs/`** — subsystem reference docs (see *Reading paths* below).
|
||||||
|
- **`branding/`**, **`scripts/`** — static assets + helper scripts.
|
||||||
|
|
||||||
|
## Reading paths
|
||||||
|
|
||||||
|
**[`docs/README.md`](docs/README.md) is the single index**, grouped by
|
||||||
|
topic. Don't add a second copy of it here — a parallel list used to live
|
||||||
|
in this file and drifted.
|
||||||
|
|
||||||
|
## Conventions & process
|
||||||
|
|
||||||
|
- **Never add `#[allow(clippy::…)]`** — fix the lint instead (extract a
|
||||||
|
helper, add backticks, etc.). Worked examples:
|
||||||
|
[`docs/process/conventions.md`](docs/process/conventions.md#building--local-checks).
|
||||||
|
- **Pre-push lint hook** (catches tracker-tag and comment-block failures
|
||||||
|
before CI does — install once per clone):
|
||||||
|
`ln -sf ../../scripts/pre-push .git/hooks/pre-push`
|
||||||
|
- **Comment/PR-description length**: size it to how non-obvious the
|
||||||
|
thing is, not to how much investigation it took to find it — a
|
||||||
|
mechanical fix gets a one-line comment, a genuinely non-obvious
|
||||||
|
invariant or decision earns real prose. Don't restate a module's `//!`
|
||||||
|
doc in a comment further down the same file.
|
||||||
|
- **Operator-facing doc with real implementation detail mixed in**: wrap
|
||||||
|
the implementation part in a `<details><summary>…</summary>` block
|
||||||
|
(blank line after `<summary>` so the markdown inside renders) rather
|
||||||
|
than a sibling page — see `docs/integrations/github.md`'s
|
||||||
|
"Implementation" section. It only collapses in a rendered browser, and
|
||||||
|
the `<summary>` line isn't markdown-processed, so keep it plain prose
|
||||||
|
with no backticks.
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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") {
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.";
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue