hyperhive/CLAUDE.md
müde 5aad2d67e1 forge: mirror applied config repos to a private agent-configs org
on startup (and after every applied-repo ref mutation) core pushes
each agent's hive-c0re-owned applied repo — main plus every
proposal/approved/building/deployed/failed/denied tag — to
agent-configs/<name> on the local forge. the org is private and
agents are not members, so core is the only principal that can read
it.

the tokenised push url is passed inline, never stored as a named
remote: the applied repo is bind-mounted read-only into the manager,
so a token in .git/config would leak the core admin credential to an
agent.

push_config is best-effort at every site (ensure_all, spawn,
approve, deny, submit) — a missing or down forge never blocks a
deploy.
2026-05-20 10:24:50 +02:00

538 lines
29 KiB
Markdown

# 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 +
scratchpad.
- High-level project intro: **[README.md](README.md)**.
- Open work + backlog: **[TODO.md](TODO.md)**.
## File map
```
hive-c0re/ host daemon + CLI (one binary, subcommand-dispatched)
src/main.rs clap setup; serve / spawn / kill / rebuild / list /
pending / approve / deny / destroy [--purge] /
request-spawn; periodic vacuum tasks
src/server.rs host admin socket (HostRequest → dispatch)
src/client.rs admin-socket client
src/manager_server.rs manager-privileged socket (ManagerRequest)
src/agent_server.rs per-sub-agent socket listener (long-poll Recv)
src/broker.rs sqlite Message store + intra-process broadcast
channel (`MessageEvent`) for `recv_blocking` +
the dashboard forwarder; hourly vacuum of
delivered>30d
src/dashboard_events.rs unified wire-facing event channel feeding
`/dashboard/stream`. Carries broker `Sent` /
`Delivered` (mirrored by the forwarder task
in main.rs) + mutation events
(`ApprovalAdded` / `ApprovalResolved`,
`QuestionAdded` / `QuestionResolved`,
`TransientSet` / `TransientCleared`). Each
frame carries a monotonic per-process `seq`
clients use to dedupe against snapshot reads.
src/approvals.rs sqlite Approval queue + kinds
src/operator_questions.rs sqlite question queue backing `ask` /
`answer` (both operator + agent-to-agent)
src/questions.rs shared dispatch for `Ask` / `Answer` —
used by both agent + manager surfaces
src/reminder_scheduler.rs 5s poll loop: drains due reminders,
resolves file_path container→host, persists
payload + delivers pointer string
src/events_vacuum.rs host-side hourly sweep of every agent's
/state/hyperhive-events.sqlite
src/crash_watch.rs poll every 10s; fire HelperEvent::ContainerCrash
when a previously-running container disappears
without an operator-initiated transient
src/container_view.rs ContainerView struct + build_all helper;
shared between dashboard.rs (cold-load via
/api/state) and coordinator.rs's
rescan_containers_and_emit
src/coordinator.rs shared state (broker/approvals/operator_questions/
transient/sockets) + tombstone enumeration +
kick_agent + notify_agent (helper-event push) +
last_containers cache + rescan_and_emit diff helper
src/loose_ends.rs loose-ends aggregator (pending approvals +
unanswered questions + pending reminders) —
for_agent (filtered) and hive_wide (manager
surface). Backs AgentRequest::GetLooseEnds +
ManagerRequest::GetLooseEnds (the
get_loose_ends MCP tool).
src/actions.rs approve/deny/destroy (transient-aware)
src/auto_update.rs startup rebuild scan + ensure_manager +
meta::lock_update_hyperhive bump
src/lifecycle.rs `nixos-container` shellouts; per-agent applied
+ proposed git repo seeding; tag plumbing
src/meta.rs single hive-c0re-owned flake at /var/lib/
hyperhive/meta/ — sync_agents, two-phase
prepare/finalize/abort, lock_update_*
src/migrate.rs startup auto-migration from pre-meta layout
(idempotent, marker-guarded phase 4)
src/dashboard.rs axum HTTP: static shell + /api/state JSON + actions
+ journald viewer + bind-with-retry (SO_REUSEADDR)
+ deployed_sha chip per container +
/dashboard/{stream,history} subscribing to the
unified DashboardEvent channel
assets/ index.html, dashboard.css, app.js (include_str!)
hive-fr0nt/ shared frontend-assets crate (browser only).
src/lib.rs pub const BASE_CSS / TERMINAL_CSS / TERMINAL_JS /
MARKED_JS re-exports; both binaries
`include_str!` them and prepend to their per-
page serving routes.
assets/base.css Catppuccin palette + body typography (one source
of truth, no per-page redeclaration).
assets/terminal.css `.terminal-wrap` + `.live` + `.tail-pill` +
`.row` / `details.row` styling for both
pages' lit log panes. Unified prefix-column
(padding-left + negative text-indent) so glyph
alignment is consistent across row kinds + a
`.md` block scope for marked-rendered bodies.
assets/terminal.js `window.HiveTerminal.create(opts)`: scroll-
sticky log + "↓ N new" pill + history
backfill + SSE subscribe-buffer-snapshot-
dedupe dance. Pages register a kind→renderer
map; the terminal owns the lifecycle.
assets/marked.min.js vendored marked v4.0.2 UMD bundle. Per-agent
terminal uses the global `marked.parse` for
markdown bodies on send / recv / ask / answer
/ assistant text rows.
hive-ag3nt/ in-container harness crate; produces TWO binaries
src/lib.rs re-exports + DEFAULT_SOCKET, DEFAULT_WEB_PORT
src/client.rs generic JSON-line request/response over unix socket
src/web_ui.rs per-container axum HTTP page (incl /api/cancel,
/api/compact, /api/model, /events/history)
src/turn_stats.rs per-turn analytics sink (one sqlite row per
turn at /state/hyperhive-turn-stats.sqlite);
schema + best-effort writer
src/events.rs LiveEvent + broadcast Bus + sqlite-backed history
(/state/hyperhive-events.sqlite) + TurnState +
model selection (persisted at /state/hyperhive-model)
src/turn.rs claude --print + stream-json pump; --compact retry
src/mcp.rs embedded MCP server (rmcp): AgentServer + ManagerServer
src/login.rs probe /root/.claude/ for a valid session
src/login_session.rs drives `claude auth login` over stdio pipes
src/bin/hive-ag3nt.rs sub-agent main (Serve + Mcp subcommands)
src/bin/hive-m1nd.rs manager main (Serve + Mcp subcommands)
assets/ index.html, agent.css, app.js (include_str!)
prompts/ static role/tools/settings for claude (include_str!):
agent.md — sub-agent system prompt
manager.md — manager system prompt
claude-settings.json — --settings JSON
hive-sh4re/ wire types (HostRequest/Response, AgentRequest/Response,
ManagerRequest/Response, Message, Approval, HelperEvent)
nix/
modules/hive-c0re.nix systemd service + firewall + git wiring
templates/harness-base.nix shared scaffolding for sub-agents + manager
templates/agent-base.nix sub-agent nixosConfiguration
templates/manager.nix manager nixosConfiguration
docs/
conventions.md naming, identity=socket, async forms, commit style
gotchas.md NixOS / nspawn lessons learned the hard way
web-ui.md dashboard + per-agent page layouts and endpoints
turn-loop.md claude invocation, wake prompt, MCP tool surface
approvals.md approval flow, manager policy, helper events
persistence.md sqlite dbs, retention, state dir layout
```
## Reading paths
Pick the doc that matches your task. None depend on the others —
read them à la carte.
- **"What does the dashboard look like?"** →
[`docs/web-ui.md`](docs/web-ui.md).
- **"How does the per-agent terminal classify + colour
events?"** → [`docs/terminal-rendering.md`](docs/terminal-rendering.md)
(as-built row taxonomy + layout contract + markdown +
extra-MCP fallback).
- **"How does claude get its prompt and what tools does it have?"** →
[`docs/turn-loop.md`](docs/turn-loop.md).
- **"How do config changes flow from manager to operator to
container?"** → [`docs/approvals.md`](docs/approvals.md).
- **"What state survives destroy / purge / restart?"** →
[`docs/persistence.md`](docs/persistence.md).
- **"Naming, commit style, wire protocol, the `data-async`
pattern."** → [`docs/conventions.md`](docs/conventions.md).
- **"Why does the nspawn flag look like that?"** →
[`docs/gotchas.md`](docs/gotchas.md).
## Quick reminders
- **Commit before test.** Stage and commit when work *looks*
ready, then run validation. Failures get a follow-up commit
rather than an amend.
- **Commit messages: short, lowercase, no `Co-Authored-By`
trailer.** Imperative mood.
- **`rebuild` is the reconcile verb.** Anything that changes
per-container state on the host should be re-applied there so
the dashboard's `↻ R3BU1LD` is sufficient to recover.
- **Identity = socket.** No auth tokens — the socket path
identifies the principal.
- **Actions are factored** between admin socket and dashboard via
`actions.rs` and `dashboard.rs::lifecycle_action`, so the two
surfaces never drift.
## Scratchpad
In-flight or recent context that hasn't earned a section yet.
Prune freely.
- **Just landed:** applied config repos mirrored to the
forge. New private `agent-configs` Forgejo org (renamed
from the unused `agents` org in `SEEDED_ORGS`); core is the
only principal with access (site admin + private repos +
agents not members). `forge::push_config(name)` mirrors an
agent's hive-c0re-owned applied repo — `main` + every tag
(proposal/approved/building/deployed/failed/denied) — to
`agent-configs/<name>.git` via `git push --force`. The
tokenised URL is passed inline per push, never stored as a
named remote: the applied repo is RO-bind-mounted into the
manager at `/applied`, so a token in `.git/config` would
leak core's admin credential to an agent. Call sites:
`forge::ensure_all` (startup, per agent — catches migrate +
offline-forge drift), the spawn task in `actions::approve`
(+ `ensure_config_repo`), `actions::approve` ApplyCommit
branch, `actions::deny` ApplyCommit branch, and
`manager_server::submit_apply_commit`. All best-effort
(warn + continue). `ensure_repo` refactored to share a
`create_repo` helper with the new `ensure_org_repo`.
- **Just landed:** answer questions inline from the per-agent
web page. Question rows in the loose-ends section grew a
textarea + send button; the operator answers as operator by
POSTing cross-origin to the core dashboard's
`/answer-question/{id}` (CORS shim `with_cors` on that
route), never the per-agent socket — keeps the
operator-authority path off the agent's own socket. See
`TODO-ops.md` for the boundary rationale + the deployment/
gateway/privsep cluster.
- **Just landed:** sub-agents get a read-only view of their own
config repo. `set_nspawn_flags` now adds
`--bind-ro={proposed_dir}:/agents/<name>/config` for every
sub-agent container (manager unchanged — it already has the whole
`/agents` tree RW). The agent can read `agent.nix` + whatever
extra files the manager split the config into, so it can request
precise changes from the manager instead of guessing. RO is
load-bearing: config edits only ever flow through the manager's
proposed repo + the approval queue. `setup_proposed` seeds the
dir before spawn reaches `set_nspawn_flags`; a defensive
`create_dir_all` keeps a missing repo from becoming a
won't-boot container. Takes effect on next rebuild/restart of
each existing sub-agent. `agent.md` system prompt + `docs/
persistence.md` updated.
- **Just landed:** `request_apply_commit` fetch fix. The old
`git_fetch_to_tag` built a refspec `<sha>:refs/tags/proposal/<id>`
and ran `git fetch <proposed> <sha>:...` — but `git fetch` resolves
the left side of a refspec as a remote *ref name*, and a bare
commit sha is not one ("couldn't find remote ref ..."). Fetching
by sha would need a full 40-hex sha plus
`uploadpack.allow*SHA1InWant` on the remote. Surfaced on the first
real `request_apply_commit` (the `gui` agent bootstrap — initial
`deployed/0` seeding uses a different path). Fix: `git_fetch_to_tag`
now resolves the sha LOCALLY against the proposed repo
(`git rev-parse <sha>^{commit}`), fetches all of proposed's heads
into applied's object db (`+refs/heads/*:refs/remotes/proposal-src/*`),
then `git tag`s the resolved sha — all-local, no upload-pack
sha-want negotiation. Plus: `submit_apply_commit` now shape-checks
`commit_ref` is a 7-40 char hex sha (`validate_commit_ref`) and
rejects branch/tag names so the proposal always pins an immutable
commit. Tool description + `RequestApplyCommit` wire doc +
`docs/approvals.md` updated. 3 new tests in `manager_server::tests`.
- **Just landed:** inbox batching unified into `recv(max?)`.
No separate `recv_batch` tool — the existing `recv` tool
grew an optional `max: u32` arg (default 1, server-side
cap 32) so a single round-trip drains up to N popped rows
with the same delivery + ack bookkeeping per row
(`delivered_at = NOW`, `unacked_ids` list, redelivered
tag from `requeue_inflight`). `wait_seconds` still applies
to the FIRST message; once one lands the call drains up
to `max` in total — long-poll + drain compose. Wake
prompt's pending-inbox hint points at `recv(max: N)`.
Wire shape: `AgentRequest::Recv { wait_seconds, max }`
(added `max`), `AgentResponse::Messages { messages:
Vec<DeliveredMessage> }` (collapsed the old
`Message` + `Empty` + `Batch` trio into one always-list
variant — empty vec = idle). `DeliveredMessage` is a flat
shared struct in `hive-sh4re`. `format_recv` renders
single = the historical `from: X\n\nbody` block, multi =
`popped N message(s)` header with `---` separators +
per-message redelivery banners; empty = "(empty)". Broker
primitive: dropped the singular `recv`, kept just
`recv_batch(recipient, max)` and `recv_blocking_batch`
(which long-polls then drains via `recv_batch`). 4 new
broker tests on top of the existing 7 (recv_batch_*
family). Closes the "inbox batching hint" item from the
ergonomics wishlist with one tool instead of two; lower
context bloat in claude's prompt.
- **Just landed:** lease-style message delivery / no-drop
on turn fail. The `messages` table gained an `acked_at`
column (idempotent ALTER + backfill = `delivered_at` so
pre-migration delivered rows count as already-acked).
`Broker::recv` now returns `Delivery { id, redelivered,
message }` — the harness gets the row id back so
`AckTurn` can sweep every popped id at turn-end-OK. Two
new wire arms on both agent + manager surfaces:
`AckTurn` (drains the broker's per-recipient in-memory
`unacked_ids` list and stamps the rows `acked_at = NOW`)
and `RequeueInflight` (one-shot at harness boot: resets
`delivered_at = NULL` on every still-inflight row +
remembers each id so the next `Recv` carries
`redelivered: true`). Both bin loops call
`requeue_inflight` once before entering serve, and
`ack_turn` after every `TurnOutcome::Ok` (Failed +
PromptTooLong intentionally skip the ack so the popped
rows stay in-flight for the next boot's requeue).
`format_recv` + `format_wake_prompt` on both bins
surface a `[redelivered after harness restart — may
already be handled]` banner so claude knows the
side-effects of any previous handling may already have
happened. Lock order: `inflight` mutex first then
`conn` mutex in all three methods (`recv` / `ack_turn`
/ `requeue_inflight`) so a concurrent pop can't race
the requeue's DB update vs in-memory populate and
miss the redelivered tag. `vacuum_delivered` filter
flipped from `delivered_at < cutoff` to `acked_at IS
NOT NULL AND acked_at < cutoff` so unacked-but-
delivered rows survive vacuum (they're recoverable via
`requeue_inflight`). 7 new tests in `broker::tests`
cover happy path, crash recovery, idempotency, per-
recipient isolation, batch ack, vacuum preservation,
and FIFO ordering on requeue. Closes the "post-rebuild
system-message missed wake" bug class entirely (any
turn that wakes from a `delivered_at NOT NULL,
acked_at NULL` row resurfaces on next boot).
- **Just landed:** ctx + cost badges split. The per-agent
page now shows TWO chips — `ctx · N` (last inference's
prompt size = actual context window utilisation, parsed
from each `assistant` event's `.message.usage`; the
number to watch for compaction) and `cost · M` (sum
across every inference in the turn, the previous
behaviour now correctly labelled — tool-heavy turns
rebill the cached prefix per call and blow past the
model's window). Both fed by a single
`TokenUsageChanged { ctx, cost }` SSE event at turn-end
via `Bus::record_turn_usage`. `turn_stats` grew four
`last_*_tokens` columns (idempotent ALTER migration) so
cold-load seeds both badges from the most recent row.
Pre-migration rows yield no `ctx` seed (empty badge
until next turn) rather than a misleading zero.
- **Just landed:** per-agent terminal coherence pass.
Unified prefix column (padding-left + negative
text-indent so every row kind aligns); `<details>`
summaries drop the directional glyph and let CSS
`▸/▾` sit in the prefix column; turn boundaries
de-weighted (border-left rule only, no bold/margin/
tint); stderr lines render orange `!`,
operator-initiated notes mauve italic, catch-all `.sys`
escalated to orange so unrecognised stream-json
surfaces. Message-bearing tool calls
(`send`/`ask`/`answer`/`recv`) render default-open
with markdown bodies via vendored `marked` v4.0.2
(`hive-fr0nt::MARKED_JS`); assistant `text` rows also
markdown-rendered. Extra-MCP tools get a generic
args pretty-printer (`fmtArgsGeneric`) instead of
raw JSON. tool_use_id → name map carries through the
stream so `renderToolResult` knows when a result came
from `recv` and should default-open with markdown.
See `docs/terminal-rendering.md` for the as-built
taxonomy. Bonus: ctx badge seeded from `turn_stats`
on cold load via `Bus::seed_usage` so the chip paints
real numbers before the next turn finishes.
- **Just landed:** `open_threads``loose_ends` rename
(more honest about what the list is) + new
`cancel_loose_end(kind, id)` MCP tool on both
surfaces. `kind = "question"` posts a `[cancelled by
<self>]` answer to unblock the asker; `kind =
"reminder"` hard-deletes before fire. Auth: sub-agent
must own the row (`asker == self` / `owner == self`);
manager bypasses for hive-wide cleanup. `LooseEnd`
enum gained a `Reminder { id, owner, message, due_at,
age_seconds }` variant; sub-agent flavour filters by
owner, manager unfiltered. Shared dispatch in
`hive-c0re/src/questions.rs::handle_cancel_loose_end`.
Per-agent web UI's `/api/open-threads`
`/api/loose-ends`. Closes the agent-side "I have no
way to cancel what I queued" friction from the
ergonomics wishlist.
- **Just landed:** `whoami` MCP tool on both surfaces —
returns `{ name, role, pronouns, hyperhive_rev }`.
Lets an agent self-identify without scraping its own
system prompt; pronouns are pulled from the
`HIVE_OPERATOR_PRONOUNS` env that the system prompt
also substitutes, so the two stay in sync.
- **Just landed:** reminder delivery failures persist +
surface. The 5s scheduler now records the failure
reason on the `reminders` row instead of silently
dropping; the web UI surfaces them in the loose-ends
section so the owner agent can see "this reminder
never landed because the target was destroyed" without
reading journald.
- **Just landed:** path linkify. The broker tags every
message body server-side with `file_refs` at ingest
via `/api/state-file/check` (validates each path,
attaches container→host resolution). Last segment
must look like `name.ext` so directory mentions don't
fire spurious links. Dashboard renders refs as
collapsible path-preview blocks below the message
body; per-agent terminal picks them up from the same
field. Server-side validation means the JS doesn't
re-walk allow-lists on every render.
- **Just landed:** tombstones + meta_inputs as
`DashboardEvent`s. Closes the last two refetch loops
on the dashboard side — `purge-tombstone` and
`meta-update` POSTs now flip to 200 with
`data-no-refresh`. The 5s `/api/state` poll is gone
entirely; everything event-driven.
- **Just landed:** per-agent UI gained an open-threads
section (questions + approvals + reminders pending
against this agent) + the container row on the
dashboard gained a `⏰ N` pending-reminder count
chip. task_started / task_notification stream-json
events now pretty-render in the terminal with the
`⌁` glyph so subagent (Task tool) activity is
visually distinct from main-session tool calls.
- **Just landed:** per-agent extra MCP servers via the
`hyperhive.extraMcpServers.<key>` NixOS option in
`agent.nix`. Declares `{ command, args, env,
allowedTools }`; the module writes the whole map to
`/etc/hyperhive/extra-mcp.json`; the harness reads that
file and merges each entry into both `--mcp-config`
and `--allowedTools` (mapped to `mcp__<key>__<pattern>`).
Unblocks matrix / bitburner / any agent with rich
domain tooling — the agent flake's `inputs` block pulls
the external flake, `agent.nix` references it via
`flakeInputs.<name>.packages.${pkgs.system}.default`.
- **Just landed:** per-turn analytics sink. New
`hive-ag3nt::turn_stats` writes one row per claude turn to
`/state/hyperhive-turn-stats.sqlite`: identity (model,
wake_from, result_kind), timing (started/ended_at,
duration_ms), cost (full token-usage breakdown), behaviour
(tool_call_count + per-tool JSON map), and post-turn snapshot
metrics (open_threads_count, open_reminders_count fetched via
the existing GetOpenThreads + new CountPendingReminders RPC).
Both ag3nt + m1nd bin loops capture, both Bus accumulates
tool_use blocks via observe_stream during the stdout pump.
Writes are best-effort. No host-side vacuum yet — TODO under
Telemetry; same shape as events_vacuum, target 90d retention.
- **Just landed:** agent web UI event-driven badges. New
`LiveEvent::StatusChanged / ModelChanged / TokenUsageChanged
/ TurnStateChanged` variants replace the per-agent page's
/api/state polling for the state row. Status/model/token/state
badges all update from SSE; /api/state only fetched on cold
load + during the login flow (session output isn't event-
shaped). Per-agent endpoints (`/api/cancel|compact|model|
new-session`, `/login/*`) all flip 303→200. New `alive-badge`
chip carries the harness reachability signal (replaces the
"● harness alive" paragraph); new `ctx-badge` mirrors Claude
Code's bottom-right "N tokens" indicator. Every chip carries
a `title=...` tooltip for hover detail.
- **Just landed:** events_vacuum simplified to age-only —
`KEEP_SECS = 7d`, no row cap. Chatty turn no longer evicts
a quiet day's history sooner than expected. Hourly sweep
unchanged.
- **Just landed:** Phase 6 container events. New
`DashboardEvent::ContainerStateChanged { container }` +
`ContainerRemoved { name }` close the last refetch loop on the
dashboard side. `Coordinator::rescan_containers_and_emit` builds a
fresh `container_view::build_all` snapshot, diffs it against a
cached `last_containers` map, and fires per-row events for the
delta. Called from every mutation site: `actions::approve`
(post-spawn), `actions::destroy`, the `lifecycle_action` wrapper
in `dashboard.rs` (start/stop/restart/rebuild), `auto_update::
rebuild_agent`, and the existing 10s `crash_watch` poll loop.
`ContainerView` extracted to its own module so coordinator +
dashboard can both build it. Dashboard endpoints (`/restart`,
`/destroy`, `/kill`, `/rebuild`, `/start`, `/update-all`,
`/meta-update`, `/purge-tombstone`) now return 200; matching
forms carry `data-no-refresh` where the event coverage is
complete (purge + meta-update keep the refetch since tombstones
+ meta_inputs aren't event-derived yet). Client drops the 5s
periodic `/api/state` poll entirely — initial cold load + SSE
for everything afterwards; pending overlay reads from
`transientsState` since the new event payload doesn't carry it.
- **Just landed:** dashboard event refactor. New `hive-fr0nt`
workspace crate hosts shared frontend assets (palette + terminal
CSS + `window.HiveTerminal.create` JS) so both the dashboard and
the per-agent web UI render their live panes through the same
code; the dashboard's `#msgflow` now feels like the agent's
terminal (sticky-bottom + pill + lit chrome). New unified
`DashboardEvent` channel on `Coordinator` (replaces the
broker-only `/messages/stream`); a background forwarder mirrors
broker traffic onto it as `Sent` / `Delivered` variants, and
the mutation-event variants
(`ApprovalAdded` / `ApprovalResolved`, `QuestionAdded` /
`QuestionResolved`, `TransientSet` / `TransientCleared`) cover
every in-process state change the dashboard cares about. Each
frame carries a monotonic per-process `seq`; snapshot endpoints
return their seq alongside the state, and the terminal's
open-buffer-then-fetch-history dance drops any buffered frame
with `seq <= history_seq` so an event landing between subscribe
and history-fetch is neither shown twice nor lost. Operator
inbox + approvals + questions + transients are now derived
client-side from the event stream (cold-loaded from
`/api/state` for first paint, mutated live from SSE
thereafter); `/op-send` + per-agent `/send` return 200 instead
of 303-and-refetch. Container-list events still pending —
`ContainerView` is sourced from external `nixos-container list`,
so the 5s `/api/state` poll continues to drive the containers
section. Approval diffs are now raw unified-diff text on the
wire (per-line classification happens in JS) so they fit in
SSE payloads without HTML escaping. Bug fix: `LiveEvent::Note`
was a newtype variant that serde silently failed to serialize
— converted to `Note { text: String }` (wire shape matches what
the JS already read).
- **Just landed:** `ask_operator``ask` rename + optional
`to: <agent>` param for agent-to-agent structured Q&A.
Recipient defaults to the operator (dashboard); peer
questions land in the target's inbox as `QuestionAsked`
events and the recipient replies via new `answer(id,
answer)` tool. Answer always flows back as
`QuestionAnswered { id, question, answer, answerer }`
(renamed from `OperatorAnswered`; `answerer` distinguishes
operator vs peer vs `ttl-watchdog`). Authorisation:
operator-targeted questions can only be answered by the
operator; agent-targeted by the named target (or the
operator as override). Self-ask rejected. Shared dispatch
lives in `hive-c0re/src/questions.rs`. Dashboard's
`pending()` filters on `target IS NULL` so peer questions
never leak into the operator's queue.
- **Just landed:** dashboard now has a terminal-style
compose textbox under the message-flow stream — `@name`
picks the recipient (sticky in localStorage, auto-
completed from `containers[]`), POSTs `/op-send`. New
per-agent `↻ new session` button drops `--continue` for
one turn. Claude spawns with `cwd = /state` so relative
paths in tool calls land in the durable dir.
- **Just landed (prior overhaul still underneath):** tag-
driven config-apply. Two-repo split (proposed = manager
RW, applied = core-only); `request_apply_commit` fetches
the manager's commit into applied and pins it as
`proposal/<id>`; approve / deny / build walk through
tags on the same commit; `applied/main` only fast-
forwards on `deployed/`. `failed/` + `denied/` are
annotated. See `docs/approvals.md`.
- **Recent (since last compaction):** inline +/- diffs on
Write/Edit, send full body via collapsed details, operator
cancel + ttl on questions, deny-with-reason, dashboard
back-link + last-turn timing + model chip, per-agent inbox
view, bind-retry + SO_REUSEADDR, journald viewer,
agent.nix viewer, server-side TurnState, recv(wait_seconds)
max 180s, runtime /model switch + persistence to /state,
crash watcher + ContainerCrash / NeedsLogin / LoggedIn /
NeedsUpdate events, manager `update` tool, pure-hash
agent_web_port + collision banner + spawn/rebuild preflight,
browser notifications, focus-preserving refresh, generalised
<details data-restore-key> survival, prompt-on-submit pattern.
- **Open threads:** custom per-agent MCP tools (groundwork for
moving bitburner-agent into hyperhive), two-step spawn,
per-agent send allow-list, telemetry/charts, notes
compaction, unprivileged containers, Bash allow-list,
xterm.js. **Known bug** (in TODO.md): question id=5 was
queued but didn't render — likely a `pending()` row-decode
error swallowed by `unwrap_or_default`; investigate by curl
/api/state | jq '.questions' + browser console.