split claude.md into docs/ — per-topic, human-readable

claude.md was eating 400 lines of subsystem detail that's useful
when you're working on that subsystem and noise the rest of the
time. split into:

- docs/conventions.md   naming, identity, async forms, commit style
- docs/gotchas.md       nspawn / nixos-container quirks
- docs/web-ui.md        dashboard + per-agent layouts and endpoints
- docs/turn-loop.md     claude invocation, wake prompt, mcp surface
- docs/approvals.md     approval flow, manager policy, helper events
- docs/persistence.md   sqlite dbs, retention, state dir layout

claude.md is now the entry point — file map, reading paths
("pick the doc that matches your task"), quick reminders that
fit on one screen, and a small scratchpad section for in-flight
context. references the docs; the docs don't reference claude.md.

no content was lost — the docs/ files cover everything the old
claude.md did, plus things i wrote up better while extracting.
This commit is contained in:
müde 2026-05-15 20:17:11 +02:00
parent c27111ac32
commit 8b10731aa4
7 changed files with 708 additions and 396 deletions

143
docs/web-ui.md Normal file
View file

@ -0,0 +1,143 @@
# Web UI
Two web surfaces share the same skeleton: the dashboard (port 7000)
and the per-agent UIs (manager on :8000, sub-agents on a hashed
:8100-8999). Both are SPAs — `GET /` returns a static shell,
`/api/state` returns JSON, JS renders. No full-page reloads.
## Shape (shared by both)
- `GET /``assets/index.html` (placeholders for state-driven
sections, shipped via `include_str!` so the binary has no runtime
file dependency).
- `GET /static/*.css` + `GET /static/*.js` → static assets.
- `GET /api/state` → JSON snapshot the JS app renders into the DOM.
- `GET /events/stream` (per-agent) / `GET /messages/stream`
(dashboard) → `text/event-stream` SSE for live updates.
The JS app handles all `form[data-async]` submissions via a delegated
listener: read `data-confirm`, swap the button to a spinner, POST
`application/x-www-form-urlencoded`, re-enable the button on success
(refreshState may keep the form mounted, so we don't rely on a
re-render), call `refreshState()`. State shapes live in
`dashboard.rs::StateSnapshot` and `web_ui.rs::StateSnapshot`
when adding state fields, plumb through the snapshot struct and the
relevant `assets/app.js` render function.
## Dashboard sections (top to bottom)
1. **C0NTAINERS** — live containers with their action surface.
2. **K3PT ST4T3** — destroyed-but-state-kept tombstones (size +
age + claude-creds badge). Two actions: `⊕ R3V1V3` (queues a
Spawn approval; existing state is reused), `PURG3` (wipes
state + applied dirs; `POST /purge-tombstone/{name}`).
3. **M1ND H4S QU3STI0NS** — pending `ask_operator` questions
(amber pulsing border). Always renders a free-text fallback
alongside any option list; `multi=true` renders options as
checkboxes; submit merges selections + free text comma-joined.
4. **0PER4T0R 1NB0X** — recent messages addressed to `operator`
(last 50, from the broker).
5. **P3NDING APPR0VALS** — the queue. The R3QU3ST SP4WN form
lives at the top of this section since submitting it immediately
queues an approval that lands directly below.
6. **MESS4GE FL0W** — live broker SSE tail.
### Container row
Two-line layout (`assets/app.js::renderContainers`):
- Line 1: agent name (link → new tab), m1nd/ag3nt chip, `needs
login` / `needs update` warning badges, in-flight `◐ pending-state…`
pill (replaces buttons during start/stop/restart/rebuild/destroy),
container name + port.
- Line 2: action buttons — `↻ R3BU1LD` always, `DESTR0Y` + `PURG3`
on sub-agents, `↺ R3ST4RT` + (sub-agents) `■ ST0P` when running,
`▶ ST4RT` when stopped. Buttons dim + disable while a transient
lifecycle action is in flight.
`↻ UPD4TE 4LL` button appears above the containers list when any
agent is stale.
Banner pulses on each broker SSE event (`pulseBanner` with a 4 s
grace timer).
### Dashboard endpoints
- `POST /{approve,deny}/{id}` — approve/deny a pending approval.
- `POST /{rebuild,kill,restart,start,destroy}/{name}` — lifecycle.
- `POST /purge-tombstone/{name}` — wipe a tombstone's state dirs.
- `POST /answer-question/{id}` — answer a pending operator question.
- `POST /request-spawn` — queue a Spawn approval.
- `POST /update-all` — rebuild every stale container.
## Per-agent page
Layout, top to bottom:
- Banner (gradient shimmer while state=thinking).
- Title with `↻ R3BU1LD` button.
- Status section (online / needs login / login-in-progress).
- State badge row (`💤 idle / 🧠 thinking / ○ offline · <age>`) +
`■ cancel turn` button visible while state=thinking.
- Terminal-wrap: live event tail (with sticky-bottom auto-scroll
and a `↓ N new` pill when not at bottom) followed by an
operator-input textarea acting as a prompt.
### Live view
Each agent runs an `events::Bus`: a `tokio::sync::broadcast<LiveEvent>`
plus a sqlite-backed history at `/state/hyperhive-events.sqlite`.
The harness emits `TurnStart { from, body, unread }`,
`Stream(value)` (one per parsed stream-json line), `Note`,
`TurnEnd { ok, note }`. The web UI:
- fetches `GET /events/history` on page load and replays the last
2000 events (oldest first, with `.no-anim` so they don't stagger);
- then subscribes to `GET /events/stream` (SSE) for live tail;
- shows a granular state badge above the terminal, driven from
`turn_start`/`turn_end`, with a flash animation on transition;
- sticky-bottom auto-scroll: scrolling up parks the view; new rows
surface a "↓ N new" pill instead of yanking;
- terminal-themed: phosphor mauve glow, Crust bg, backdrop-filter
blur, row fade-in slide-up.
Per-stream rendering:
- `Stream` `tool_use``→ Read /path` / `→ Bash $ cmd` / `→ send →
operator: "..."` etc., per-tool pretty rather than raw JSON.
- `Stream` `tool_result` short → flat `← ...`; long → collapsed
`<details>` `▸ ← Nl · headline` (click to expand full body).
- `Stream` `thinking` → text content if claude provided one,
otherwise the bare `· thinking …` indicator.
- `Stream` `system init`, `result`, `rate_limit_event` are dropped
— too noisy.
- `Note``· text`.
- `TurnEnd``✓ turn ok` / `✗ turn fail — note`, triggers a
`refreshState()`.
### Terminal-embedded prompt
The operator input lives *inside* the terminal-wrap as a
prompt-style textarea below the live tail: multi-line (Enter sends,
Shift+Enter newlines), tab-completes slash commands.
Slash commands today:
- `/help` — list commands locally
- `/clear` — wipe the local terminal view (server history kept)
- `/cancel``POST /api/cancel` → host shellouts `pkill -INT
claude`, emits a Note. Also surfaces as a `■ cancel turn` button
in the state row while state=thinking.
- `/compact``POST /api/compact` → host spawns
`turn::compact_session` in the background; output streams into
the live panel.
Unknown `/foo` shows an error row instead of being silently sent.
### Per-agent endpoints
- `POST /send` — operator-injected message into this agent's inbox.
- `POST /login/{start,code,cancel}` — claude OAuth login flow.
- `POST /api/cancel` — SIGINT the in-flight claude turn.
- `POST /api/compact` — run `/compact` on the persistent session.
- `GET /events/history` — replay buffer for the terminal.