From 8406a45275f194958fa5b7f41802339737522cb3 Mon Sep 17 00:00:00 2001 From: atlas Date: Thu, 2 Jul 2026 19:55:26 +0200 Subject: [PATCH] feat(#1997): add prettier markdown formatter to treefmt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .prettierrc: proseWrap=preserve (no prose reflow) - .prettierignore: exclude hivectl-cli.md (auto-generated) + 11 docs with multi-line list-item continuations prettier would strip to col 0 (CommonMark limitation in prettier's list handling) - format 16 markdown files: cosmetic only (*โ†’_, table alignment, heading normalisation) โ€” verified no broken continuations, idempotent --- .prettierignore | 16 +++++ README.md | 24 ++++---- docs/agent-hierarchy.md | 41 ++++++------- docs/boundary.md | 10 ++-- docs/ci.md | 3 +- docs/coordinator.md | 39 +++++++------ docs/forge.md | 38 ++++++------ docs/gotchas.md | 4 +- docs/network.md | 37 ++++++------ docs/security.md | 56 +++++++++--------- docs/swarm.md | 6 +- docs/tools/bash.md | 9 +-- docs/tools/forge.md | 26 ++++----- docs/tools/lifecycle.md | 14 ++--- docs/web-ui/css-vars.md | 109 ++++++++++++++++++----------------- docs/web-ui/shape.md | 14 ++--- hive-ag3nt/prompts/system.md | 38 ++++++------ 17 files changed, 254 insertions(+), 230 deletions(-) diff --git a/.prettierignore b/.prettierignore index 01999f63..30e637ce 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,3 +2,19 @@ # The hivectl-docs CI check diffs this against fresh binary output, # so reformatting it would break that check. docs/tools/hivectl-cli.md + +# Files with multi-line list-item continuations that prettier strips to col 0. +# prettier's `proseWrap: "preserve"` prevents prose reflow but not list-item +# indentation normalisation, so these files need manual re-wrap before +# adding them back to the formatter scope. +CLAUDE.md +docs/approvals.md +docs/conventions.md +docs/gateway.md +docs/matrix.md +docs/persistence.md +docs/terminal-rendering.md +docs/tools/matrix.md +docs/turn-loop.md +docs/web-ui/agent.md +docs/web-ui/dashboard.md diff --git a/README.md b/README.md index b33acc54..b83d55a4 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ > approves them in a browser, every deploy is a tag. cyberpunk-themed > dashboard included. ๐Ÿ’œโšก -Claude code is great in one window, *exponentielle* across many โ€” but +Claude code is great in one window, _exponentielle_ across many โ€” but only if you can keep the agents from stepping on each other, give them durable identity, and stop them from eating production. hyperhive is the substrate. @@ -43,15 +43,15 @@ host (NixOS, runs hive-c0re.service) Depth lives in [`docs/`](docs/) โ€” pick the one matching your task: -| reading path | doc | -| --- | --- | -| dashboard layout + endpoints | [`docs/web-ui.md`](docs/web-ui.md) ([shape](docs/web-ui/shape.md) ยท [dashboard](docs/web-ui/dashboard.md) ยท [agent](docs/web-ui/agent.md)) | -| claude turn loop + MCP tools | [`docs/turn-loop.md`](docs/turn-loop.md) | -| config-edit + approval state machine | [`docs/approvals.md`](docs/approvals.md) | -| what survives destroy / purge / restart | [`docs/persistence.md`](docs/persistence.md) | -| naming, wire protocol, commit style | [`docs/conventions.md`](docs/conventions.md) | -| nginx vhost map + sub-domain routing | [`docs/gateway.md`](docs/gateway.md) | -| NixOS / nspawn gotchas | [`docs/gotchas.md`](docs/gotchas.md) | +| reading path | doc | +| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| dashboard layout + endpoints | [`docs/web-ui.md`](docs/web-ui.md) ([shape](docs/web-ui/shape.md) ยท [dashboard](docs/web-ui/dashboard.md) ยท [agent](docs/web-ui/agent.md)) | +| claude turn loop + MCP tools | [`docs/turn-loop.md`](docs/turn-loop.md) | +| config-edit + approval state machine | [`docs/approvals.md`](docs/approvals.md) | +| what survives destroy / purge / restart | [`docs/persistence.md`](docs/persistence.md) | +| naming, wire protocol, commit style | [`docs/conventions.md`](docs/conventions.md) | +| nginx vhost map + sub-domain routing | [`docs/gateway.md`](docs/gateway.md) | +| NixOS / nspawn gotchas | [`docs/gotchas.md`](docs/gotchas.md) | ## Quick start @@ -96,7 +96,7 @@ Per-agent config lives in each agent's `agent.nix` (proposed, operator-approved, ### Multi-account Matrix support -`hyperhive.matrixAccounts` declares *additional* matrix accounts for an agent, beyond the hive-internal one. Each entry is keyed by account name and specifies: +`hyperhive.matrixAccounts` declares _additional_ matrix accounts for an agent, beyond the hive-internal one. Each entry is keyed by account name and specifies: - `tokenFile` โ€” path to the matrix bearer token (provisioned out-of-band) - `sessionDir` โ€” path to the per-account matrix-sdk sqlite state (crypto keys + cache) @@ -114,7 +114,7 @@ hyperhive.matrixAccounts = { }; ``` -The hive-internal account is always named `main` (synthesized from `hyperhive.matrix.url` + agent state). This option only declares *extras*; the `main` name is reserved and cannot be used here. Requires `hyperhive.matrix.enable = true`. +The hive-internal account is always named `main` (synthesized from `hyperhive.matrix.url` + agent state). This option only declares _extras_; the `main` name is reserved and cannot be used here. Requires `hyperhive.matrix.enable = true`. For more details see [`docs/matrix.md`](docs/matrix.md). diff --git a/docs/agent-hierarchy.md b/docs/agent-hierarchy.md index a8be30b5..a11ac9a6 100644 --- a/docs/agent-hierarchy.md +++ b/docs/agent-hierarchy.md @@ -12,9 +12,9 @@ Topology lives in the hive-c0re-owned **meta repo**, alongside ```json { - "ruth": null, + "ruth": null, "alice": null, - "bob": "alice" + "bob": "alice" } ``` @@ -34,6 +34,7 @@ operator-driven: All three converge on `topology::set_parent`, which delegates the validation rules to a pure `apply_set_parent` helper. Refuses: + - unknown `child` / `new_parent` (typo guard), - self-parenting, - cycles (32-hop ancestor walk, mirroring `is_descendant_of`). @@ -93,15 +94,15 @@ where system-level facts live. Once enforcement lands the rules collapse into: -| operation | who can do it | -|---|---| -| `kill` / `start` / `restart` / `update` (any descendant) | any ancestor | -| `request_init_config` (spawn a new child) | any agent, child added under self | -| `request_apply_commit` (any descendant's config) | any ancestor | -| `get_logs` (any descendant) | any ancestor | -| moderate questions / reminders (cancel any open thread of a descendant) | any ancestor | -| `send` / `recv` routing | parent โ†” same-parent siblings โ†” self โ†” descendants; explicit allow-list for anyone else | -| `request_update_meta_inputs` (bump meta lock) | root agents only (today: just `manager`) | +| operation | who can do it | +| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | +| `kill` / `start` / `restart` / `update` (any descendant) | any ancestor | +| `request_init_config` (spawn a new child) | any agent, child added under self | +| `request_apply_commit` (any descendant's config) | any ancestor | +| `get_logs` (any descendant) | any ancestor | +| moderate questions / reminders (cancel any open thread of a descendant) | any ancestor | +| `send` / `recv` routing | parent โ†” same-parent siblings โ†” self โ†” descendants; explicit allow-list for anyone else | +| `request_update_meta_inputs` (bump meta lock) | root agents only (today: just `manager`) | "Ancestor" walks `ContainerView.parent` chains; cycles are guarded by a visited-set at dispatch time (a malformed topology.json can't lock the @@ -129,14 +130,14 @@ The `ManagerRequest::*` variants in `hive-sh4re/src/lib.rs` are operations the manager flavour socket can make that sub-agent sockets can't: -| variant | semantic | post-milestone | -|---|---|---| -| `RequestInitConfig` | seed an agent's proposed config repo | **topology** โ€” existing direct child (re-init) or a brand-new name (child added under self on approval); a name owned by a different parent is refused | -| `RequestApplyCommit` | submit a commit sha for operator approval | **topology** โ€” descendants only | -| `Kill` / `Start` / `Restart` / `Update` | container lifecycle on an existing agent | **topology** โ€” descendants only | -| `RequestUpdateMetaInputs` | bump meta `flake.lock` | **per-agent cap** (root-only today; a future "let coder bump its own input" might grant it) | -| `GetLogs` | journalctl scrape of a sub-agent | **topology** โ€” descendants only | -| `Wake` | inject a `from: ` message into self's inbox | **not really privileged** โ€” the wire surface exists because daemon co-processes (e.g. `forge_notify`) need it. Sub-agents have the same via their own socket. | +| variant | semantic | post-milestone | +| --------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `RequestInitConfig` | seed an agent's proposed config repo | **topology** โ€” existing direct child (re-init) or a brand-new name (child added under self on approval); a name owned by a different parent is refused | +| `RequestApplyCommit` | submit a commit sha for operator approval | **topology** โ€” descendants only | +| `Kill` / `Start` / `Restart` / `Update` | container lifecycle on an existing agent | **topology** โ€” descendants only | +| `RequestUpdateMetaInputs` | bump meta `flake.lock` | **per-agent cap** (root-only today; a future "let coder bump its own input" might grant it) | +| `GetLogs` | journalctl scrape of a sub-agent | **topology** โ€” descendants only | +| `Wake` | inject a `from: ` message into self's inbox | **not really privileged** โ€” the wire surface exists because daemon co-processes (e.g. `forge_notify`) need it. Sub-agents have the same via their own socket. | ### C โ€” storage / mounts (`hive-c0re::lifecycle`) @@ -150,6 +151,7 @@ The manager container's nspawn bind set: manager can read the system-wide deploy log Tree-shape version: + - Each agent gets RW to `/agents//` for every descendant in its subtree. The root agent (today: manager) gets RW to the full forest as a special case of "the root has every other agent as a @@ -182,7 +184,6 @@ direct `$HYPERHIVE_STATE_DIR/` read. ### F โ€” drive-by checks across c0re - - `loose_ends.rs`: manager sees hive-wide loose-ends, sub-agents only their own. **Topology** โ€” every agent sees its own + its descendants'. diff --git a/docs/boundary.md b/docs/boundary.md index dc47a117..c1608f7c 100644 --- a/docs/boundary.md +++ b/docs/boundary.md @@ -1,12 +1,12 @@ # The operator/agent boundary Design rationale for hyperhive's two-principal trust model. The -*implementation* work โ€” container network isolation, the unifying +_implementation_ work โ€” container network isolation, the unifying gateway, core-daemon privsep โ€” is tracked as `area:ops` issues on the forge. Today "the operator surface" and "the agent surface" are a -*convention*, not a boundary โ€” nothing stops a container from +_convention_, not a boundary โ€” nothing stops a container from curling the core daemon on `localhost:`, or another agent's web UI. Network isolation, the gateway, and privsep together turn that convention into an enforced boundary. @@ -19,7 +19,7 @@ that convention into an enforced boundary. lifecycle POSTs) are served by the core daemon and only reachable via the gateway. - **Agent** โ€” speaks only for itself, only over its per-agent - unix socket. The socket's identity *is* the agent (see + unix socket. The socket's identity _is_ the agent (see `docs/conventions.md`, "identity = socket"). An agent must not be able to reach the core daemon's HTTP surface, another agent's socket, or another agent's web UI. @@ -30,7 +30,7 @@ that convention into an enforced boundary. point.** They live on the core backend. Worked example โ€” answering an operator-targeted question is a -`POST /answer-question/{id}` on the core dashboard, *never* an +`POST /answer-question/{id}` on the core dashboard, _never_ an `AgentRequest` variant. If it were a per-agent-socket request, an agent could `curl` its own socket and spoof an operator answer. The per-agent web UI POSTs cross-origin to the core for these @@ -44,7 +44,7 @@ container can reach `localhost:`, the dashboard, and every other agent's web port. Until that changes, the operator/agent split is on the honour system โ€” every boundary claim above is aspirational. Network isolation is what makes the -boundary *real*; the gateway and privsep are ergonomics and +boundary _real_; the gateway and privsep are ergonomics and defence-in-depth layered on top. The `area:ops` issues followed this sequencing: diff --git a/docs/ci.md b/docs/ci.md index 5c1f9d49..b3da8e13 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -7,6 +7,7 @@ The `hive-ci` module runs a Forgejo Actions runner in a `hive-ci` nixos-containe Set `services.hyperhive.forge.ci.enable = true` in the host NixOS config. That's it โ€” no manual token provisioning. **Requirements:** + - The internal forge is always present (mandatory), so the runner always has a hive-forge instance to register against โ€” nothing extra to enable. - Optional: tune `services.hyperhive.forge.ci.name` (runner name in forge admin panel), `concurrency` (parallel job capacity), `labels` (workflow targeting), `jobTimeout` (per-job wall-clock cap, default `"1h"`, Go duration string e.g. `"3h"` โ€” a job that exceeds it is killed so a hung or runaway build can't hold the runner's single slot indefinitely). @@ -148,7 +149,7 @@ following to your host config: ``` **Remote builders:** if CI dispatches builds to a remote builder (e.g. via -`nix.buildMachines` / `ssh-ng://`), the build outputs land in *that host's* +`nix.buildMachines` / `ssh-ng://`), the build outputs land in _that host's_ store, so the same GC config should be applied wherever the builder runs โ€” GC on the coordinator host won't reclaim space on the builder. diff --git a/docs/coordinator.md b/docs/coordinator.md index 7a90920f..b8ac35d6 100644 --- a/docs/coordinator.md +++ b/docs/coordinator.md @@ -32,17 +32,17 @@ somewhere." ### Queue kinds -| Kind | Description | -|------|-------------| -| `Rebuild` | Single-agent rebuild. Covers manual, approval-driven, auto-update, and meta-update cascade variants โ€” all funnel through the same path. The start-after-rebuild is **deferred to a fast-lane `Start` follow-up** (`parent_id` = this entry) so the build lane is freed as soon as the profile-swap finishes instead of waiting out the container boot โ€” see *Deferred start* under the rebuild path below. | -| `MetaUpdate` | `nix flake update` on the meta flake. The worker runs the lock bump itself, then enqueues a cascade of `Rebuild` entries with `parent_id` set to the meta-update's id. | -| `Spawn` | First-deploy of a new agent (approval-driven). Same serialisation as `Rebuild` from the operator's POV. | -| `Destroy` | For future use (`destroy --purge` does real I/O). Variant exists so the wire shape doesn't change later; not currently routed through the queue. | -| `Restart` | Stop + start a container without touching config (~5-10s). Routed through the queue so it serialises against in-flight rebuilds for the same agent โ€” prevents a restart racing a rebuild mid-flight. Sources: dashboard โ†บ button, the `restart` MCP tool. | -| `PermChange` | Write a tool-group or capability change to the shared JSON file (`tool-groups.json` / `capabilities.json`), then rebuild the agent so the updated `HIVE_TOOL_GROUPS` / `HIVE_CAPABILITIES` env var takes effect. Serialising the file write through the queue prevents concurrent dashboard batch-apply actions from racing on the shared file. After a successful file write, emits `CapabilitiesChanged` or `ToolGroupsChanged` SSE snapshot so the P3RM1SS10NS tab updates live. | +| Kind | Description | +| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `Rebuild` | Single-agent rebuild. Covers manual, approval-driven, auto-update, and meta-update cascade variants โ€” all funnel through the same path. The start-after-rebuild is **deferred to a fast-lane `Start` follow-up** (`parent_id` = this entry) so the build lane is freed as soon as the profile-swap finishes instead of waiting out the container boot โ€” see _Deferred start_ under the rebuild path below. | +| `MetaUpdate` | `nix flake update` on the meta flake. The worker runs the lock bump itself, then enqueues a cascade of `Rebuild` entries with `parent_id` set to the meta-update's id. | +| `Spawn` | First-deploy of a new agent (approval-driven). Same serialisation as `Rebuild` from the operator's POV. | +| `Destroy` | For future use (`destroy --purge` does real I/O). Variant exists so the wire shape doesn't change later; not currently routed through the queue. | +| `Restart` | Stop + start a container without touching config (~5-10s). Routed through the queue so it serialises against in-flight rebuilds for the same agent โ€” prevents a restart racing a rebuild mid-flight. Sources: dashboard โ†บ button, the `restart` MCP tool. | +| `PermChange` | Write a tool-group or capability change to the shared JSON file (`tool-groups.json` / `capabilities.json`), then rebuild the agent so the updated `HIVE_TOOL_GROUPS` / `HIVE_CAPABILITIES` env var takes effect. Serialising the file write through the queue prevents concurrent dashboard batch-apply actions from racing on the shared file. After a successful file write, emits `CapabilitiesChanged` or `ToolGroupsChanged` SSE snapshot so the P3RM1SS10NS tab updates live. | | `GracefulStop` | Quiesce then stop a container (the `?graceful=true` path on `/api/kill/`). Signals the harness (its next `Recv` returns `GracefulStop` โ€” the inbound fence โ€” so it runs a stop-checkpoint turn that flushes durable `/state`, then takes the normal post-turn compaction path if it crossed the watermark, then exits) and **immediately releases the build lane**, spawning a detached watcher that holds the `Stopping` transient across the drain (bounded by a 3-min timeout โ†’ hard-stop fallback) and then enqueues a fast-lane `Stop` (`parent_id` = this entry) for the actual `nixos-container stop`. Net: a whole-hive graceful stop signals every agent up front, drains overlap, and only the container teardowns serialise (on the fast lane). Queued so the signal can't race an in-flight rebuild for the same agent. | -**Intentionally not queued** (sub-second ops): the *hard* `start`, `stop`, `kill` via the direct API paths. (A *graceful* stop is the `GracefulStop` kind above โ€” it takes a checkpoint turn, so it rides the queue.) The queue's fast lane does carry `Start` / `Stop` kinds, but only as **follow-ups** other entries enqueue for themselves โ€” the graceful-stop teardown and the deferred start-after-rebuild โ€” so the container op groups under its parent entry on the dashboard. +**Intentionally not queued** (sub-second ops): the _hard_ `start`, `stop`, `kill` via the direct API paths. (A _graceful_ stop is the `GracefulStop` kind above โ€” it takes a checkpoint turn, so it rides the queue.) The queue's fast lane does carry `Start` / `Stop` kinds, but only as **follow-ups** other entries enqueue for themselves โ€” the graceful-stop teardown and the deferred start-after-rebuild โ€” so the container op groups under its parent entry on the dashboard. ### Dedup @@ -53,12 +53,12 @@ since the current run started). ### Sources -| Source | Meaning | -|--------|---------| -| `Manual` | Operator clicked rebuild / update-all / meta-update on the dashboard, or any other direct human action (CLI, an agent MCP tool). | -| `AutoUpdate` | Legacy startup-sweep source (flat, no parent). Replaced by `StartupSweep` for new boots. | -| `StartupSweep` | Child of a `StartupSweep` parent entry; boot-time per-agent rebuild with the sweep as the visual group header. | -| `Approval` | Triggered by an operator-approved `ApprovalKind::{Spawn, ApplyCommit}`. | +| Source | Meaning | +| -------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `Manual` | Operator clicked rebuild / update-all / meta-update on the dashboard, or any other direct human action (CLI, an agent MCP tool). | +| `AutoUpdate` | Legacy startup-sweep source (flat, no parent). Replaced by `StartupSweep` for new boots. | +| `StartupSweep` | Child of a `StartupSweep` parent entry; boot-time per-agent rebuild with the sweep as the visual group header. | +| `Approval` | Triggered by an operator-approved `ApprovalKind::{Spawn, ApplyCommit}`. | ### Cascade parent tracking @@ -83,6 +83,7 @@ in the queue as a terminal entry (`Done` / `Failed` / `Cancelled`) or no longer the queue at all (evicted by `trim_history`, which only evicts terminals). Use cases: + - Chain a `Rebuild` after an explicit prerequisite step without coupling them through the `parent_id` cascade mechanism. - Sequence a `PermChange` + `Rebuild` pair where the rebuild must not start until the @@ -243,10 +244,10 @@ agent container. rebuild, so changes take effect on the next lifecycle op without requiring a host rebuild. -| Option | Default | Description | -|--------|---------|-------------| -| `services.hyperhive.c0re.agentCpuQuota` | `"200%"` | CPU cap per agent, as a percentage of one core (`"200%"` = 2 cores). Raise if agents hit CPU limits during builds or heavy tool use. | -| `services.hyperhive.c0re.agentMemoryMax` | `"4G"` | Memory cap per agent. Raise for agents that run large nix builds or hold big in-memory data. | +| Option | Default | Description | +| ---------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| `services.hyperhive.c0re.agentCpuQuota` | `"200%"` | CPU cap per agent, as a percentage of one core (`"200%"` = 2 cores). Raise if agents hit CPU limits during builds or heavy tool use. | +| `services.hyperhive.c0re.agentMemoryMax` | `"4G"` | Memory cap per agent. Raise for agents that run large nix builds or hold big in-memory data. | For a hive-wide cap across all containers together, set `systemd.slices.machine.serviceConfig.CPUQuota` in your NixOS diff --git a/docs/forge.md b/docs/forge.md index ccf602cf..2e40ed48 100644 --- a/docs/forge.md +++ b/docs/forge.md @@ -14,16 +14,16 @@ Two scope sets live in `hive-c0re::forge`: **`TOKEN_SCOPES`** (per-agent tokens): -| Scope | Why | -|-------|-----| -| `write:repository` | Create, clone, push, delete repos; merge PRs. | -| `write:issue` | Open / comment / review issues **and** pull requests (Forgejo namespaces PR conversation under issues). | -| `write:user` | Edit own profile, create repos under own user. | -| `write:organization` | Create + manage orgs (lets agents share a forge namespace). | -| `read:user` | Token-owner endpoint used for self-identification at harness startup. | -| `write:misc` | Hooks, attachments, the rest of the long tail. | -| `read:notification` | Poll `GET /notifications` for unread events. | -| `write:notification` | Mark notifications read via `PATCH /notifications/threads/{id}`. | +| Scope | Why | +| -------------------- | ------------------------------------------------------------------------------------------------------- | +| `write:repository` | Create, clone, push, delete repos; merge PRs. | +| `write:issue` | Open / comment / review issues **and** pull requests (Forgejo namespaces PR conversation under issues). | +| `write:user` | Edit own profile, create repos under own user. | +| `write:organization` | Create + manage orgs (lets agents share a forge namespace). | +| `read:user` | Token-owner endpoint used for self-identification at harness startup. | +| `write:misc` | Hooks, attachments, the rest of the long tail. | +| `read:notification` | Poll `GET /notifications` for unread events. | +| `write:notification` | Mark notifications read via `PATCH /notifications/threads/{id}`. | **`CORE_TOKEN_SCOPES`** (hive-c0re's own `core` user): everything in `TOKEN_SCOPES` plus `read:admin` and `write:admin`. Site-admin @@ -102,7 +102,7 @@ resurfaces next tick. **Size bound:** the per-poll prune retains only ids present in the single `limit=UNREAD_FETCH_LIMIT` (50) fetch page, so the cursor never -exceeds that many entries โ€” it tracks the unread *window*, not the +exceeds that many entries โ€” it tracks the unread _window_, not the all-time notification count. The fetch limit and the bound are the same constant in `forge_notify.rs` (with a debug assertion), so a future pagination change grows the ceiling visibly rather than @@ -165,7 +165,7 @@ silently (mark-read without delivery): `user.login` matches `own_login`. - **Self-authored creations** (an agent opening its own PR/issue) โ€” the already-fetched subject payload's poster `user.login` matches - `own_login`. Only *creations* are dropped; a later state change on the + `own_login`. Only _creations_ are dropped; a later state change on the agent's own subject is driven by someone else and still surfaces. `own_login` is fetched once at startup via `GET /api/v1/user`. On @@ -208,13 +208,13 @@ lookahead. Five shapes, distinguished by the notification's classification: -| Trigger | Wrapper | -| --- | --- | -| Comment on issue / PR | `[comment on PR #N owner/repo] title\nurl: ...\n\nauthor: body\nassignee: ...` | -| Review submission | `[PR approved #N owner/repo] title\nurl: ...\n\nreviewer: body\nassignee: ...` | -| New issue / PR | `[new PR #N owner/repo] title\nurl: ...\n\n\nassignee: ...` | +| Trigger | Wrapper | +| ----------------------------------- | --------------------------------------------------------------------------------- | +| Comment on issue / PR | `[comment on PR #N owner/repo] title\nurl: ...\n\nauthor: body\nassignee: ...` | +| Review submission | `[PR approved #N owner/repo] title\nurl: ...\n\nreviewer: body\nassignee: ...` | +| New issue / PR | `[new PR #N owner/repo] title\nurl: ...\n\n\nassignee: ...` | | Later activity (open, not creation) | `[activity on PR #N owner/repo] title\nurl: ...\n\n\nassignee: ...` | -| State change | `[PR merged #N owner/repo] title\nurl: ...\nassignee: ...` | +| State change | `[PR merged #N owner/repo] title\nurl: ...\nassignee: ...` | Review labels come from the Forgejo `state` field: `APPROVED` โ†’ `approved`, `REQUEST_CHANGES` โ†’ `changes requested`, `COMMENT` โ†’ @@ -232,7 +232,7 @@ original open notification and miss the review (#1637). So the `open` state only earns the `new ` label when the notification's event time (`updated_at`) is within `NEW_ITEM_TOLERANCE_SECS` (120s) of the subject's `created_at`. Anything later is labeled `activity on ` -โ€” neutral and non-misleading, since we can't cheaply say *what* the +โ€” neutral and non-misleading, since we can't cheaply say _what_ the activity was without an extra reviews fetch. Missing/unparseable timestamps default to `new` (preserve prior behavior rather than mask a genuine new item). Timestamps are parsed by a small dependency-free diff --git a/docs/gotchas.md b/docs/gotchas.md index 71ed4694..23181fa3 100644 --- a/docs/gotchas.md +++ b/docs/gotchas.md @@ -201,7 +201,7 @@ Never use raw `curl` for forge access. Agent containers bind-mount the host's nix-daemon socket. nspawn containers don't get user-namespaces by default, so `nix build` -invocations *inside* the container can't set up the build sandbox +invocations _inside_ the container can't set up the build sandbox and fail outright if the host daemon's `nix.settings.sandbox-fallback` is `false` (nixpkgs default). `nix/templates/harness-base.nix` does `lib.mkForce true` so builds @@ -277,7 +277,7 @@ connects to the compositor at `127.0.0.1:`. ("Failed to connect to the bus" โ†’ binds `xdg_wm_base` then destroys it = invisible window even though CDP works). The fix is **not** to wrap each client in its own `dbus-run-session` (a private throwaway bus per - process โ€” that's a *separate* session, defeating the one-session + process โ€” that's a _separate_ session, defeating the one-session model); it's this one shared bus, whose address is exported as `DBUS_SESSION_BUS_ADDRESS=unix:path=/run/gui/bus` via `systemd.globalEnvironment` so weston, the harness and every GUI client diff --git a/docs/network.md b/docs/network.md index 164a3ea0..81ba505f 100644 --- a/docs/network.md +++ b/docs/network.md @@ -15,13 +15,13 @@ of shared netns. ## v1 vs v2 -| feature | v1 (this PR) | v2 (after netns isolation) | -|---|---|---| -| bridge interface | created on host, no slave NICs | per-agent veth pairs attach | -| dnsmasq binding | bridge IP (reachable via host loopback in shared netns) | bridge IP (reachable via veth in private netns) | -| agent container netns | shared host | private | -| agent `/etc/resolv.conf` | unchanged (host DNS) | `nameserver ` | -| `address` rules target | `` (works in both modes) | unchanged from v1 | +| feature | v1 (this PR) | v2 (after netns isolation) | +| ------------------------ | ------------------------------------------------------- | ----------------------------------------------- | +| bridge interface | created on host, no slave NICs | per-agent veth pairs attach | +| dnsmasq binding | bridge IP (reachable via host loopback in shared netns) | bridge IP (reachable via veth in private netns) | +| agent container netns | shared host | private | +| agent `/etc/resolv.conf` | unchanged (host DNS) | `nameserver ` | +| `address` rules target | `` (works in both modes) | unchanged from v1 | The `address` rules ship pointing at the bridge IP from v1 so the DNS contract is fixed before any container actually depends on it @@ -91,9 +91,10 @@ agent containers. ## Firewall posture `networking.firewall.interfaces..allowedUDPPorts = [ 53 ]` -+ `allowedTCPPorts = [ 53 ]` opens the resolver on the bridge -interface only. Other interfaces stay closed. The hive resolver -isn't an external-facing service. + +- `allowedTCPPorts = [ 53 ]` opens the resolver on the bridge + interface only. Other interfaces stay closed. The hive resolver + isn't an external-facing service. When `isolateContainers = true`, `allowedTCPPorts` is extended with `[ 80 443 ]` so isolated agents can reach nginx (gateway container, @@ -131,14 +132,14 @@ agent containers from shared host netns to private netns. Set only after ### What the nix side does when `isolateContainers = true` -| effect | mechanism | -|---|---| -| IP forwarding | `boot.kernel.sysctl."net.ipv4.ip_forward" = 1` | -| Internet NAT | `networking.nat { enable = true; internalInterfaces = [ bridgeName ]; }` โ€” MASQUERADE on packets leaving via any external NIC | -| Loopback DROP | `networking.firewall.extraInputRules` โ€” drops bridge-subnet โ†’ `127.0.0.0/8` traffic; defence-in-depth against routing table leaks | -| Gateway access | `networking.firewall.interfaces..allowedTCPPorts = [ 80 443 ]` โ€” lets isolated agents reach nginx on the host (shared netns) | -| Forge URL | `HIVE_FORGE_URL` flips from `http://127.0.0.1:3000` to `http://forge.` โ€” agents resolve via dnsmasq, nginx proxies to forgejo | -| c0re signal | `HIVE_NETWORK_ISOLATION=1`, `HIVE_NETWORK_BRIDGE`, `HIVE_NETWORK_SUBNET` in `systemd.services.hive-c0re.environment` | +| effect | mechanism | +| -------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| IP forwarding | `boot.kernel.sysctl."net.ipv4.ip_forward" = 1` | +| Internet NAT | `networking.nat { enable = true; internalInterfaces = [ bridgeName ]; }` โ€” MASQUERADE on packets leaving via any external NIC | +| Loopback DROP | `networking.firewall.extraInputRules` โ€” drops bridge-subnet โ†’ `127.0.0.0/8` traffic; defence-in-depth against routing table leaks | +| Gateway access | `networking.firewall.interfaces..allowedTCPPorts = [ 80 443 ]` โ€” lets isolated agents reach nginx on the host (shared netns) | +| Forge URL | `HIVE_FORGE_URL` flips from `http://127.0.0.1:3000` to `http://forge.` โ€” agents resolve via dnsmasq, nginx proxies to forgejo | +| c0re signal | `HIVE_NETWORK_ISOLATION=1`, `HIVE_NETWORK_BRIDGE`, `HIVE_NETWORK_SUBNET` in `systemd.services.hive-c0re.environment` | `HIVE_NETWORK_SUBNET` is the host-side bridge IP + prefix (e.g. `10.42.0.1/24`), **not** the canonical network address. The Rust side diff --git a/docs/security.md b/docs/security.md index a8b6144b..435a066a 100644 --- a/docs/security.md +++ b/docs/security.md @@ -15,10 +15,10 @@ An agent is **trusted code running inside its own nspawn container**. The boundary that matters is the container: a sub-agent cannot see the host netns, another agent's container, or another agent's state dir. Within its own container the agent is privileged โ€” it has **passwordless `sudo` by -default**. Isolating credentials *from the agent itself* is therefore **not a +default**. Isolating credentials _from the agent itself_ is therefore **not a goal**: an agent can read its own tokens, its own `/home//.claude`, and run arbitrary commands as root inside its container. (The narrow exception is -*cross-tenant* leakage โ€” e.g. the unsandboxed-nix-build `0600` token policy +_cross-tenant_ leakage โ€” e.g. the unsandboxed-nix-build `0600` token policy below stops a build's nixbld user reading the agent's own forge token, and the state-file endpoint stops one agent proxying another's files. Those harden the boundary; they do not sandbox the agent from itself.) @@ -36,7 +36,7 @@ Each agent gets its own scoped credentials, never shared: - **matrix token** โ†’ that agent's matrix account only. So a compromised/confused agent's reach on the forge or matrix is bounded by -*its own* account's scope, not the swarm's. This is the main thing standing +_its own_ account's scope, not the swarm's. This is the main thing standing between "one agent does something dumb" and "the whole hive is affected". ### Threat model: prompt injection โ†’ confused deputy @@ -44,7 +44,7 @@ between "one agent does something dumb" and "the whole hive is affected". The realistic adversary **never needs to breach the container**. They supply **untrusted input the agent reads and acts on**: a poisoned issue or PR comment, a cloned repo's README/CI, a scraped webpage, a crafted matrix -message. The agent is the trusted, capable party; the *input* is the +message. The agent is the trusted, capable party; the _input_ is the untrusted part. A successful injection turns the agent into a **confused deputy** โ€” it uses its legitimate capabilities (push, comment, deploy, run shell) on the attacker's behalf. @@ -52,7 +52,7 @@ shell) on the attacker's behalf. Mitigations are therefore about **bounding capability and inserting human checkpoints**, not about sandboxing the agent from its own tools: -- **Operator merges, not the agent** โ€” an agent may *push* branches, but a +- **Operator merges, not the agent** โ€” an agent may _push_ branches, but a **human (the operator) merges the PR**, keeping a person in the loop on the highest-value action. On the **internal forge this is technically enforced, not just convention**: agents can't create repos (`max_repo_creation = 0`), @@ -76,7 +76,7 @@ accepting**. The rule of thumb: If an agent can deploy to prod, you are accepting the risk of a dropped production database (via injection or plain error). If that's unacceptable, -the answer is *don't grant the capability* โ€” not "grant it and hope the +the answer is _don't grant the capability_ โ€” not "grant it and hope the sandbox holds", because there is no sandbox between an agent and the tools you handed it. @@ -101,7 +101,7 @@ before touching the filesystem: (canonical host form or the in-container view `/agents//state/`) - `/var/lib/hyperhive/shared/` โ€” shared docs (`/shared/` in-container) -`/state/...` without an agent prefix is explicitly *not* accepted โ€” it is +`/state/...` without an agent prefix is explicitly _not_ accepted โ€” it is ambiguous from the host's perspective. Defense-in-depth layers (in order): @@ -181,22 +181,22 @@ for every operation that genuinely requires root. **Narrow interface** โ€” `PrivRequest` variants map 1:1 to specific known operations; there is no arbitrary command pass-through: -| Operation | What it runs | -|-----------|-------------| -| `StartContainer` / `StopContainer` / `KillContainer` | `nixos-container start/stop/kill ` | -| `CreateContainer` / `UpdateContainer` | `nixos-container create/update --flake ` | -| `DestroyContainer` | `nixos-container destroy ` | -| `ListContainers` | `nixos-container list` | -| `ReadContainerJournal` | `journalctl -M -n [filters...]` | -| `ReloadGatewayNginx` | `systemctl -M hive-gateway reload/start/reset-failed nginx` | -| `WriteNspawnFlags` | write `/etc/nixos-containers/.conf` (bind-mount list + network isolation vars) | -| `WriteResourceLimits` | write `CPUQuota=`/`MemoryMax=` systemd drop-in for agent container | -| `RemoveServiceDropin` | remove `container@.service.d/` drop-in on destroy | -| `DaemonReload` | `systemctl daemon-reload` | -| `ChownSocketDir` / `ChmodSocketDir` | chown/chmod `/run/hive-agent//` socket directory | -| `RunForgeAdmin` | `nixos-container run hive-forge -- runuser -u forgejo -- forgejo admin ` | -| `WriteAgentForgeToken` / `WriteAgentMatrixToken` | write `0600` credential file into agent state dir | -| `RestartMatrixDaemon` | `systemctl --machine=h- restart hive-matrix-daemon.service` | +| Operation | What it runs | +| ---------------------------------------------------- | ----------------------------------------------------------------------------------------- | +| `StartContainer` / `StopContainer` / `KillContainer` | `nixos-container start/stop/kill ` | +| `CreateContainer` / `UpdateContainer` | `nixos-container create/update --flake ` | +| `DestroyContainer` | `nixos-container destroy ` | +| `ListContainers` | `nixos-container list` | +| `ReadContainerJournal` | `journalctl -M -n [filters...]` | +| `ReloadGatewayNginx` | `systemctl -M hive-gateway reload/start/reset-failed nginx` | +| `WriteNspawnFlags` | write `/etc/nixos-containers/.conf` (bind-mount list + network isolation vars) | +| `WriteResourceLimits` | write `CPUQuota=`/`MemoryMax=` systemd drop-in for agent container | +| `RemoveServiceDropin` | remove `container@.service.d/` drop-in on destroy | +| `DaemonReload` | `systemctl daemon-reload` | +| `ChownSocketDir` / `ChmodSocketDir` | chown/chmod `/run/hive-agent//` socket directory | +| `RunForgeAdmin` | `nixos-container run hive-forge -- runuser -u forgejo -- forgejo admin ` | +| `WriteAgentForgeToken` / `WriteAgentMatrixToken` | write `0600` credential file into agent state dir | +| `RestartMatrixDaemon` | `systemctl --machine=h- restart hive-matrix-daemon.service` | **Container allowlist** โ€” every request is validated against an allowlist before any operation: only names matching the agent-name @@ -211,8 +211,8 @@ writes to only the paths `hive-priv` legitimately needs. ### Privilege boundary summary -| Component | Runs as | Privilege needed for | -|-----------|---------|----------------------| -| `hive-c0re` | `hive-core` | broker, HTTP dashboard, scheduling, approvals | -| `hive-priv` | `root` | container lifecycle, journal reads, bind mounts, cred writes | -| `hive-ag3nt` (per-container) | per-agent user | turn execution, MCP serving | +| Component | Runs as | Privilege needed for | +| ---------------------------- | -------------- | ------------------------------------------------------------ | +| `hive-c0re` | `hive-core` | broker, HTTP dashboard, scheduling, approvals | +| `hive-priv` | `root` | container lifecycle, journal reads, bind mounts, cred writes | +| `hive-ag3nt` (per-container) | per-agent user | turn execution, MCP serving | diff --git a/docs/swarm.md b/docs/swarm.md index eb0d6b8d..47eb4b6a 100644 --- a/docs/swarm.md +++ b/docs/swarm.md @@ -48,12 +48,12 @@ services.hyperhive.swarm.peers = { The attrset key is the peer's DNS domain. Two independent, optional trust knobs โ€” pick by what you need to trust: -- **`certFingerprint`** (`"sha256:โ€ฆ"`) โ€” pin the peer's TLS *leaf* +- **`certFingerprint`** (`"sha256:โ€ฆ"`) โ€” pin the peer's TLS _leaf_ fingerprint. Scopes **only** to hive-c0re's own peer HTTPS checks (the P33RS dashboard links + agent peer discovery below). It is **not** consulted by matrix federation โ€” tuwunel validates a peer's federation certificate against the system CA bundle independently - (see *Matrix federation* below), so a fingerprint pin does nothing + (see _Matrix federation_ below), so a fingerprint pin does nothing for a self-signed matrix cert. - **`caCert`** (path to the peer's root CA PEM) โ€” embeds that CA (at build time, into the nix store โ€” no runtime file on the host) and @@ -61,7 +61,7 @@ trust knobs โ€” pick by what you need to trust: alongside `hive-ca.pem` in every agent's `security.pki.certificateFiles` (via the meta-flake renderer) **and** in the matrix container's trust bundle, so tuwunel validates the - peer's *federation* TLS when it chains to that CA. Trust stays + peer's _federation_ TLS when it chains to that CA. Trust stays **inside the hive** (agents + the matrix container), never the host system trust store. **This is the knob that unblocks federation with a self-signed peer hive** โ€” use it instead of `certFingerprint` when diff --git a/docs/tools/bash.md b/docs/tools/bash.md index 85c899a0..6f2134f3 100644 --- a/docs/tools/bash.md +++ b/docs/tools/bash.md @@ -15,17 +15,18 @@ Submit a shell command for background execution (runs via `bash`). Stdout and stderr stream to `harness/bash-tasks/.{out,err}`. When the task completes (or times out, or the process errors), the harness fires a wake with `from: "bash-task-"` and the exit code -+ last stdout lines in the body; handle it on a future turn. -- `timeout_secs` โ€” kill the task after N seconds and mark it +- last stdout lines in the body; handle it on a future turn. + +* `timeout_secs` โ€” kill the task after N seconds and mark it `timed_out`. Omit for no timeout (runs until natural exit). -- `wait_seconds` โ€” inline poll before returning (capped at 30). +* `wait_seconds` โ€” inline poll before returning (capped at 30). When the task finishes within the window the full status is returned immediately and no wake is fired; when the window expires the task keeps running and the normal `task started: id=` response is returned. **Defaults to 3** โ€” pass `wait_seconds: 0` to disable inline waiting and always get the immediate response. -- `name` โ€” optional caller-chosen task id. When set it replaces the +* `name` โ€” optional caller-chosen task id. When set it replaces the auto-generated hex id, so it surfaces in the wake `from` (`bash-task-`), in `status()` lookups, and in the loose-ends list โ€” a memorable label instead of an opaque id. A name diff --git a/docs/tools/forge.md b/docs/tools/forge.md index 6beb0840..605182da 100644 --- a/docs/tools/forge.md +++ b/docs/tools/forge.md @@ -102,16 +102,16 @@ hive-forge repo-labels --json # full label ob Several verbs read an issue/PR and look overlapping, but each has a distinct output shape โ€” pick by what you need: -| Need | Verb | Shape | -|------|------|-------| -| The full human view of one item โ€” title, body, **and** every comment in order | `view ` | prose dump; **marks the thread read** (clears the read-before-comment guard). Top-level `view` auto-detects issue-or-PR by number; scoped `issue view ` / `pr view ` do the same when you already know the kind | -| Just the structured metadata of one item (state, labels, assignees, head shaโ€ฆ) | `issue ` / `pr ` | JSON object | -| Only the comments (skip the body), e.g. to catch up a long thread; for PRs, review bodies (approve/request-changes/comment summaries) are merged in too | `comments ` [`--tail N`] | comment + review-body list; **marks read** | -| One specific comment by its id | `comment-show ` | single comment | -| The audit trail โ€” closes, label/assignee changes, commit refs, merges (no comment bodies) | `timeline ` | event list | -| The code change | `diff ` | unified diff | -| Is this PR ready to merge โ€” mergeable + CI + reviews | `pr-status --pr ` | health view; exit code = merge-readiness | -| The review verdicts + inline review comments | `pr-reviews ` | review list | +| Need | Verb | Shape | +| ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| The full human view of one item โ€” title, body, **and** every comment in order | `view ` | prose dump; **marks the thread read** (clears the read-before-comment guard). Top-level `view` auto-detects issue-or-PR by number; scoped `issue view ` / `pr view ` do the same when you already know the kind | +| Just the structured metadata of one item (state, labels, assignees, head shaโ€ฆ) | `issue ` / `pr ` | JSON object | +| Only the comments (skip the body), e.g. to catch up a long thread; for PRs, review bodies (approve/request-changes/comment summaries) are merged in too | `comments ` [`--tail N`] | comment + review-body list; **marks read** | +| One specific comment by its id | `comment-show ` | single comment | +| The audit trail โ€” closes, label/assignee changes, commit refs, merges (no comment bodies) | `timeline ` | event list | +| The code change | `diff ` | unified diff | +| Is this PR ready to merge โ€” mergeable + CI + reviews | `pr-status --pr ` | health view; exit code = merge-readiness | +| The review verdicts + inline review comments | `pr-reviews ` | review list | Rule of thumb: **`view` first** when you're about to act on a thread (it shows everything and clears the unread guard so your follow-up `comment` @@ -172,7 +172,7 @@ hive-forge pr-status --pr 42 # last comment: 2026-06-05T19:13:28+02:00 by argus ``` -Note: review verdicts come from *formal* Forgejo reviews (the +Note: review verdicts come from _formal_ Forgejo reviews (the approve / request-changes API). Reviewers who post their verdict as a plain comment show under `last comment`, not `reviews`. @@ -243,14 +243,14 @@ to discover valid label names before triaging or to audit the label set. increment it). `--json` wraps the output. - `ci-rerun` re-runs CI without pushing an empty commit (the old retrigger path, which littered PR history). Forgejo has no token-usable - REST endpoint to re-run an *existing* run (the run-page rerun buttons + REST endpoint to re-run an _existing_ run (the run-page rerun buttons are CSRF-gated web routes a token POST 404s), so this dispatches a **fresh** run of the workflow via the workflow-dispatch API (`POST โ€ฆ/actions/workflows//dispatches {"ref":""}`). Resolve the branch with exactly one of: `--pr ` (the PR's head branch), `--run ` (the same run number `ci-log` / `artifact-get` take โ€” resolves the branch + workflow from that run), or `--branch - ` (directly). `--workflow ` picks the workflow file for +` (directly). `--workflow ` picks the workflow file for `--pr` / `--branch` (default `ci.yml`). Dispatch re-runs the whole workflow โ€” there is no single-job variant. - Do NOT use raw `curl` for forge access -- the CLI handles auth, diff --git a/docs/tools/lifecycle.md b/docs/tools/lifecycle.md index c2135445..5ad65e21 100644 --- a/docs/tools/lifecycle.md +++ b/docs/tools/lifecycle.md @@ -76,13 +76,13 @@ agents after the approval resolves. ## Boundary summary -| Operation | Requires approval? | Scope | -|---|---|---| -| `kill` / `start` / `restart` / `update` | No | Direct children | -| `list_containers` | No | All descendants | -| `request_init_config` | Yes (InitConfig) | New direct child only | -| `request_apply_commit` | Yes (ApplyCommit) | Direct children | -| `request_update_meta_inputs` | Yes (MetaUpdate) | Meta flake (global) | +| Operation | Requires approval? | Scope | +| --------------------------------------- | ------------------ | --------------------- | +| `kill` / `start` / `restart` / `update` | No | Direct children | +| `list_containers` | No | All descendants | +| `request_init_config` | Yes (InitConfig) | New direct child only | +| `request_apply_commit` | Yes (ApplyCommit) | Direct children | +| `request_update_meta_inputs` | Yes (MetaUpdate) | Meta flake (global) | ## See also diff --git a/docs/web-ui/css-vars.md b/docs/web-ui/css-vars.md index ecbdb9e9..fa17eaaa 100644 --- a/docs/web-ui/css-vars.md +++ b/docs/web-ui/css-vars.md @@ -5,10 +5,10 @@ swap touches only the first: 1. **`colors.css`** โ€” the 16 `--base00`โ€ฆ`--base0F` base16 slots. **This is the entire theme swap contract.** A generator (e.g. one fed a stylix - base16 scheme, which is natively base00โ€“base0F) replaces *only this - file*. + base16 scheme, which is natively base00โ€“base0F) replaces _only this + file_. 2. **`theme.css`** โ€” the semantic layer: `--bg`, `--fg`, `--purple`, โ€ฆ - *derived* from the base16 slots (`--bg: var(--base00)` etc.). This is + _derived_ from the base16 slots (`--bg: var(--base00)` etc.). This is what the app references and it **never changes on a swap**. Both files live in `frontend/packages/shared/src/`. Per-page stylesheets @@ -34,68 +34,73 @@ Each semantic var derives from a base16 slot (or, for the three that have no clean slot, a `color-mix()` over base16 โ€” pixel-identical under the default palette). Default hexes shown are Catppuccin Mocha. -| Variable | Derives from | Default hex | Use | -|---|---|---|---| -| `--bg` | `base00` | `#1e1e2e` | page background | -| `--bg-elev` | `base01` | `#181825` | elevated surfaces: floating dropdowns, popovers | -| `--border` | `base02` | `#313244` | general borders, hover/active backgrounds | -| `--purple-dim` | `base03` | `#45475a` | subtle borders, terminal chrome, badge backgrounds | -| `--fg` | `base05` | `#cdd6f4` | primary text colour | -| `--red` | `base08` | `#f38ba8` | errors, fail state | -| `--amber` | `base09` | `#fab387` | warnings, pending / running state | -| `--yellow` | `base0A` | `#f9e2af` | flash messages, mild warnings | -| `--green` | `base0B` | `#a6e3a1` | success, ok state | -| `--cyan` | `base0C` | `#89dceb` | tool-use events, info accents | -| `--blue` | `base0D` | `#89b4fa` | links, interactive accent (distinct from cyan) | -| `--purple` | `base0E` | `#cba6f7` | accent โ€” active tabs, links, highlights | -| `--pink` | `base0F` | `#f5c2e7` | thinking events | -| `--crust` | `mix(base00 58%, #000)` | `#11111b` | terminal / code block background (below `--bg`) | -| `--muted` | `mix(base05 55.5%, base00)` | `#7f849c` | secondary / dimmed text | -| `--subtext0` | `mix(base05 77.7%, base00)` | `#a6adc8` | toolbar/status text; dimmer than `--fg`, lighter than `--muted` | +| Variable | Derives from | Default hex | Use | +| -------------- | --------------------------- | ----------- | --------------------------------------------------------------- | +| `--bg` | `base00` | `#1e1e2e` | page background | +| `--bg-elev` | `base01` | `#181825` | elevated surfaces: floating dropdowns, popovers | +| `--border` | `base02` | `#313244` | general borders, hover/active backgrounds | +| `--purple-dim` | `base03` | `#45475a` | subtle borders, terminal chrome, badge backgrounds | +| `--fg` | `base05` | `#cdd6f4` | primary text colour | +| `--red` | `base08` | `#f38ba8` | errors, fail state | +| `--amber` | `base09` | `#fab387` | warnings, pending / running state | +| `--yellow` | `base0A` | `#f9e2af` | flash messages, mild warnings | +| `--green` | `base0B` | `#a6e3a1` | success, ok state | +| `--cyan` | `base0C` | `#89dceb` | tool-use events, info accents | +| `--blue` | `base0D` | `#89b4fa` | links, interactive accent (distinct from cyan) | +| `--purple` | `base0E` | `#cba6f7` | accent โ€” active tabs, links, highlights | +| `--pink` | `base0F` | `#f5c2e7` | thinking events | +| `--crust` | `mix(base00 58%, #000)` | `#11111b` | terminal / code block background (below `--bg`) | +| `--muted` | `mix(base05 55.5%, base00)` | `#7f849c` | secondary / dimmed text | +| `--subtext0` | `mix(base05 77.7%, base00)` | `#a6adc8` | toolbar/status text; dimmer than `--fg`, lighter than `--muted` | ## Common mistakes The Catppuccin colour names do **not** map 1:1 to the variable names. Variables to avoid (undefined โ€” they will silently resolve to transparent / inherited): -| Wrong | Correct | -|---|---| -| `--text` | `--fg` | -| `--mauve` | `--purple` | -| `--surface0` | `--bg-elev` (float bg) or `--border` (border/hover) | -| `--surface1` | `--border` | -| `--surface2` | `--purple-dim` | -| `--overlay0`, `--overlay1` | `--muted` | -| `--base`, `--mantle` | `--bg`, `--bg-elev` | +| Wrong | Correct | +| -------------------------- | --------------------------------------------------- | +| `--text` | `--fg` | +| `--mauve` | `--purple` | +| `--surface0` | `--bg-elev` (float bg) or `--border` (border/hover) | +| `--surface1` | `--border` | +| `--surface2` | `--purple-dim` | +| `--overlay0`, `--overlay1` | `--muted` | +| `--base`, `--mantle` | `--bg`, `--bg-elev` | ## Usage guide **Floating menus and dropdowns** (e.g. agent context menu, tabbar overflow): + ```css background: var(--bg-elev); border: 1px solid var(--purple-dim); ``` **Hover / active state backgrounds**: + ```css background: var(--border); ``` **Active tab text / accent elements**: + ```css color: var(--purple); ``` **Muted / meta text**: + ```css color: var(--muted); ``` **Error / warning / success badges**: + ```css -color: var(--red); /* error */ -color: var(--amber); /* warning / running */ -color: var(--green); /* ok */ +color: var(--red); /* error */ +color: var(--amber); /* warning / running */ +color: var(--green); /* ok */ ``` ## Theme swapping โ€” the base16 contract @@ -104,31 +109,31 @@ color: var(--green); /* ok */ semantic names.** A theme generator (e.g. one reading a stylix base16 scheme) overrides only `colors.css`; the semantic layer in `theme.css` derives everything else, so the whole UI re-themes with nothing else to -template or regenerate. The base16 slot โ†’ semantic mapping is *internal* +template or regenerate. The base16 slot โ†’ semantic mapping is _internal_ to `theme.css` (the "Derives from" column above) โ€” a generator never needs to know our var names, and `theme.css` + the page bundles stay untouched. `colors.css` base16 slot defaults (Catppuccin Mocha): -| Slot | Default | Standard base16 role | Mapped to | -|---|---|---|---| -| `base00` | `#1e1e2e` | default bg | `--bg`, (darkened) `--crust` | -| `base01` | `#181825` | lighter bg | `--bg-elev` | -| `base02` | `#313244` | selection/surface | `--border` | -| `base03` | `#45475a` | comments/dim surface | `--purple-dim` | -| `base04` | `#585b70` | dark foreground | *(unused; kept for completeness)* | -| `base05` | `#cdd6f4` | default foreground | `--fg`, (blended) `--muted`/`--subtext0` | -| `base06` | `#f5e0dc` | light foreground | *(unused)* | -| `base07` | `#b4befe` | lightest | *(unused)* | -| `base08` | `#f38ba8` | red | `--red` | -| `base09` | `#fab387` | orange | `--amber` | -| `base0A` | `#f9e2af` | yellow | `--yellow` | -| `base0B` | `#a6e3a1` | green | `--green` | -| `base0C` | `#89dceb` | cyan | `--cyan` (our *sky*; Catppuccin's `base0C` is teal) | -| `base0D` | `#89b4fa` | blue | `--blue` | -| `base0E` | `#cba6f7` | magenta | `--purple` | -| `base0F` | `#f5c2e7` | extra accent | `--pink` | +| Slot | Default | Standard base16 role | Mapped to | +| -------- | --------- | -------------------- | --------------------------------------------------- | +| `base00` | `#1e1e2e` | default bg | `--bg`, (darkened) `--crust` | +| `base01` | `#181825` | lighter bg | `--bg-elev` | +| `base02` | `#313244` | selection/surface | `--border` | +| `base03` | `#45475a` | comments/dim surface | `--purple-dim` | +| `base04` | `#585b70` | dark foreground | _(unused; kept for completeness)_ | +| `base05` | `#cdd6f4` | default foreground | `--fg`, (blended) `--muted`/`--subtext0` | +| `base06` | `#f5e0dc` | light foreground | _(unused)_ | +| `base07` | `#b4befe` | lightest | _(unused)_ | +| `base08` | `#f38ba8` | red | `--red` | +| `base09` | `#fab387` | orange | `--amber` | +| `base0A` | `#f9e2af` | yellow | `--yellow` | +| `base0B` | `#a6e3a1` | green | `--green` | +| `base0C` | `#89dceb` | cyan | `--cyan` (our _sky_; Catppuccin's `base0C` is teal) | +| `base0D` | `#89b4fa` | blue | `--blue` | +| `base0E` | `#cba6f7` | magenta | `--purple` | +| `base0F` | `#f5c2e7` | extra accent | `--pink` | Notes for theme authors: diff --git a/docs/web-ui/shape.md b/docs/web-ui/shape.md index a38b9328..87fb8b15 100644 --- a/docs/web-ui/shape.md +++ b/docs/web-ui/shape.md @@ -3,7 +3,6 @@ > Part of [Web UI](../web-ui.md). See also: > [Dashboard layout](dashboard.md) ยท [Per-agent page](agent.md) - - `GET /` โ†’ `index.html` from the bundled frontend dist (see `frontend/`). Both binaries' routers declare their dynamic endpoints first and then `fallback_service(ServeDir::new(...))` @@ -169,8 +168,8 @@ subscribes to `streamUrl` (live tail). Both endpoints return `.no-anim` class so they don't stagger in like live events. The optional `streamFactory(url)` callback lets the dashboard hand the factory a `SharedWorker`-backed `EventSource` facade (so -multiple tabs share one upstream connection โ€” see *SSE -multiplexing* above); when omitted, the factory falls back to a +multiple tabs share one upstream connection โ€” see _SSE +multiplexing_ above); when omitted, the factory falls back to a plain `new EventSource(url)`. **`linkify` (text-node based).** Bare `http(s)://` URLs in row @@ -298,10 +297,10 @@ without its last path segment. That makes the page work under any prefix the agent ends up mounted at without rebuilding the dist. The cases that matter: -| served at | `api/state` resolves to | -|---|---| -| `/` (own port, today's shape) | `/api/state` | -| `/agent/iris/` (gateway-prefixed) | `/agent/iris/api/state` | +| served at | `api/state` resolves to | +| ------------------------------------------------ | ----------------------- | +| `/` (own port, today's shape) | `/api/state` | +| `/agent/iris/` (gateway-prefixed) | `/agent/iris/api/state` | | `/agent/iris/stats` (subpage, no trailing slash) | `/agent/iris/api/state` | The gateway upstream config strips the prefix before forwarding to @@ -316,4 +315,3 @@ the relative-path resolution stays correct: `static/app.js` from replaced), not `/stats/static/app.js`. Adding a trailing slash to those routes would break the resolution; either keep them slash-less or use `` injection at serve time. - diff --git a/hive-ag3nt/prompts/system.md b/hive-ag3nt/prompts/system.md index c0a849e4..7f0a19db 100644 --- a/hive-ag3nt/prompts/system.md +++ b/hive-ag3nt/prompts/system.md @@ -10,31 +10,31 @@ Tools (hyperhive surface): - `mcp__hyperhive__answer(id, answer)` โ€” answer a question that was routed to YOU. You'll see one in your inbox as a `question_asked { id, asker, question, options, multi }` system event when a peer or the operator calls `ask(to: "", ...)`. The answer surfaces in the asker's inbox as a `question_answered` event. Strict authorisation: you can only answer questions where you are the declared target. - `mcp__hyperhive__get_loose_ends(agent?)` โ€” list your loose ends: unanswered questions where you're asker (waiting on someone) or target (owing a reply), plus reminders you've scheduled that haven't fired. No args to list your own threads โ€” cheap server-side sweep useful at turn start. Pass `agent: ""` to inspect a peer agent's threads. Direct child agents are always accessible. For non-children, the `query_agent_state` capability is required โ€” without it the request is rejected with an error. - `mcp__hyperhive__cancel_loose_end(kind, id)` โ€” cancel one of your own open threads. `kind` is `"question"` (the asker โ€” you, in this case โ€” gets a `[cancelled by ]` answer so the waiter unblocks), `"reminder"` (hard-deleted before it fires), or `"approval"` (withdraws a pending approval you submitted that got superseded โ€” root agent only; the server rejects this kind for all other callers). `id` from the matching `get_loose_ends` row or the original submission reply. -- `mcp__hyperhive__remind(message, delay_seconds? | at_unix_timestamp?, file_path?)` โ€” schedule a message to land in your *own* inbox at a future time (sender shows as `reminder`). Set exactly one of `delay_seconds` (relative) or `at_unix_timestamp` (absolute). Use for self-paced follow-ups instead of blocking a whole turn on a long `recv` wait. A large `message` auto-spills to a file under `/agents/{label}/state/reminders/`; pass `file_path` to point at one yourself. Each agent's pending-reminder count is capped (default 50) โ€” the tool will error if the cap is already reached. +- `mcp__hyperhive__remind(message, delay_seconds? | at_unix_timestamp?, file_path?)` โ€” schedule a message to land in your _own_ inbox at a future time (sender shows as `reminder`). Set exactly one of `delay_seconds` (relative) or `at_unix_timestamp` (absolute). Use for self-paced follow-ups instead of blocking a whole turn on a long `recv` wait. A large `message` auto-spills to a file under `/agents/{label}/state/reminders/`; pass `file_path` to point at one yourself. Each agent's pending-reminder count is capped (default 50) โ€” the tool will error if the cap is already reached. - `mcp__hyperhive__set_status(text)` โ€” set a free-text status visible on the operator dashboard. **Call this at the start of every task** to say what you're working on (e.g. `"processing matrix messages"`, `"fixing #319 model priority"`, `"idle"`). Single line, โ‰ค200 chars โ€” the dashboard renders this as a short chip, so longer multi-line text is rejected. Pass an empty string to clear. Persists across harness restarts. - `mcp__hyperhive__get_agent_meta(name?)` โ€” fetch identity + status metadata for an agent: canonical `name`, current `hyperhive_rev`, plus self-reported `status` text (set via `set_status`) and how long ago it was set. Also returns `running: bool` (whether the container is up โ€” when `false`, `status_text`/`status_set_at` are stale pre-stop values) and the hive + swarm display names (`hive_name`, `swarm_name`) when the operator has configured `services.hyperhive.{hiveName, swarmName}`; both lines omitted when unset. Pass `name` to query a peer (e.g. check whether iris is idle before pinging them). Omit `name` to get your own trustworthy identity stamp โ€” useful for state files, commit messages, cross-agent attribution that won't drift across renames or session-continue boundaries where the system-prompt label could be stale. - `mcp__hyperhive__request_next_turn()` โ€” ask the harness to start another turn immediately after this one ends, even if the inbox is empty. Use for multi-turn tasks (long builds, sequential steps) where you want to continue without waiting for an external message. The next turn starts with `from: "self"` and `body: "continue"`. No-op if new inbox messages arrive before this turn ends (the harness already loops immediately on pending messages). No args. -- `mcp__hyperhive__restart(name)` โ€” *(requires `lifecycle` tool group)* restart a direct child sub-agent (stop + start). The server enforces topology: the call is rejected unless `name` is a direct child of yours per `topology.json`. No approval required. -- `mcp__hyperhive__kill(name)` โ€” *(requires `lifecycle` tool group)* stop a direct child sub-agent (graceful). Direct children only โ€” server enforces topology. State dir kept; recreating reuses prior config + credentials. No approval required. -- `mcp__hyperhive__start(name)` โ€” *(requires `lifecycle` tool group)* start a stopped direct child sub-agent. Direct children only โ€” server enforces topology. No approval required. -- `mcp__hyperhive__update(name)` โ€” *(requires `lifecycle` tool group)* rebuild a direct child sub-agent: re-applies the current hyperhive flake + agent.nix and restarts it. Direct children only โ€” server enforces topology. No approval required. Idempotent. -- `mcp__hyperhive__list_containers()` โ€” *(requires `lifecycle` tool group)* list all containers that are topological descendants of this agent (children + their subtrees). Returns each name with running/stopped status, ordered parents-first. Useful before kill/update/restart to check what's under you. -- `mcp__hyperhive__request_init_config(name, description?)` โ€” *(requires `approvals` tool group)* initialise a brand-new direct child agent's proposed config repo. Queues an `InitConfig` approval; on approval hive-c0re seeds `/agents//config/agent.nix`. `name` must be a direct child in the topology tree โ€” server enforces. Fails if the config repo already exists (use `request_apply_commit` instead). -- `mcp__hyperhive__request_apply_commit(agent, commit_ref, description?)` โ€” *(requires `approvals` tool group)* submit a config commit for a direct child agent, queued for operator approval. `agent` must be a direct child in the topology tree โ€” server enforces. `commit_ref` must be a 7-40 char hex sha (not a branch/tag name). On approval hive-c0re rebuilds the container with the pinned commit. -- `mcp__hyperhive__request_update_meta_inputs(inputs?, description?)` โ€” *(requires `approvals` tool group)* queue an approval for the operator to run `nix flake update [inputs...]` on the meta flake. Pass specific input names (e.g. `["bitburner-agent"]`) or omit / pass `[]` for all inputs. Returns immediately; lock update runs on operator approval. Does NOT trigger rebuilds โ€” call `update(name)` on affected agents after approval resolves. -- `mcp__hyperhive__request_schedule_prompt(targets, body, first_fire_at_unix, interval_seconds?, description?)` โ€” *(requires `scheduling` tool group)* queue an approval for the operator to add a scheduled prompt. On approve hive-c0re inserts a schedule row and the worker fans `body` out to each agent in `targets` at `first_fire_at_unix` (recurring every `interval_seconds` if set, one-shot when absent). Catch-up clamp: long downtime fires ONCE per recurring row on resume. -- `mcp__hyperhive__cancel_schedule(id, targets?)` โ€” *(requires `scheduling` tool group)* cancel a schedule. Omit `targets` / pass empty to cancel the whole schedule; pass a list to cancel just those recipients. Authorization: you can cancel schedules you own OR any owned by a sub-agent in your subtree. -- `mcp__hyperhive__fire_schedule_now(id)` โ€” *(requires `scheduling` tool group)* fire a scheduled prompt out of band immediately. Recurring schedules keep their cadence intact; one-shot schedules are consumed. Same authorization as `cancel_schedule`. -- `mcp__hyperhive__edit_schedule(id, body?, description?, interval_seconds?, next_fire_at_unix?, targets_add?, targets_remove?)` โ€” *(requires `scheduling` tool group)* partial-update a schedule's mutable fields. Pass only the fields you want to change. `targets_add` / `targets_remove` mutate the recipient list in the same transaction. Refuses cancelled rows. Same authorization as `cancel_schedule`. -- `mcp__hyperhive__list_schedules()` โ€” *(requires `scheduling` tool group)* snapshot every schedule in the queue. Returns id, owner, body, target set with per-target `last_fired_at` + `last_result`, `next_fire_at_unix`, recurring `interval_seconds`. -- `mcp__hyperhive__get_logs(agent, lines?)` โ€” *(requires `diagnostics` tool group)* fetch recent journal lines for a sub-agent container. Pass the plain logical agent name; `lines` defaults to 50 (capped at 500). -- `mcp__hyperhive__get_host_journal(unit?, container?, lines?, priority?, grep?, since?, until?)` โ€” *(requires `read_host_journal` capability)* fetch recent lines from the host journal. All filters optional โ€” omit to get the last N host journal lines. `unit`: filter to a systemd unit (e.g. `hive-c0re.service`). `container`: nspawn machine name verbatim (e.g. `h-iris`). Omit for host journal. Agent containers use the `h-` prefix (e.g. `h-iris`); infrastructure containers use their full name (e.g. `hive-ci`, `hive-forge`, `hive-matrix`, `hive-gateway`). `lines`: how many lines (default 30, max 100). `priority`: minimum syslog level enum. `grep`: regex matched against log message fields (journalctl --grep). `since`: show entries on or newer than this (e.g. `-1h`, `2024-01-01 12:00:00`). `until`: show entries on or older than this. +- `mcp__hyperhive__restart(name)` โ€” _(requires `lifecycle` tool group)_ restart a direct child sub-agent (stop + start). The server enforces topology: the call is rejected unless `name` is a direct child of yours per `topology.json`. No approval required. +- `mcp__hyperhive__kill(name)` โ€” _(requires `lifecycle` tool group)_ stop a direct child sub-agent (graceful). Direct children only โ€” server enforces topology. State dir kept; recreating reuses prior config + credentials. No approval required. +- `mcp__hyperhive__start(name)` โ€” _(requires `lifecycle` tool group)_ start a stopped direct child sub-agent. Direct children only โ€” server enforces topology. No approval required. +- `mcp__hyperhive__update(name)` โ€” _(requires `lifecycle` tool group)_ rebuild a direct child sub-agent: re-applies the current hyperhive flake + agent.nix and restarts it. Direct children only โ€” server enforces topology. No approval required. Idempotent. +- `mcp__hyperhive__list_containers()` โ€” _(requires `lifecycle` tool group)_ list all containers that are topological descendants of this agent (children + their subtrees). Returns each name with running/stopped status, ordered parents-first. Useful before kill/update/restart to check what's under you. +- `mcp__hyperhive__request_init_config(name, description?)` โ€” _(requires `approvals` tool group)_ initialise a brand-new direct child agent's proposed config repo. Queues an `InitConfig` approval; on approval hive-c0re seeds `/agents//config/agent.nix`. `name` must be a direct child in the topology tree โ€” server enforces. Fails if the config repo already exists (use `request_apply_commit` instead). +- `mcp__hyperhive__request_apply_commit(agent, commit_ref, description?)` โ€” _(requires `approvals` tool group)_ submit a config commit for a direct child agent, queued for operator approval. `agent` must be a direct child in the topology tree โ€” server enforces. `commit_ref` must be a 7-40 char hex sha (not a branch/tag name). On approval hive-c0re rebuilds the container with the pinned commit. +- `mcp__hyperhive__request_update_meta_inputs(inputs?, description?)` โ€” _(requires `approvals` tool group)_ queue an approval for the operator to run `nix flake update [inputs...]` on the meta flake. Pass specific input names (e.g. `["bitburner-agent"]`) or omit / pass `[]` for all inputs. Returns immediately; lock update runs on operator approval. Does NOT trigger rebuilds โ€” call `update(name)` on affected agents after approval resolves. +- `mcp__hyperhive__request_schedule_prompt(targets, body, first_fire_at_unix, interval_seconds?, description?)` โ€” _(requires `scheduling` tool group)_ queue an approval for the operator to add a scheduled prompt. On approve hive-c0re inserts a schedule row and the worker fans `body` out to each agent in `targets` at `first_fire_at_unix` (recurring every `interval_seconds` if set, one-shot when absent). Catch-up clamp: long downtime fires ONCE per recurring row on resume. +- `mcp__hyperhive__cancel_schedule(id, targets?)` โ€” _(requires `scheduling` tool group)_ cancel a schedule. Omit `targets` / pass empty to cancel the whole schedule; pass a list to cancel just those recipients. Authorization: you can cancel schedules you own OR any owned by a sub-agent in your subtree. +- `mcp__hyperhive__fire_schedule_now(id)` โ€” _(requires `scheduling` tool group)_ fire a scheduled prompt out of band immediately. Recurring schedules keep their cadence intact; one-shot schedules are consumed. Same authorization as `cancel_schedule`. +- `mcp__hyperhive__edit_schedule(id, body?, description?, interval_seconds?, next_fire_at_unix?, targets_add?, targets_remove?)` โ€” _(requires `scheduling` tool group)_ partial-update a schedule's mutable fields. Pass only the fields you want to change. `targets_add` / `targets_remove` mutate the recipient list in the same transaction. Refuses cancelled rows. Same authorization as `cancel_schedule`. +- `mcp__hyperhive__list_schedules()` โ€” _(requires `scheduling` tool group)_ snapshot every schedule in the queue. Returns id, owner, body, target set with per-target `last_fired_at` + `last_result`, `next_fire_at_unix`, recurring `interval_seconds`. +- `mcp__hyperhive__get_logs(agent, lines?)` โ€” _(requires `diagnostics` tool group)_ fetch recent journal lines for a sub-agent container. Pass the plain logical agent name; `lines` defaults to 50 (capped at 500). +- `mcp__hyperhive__get_host_journal(unit?, container?, lines?, priority?, grep?, since?, until?)` โ€” _(requires `read_host_journal` capability)_ fetch recent lines from the host journal. All filters optional โ€” omit to get the last N host journal lines. `unit`: filter to a systemd unit (e.g. `hive-c0re.service`). `container`: nspawn machine name verbatim (e.g. `h-iris`). Omit for host journal. Agent containers use the `h-` prefix (e.g. `h-iris`); infrastructure containers use their full name (e.g. `hive-ci`, `hive-forge`, `hive-matrix`, `hive-gateway`). `lines`: how many lines (default 30, max 100). `priority`: minimum syslog level enum. `grep`: regex matched against log message fields (journalctl --grep). `since`: show entries on or newer than this (e.g. `-1h`, `2024-01-01 12:00:00`). `until`: show entries on or older than this. Need new packages, env vars, or other NixOS config for yourself? You can't edit your own config directly โ€” ask a peer agent with the `approvals` tool group, or contact the operator directly. Config repos live at `/agents/{label}/config/` (read-only inside your container). All changes flow through operator-approved commits. Your config repo is mounted **read-only** at `/agents/{label}/config/` โ€” `agent.nix` plus whatever extra files define you (declared packages, env vars, MCP servers). Read it to see exactly what defines you before asking for a change, so you can point at the precise file and line. -Approval boundary: lifecycle ops on *existing* direct children (`kill`, `start`, `restart`) are at your discretion โ€” no operator approval needed (requires `lifecycle` tool group). *Creating* a new agent (two-step: `request_init_config` + `request_apply_commit`) and *changing* any agent's config (`request_apply_commit`) both go through the approval queue (requires `approvals` tool group). The operator only signs off on changes; you run the day-to-day. +Approval boundary: lifecycle ops on _existing_ direct children (`kill`, `start`, `restart`) are at your discretion โ€” no operator approval needed (requires `lifecycle` tool group). _Creating_ a new agent (two-step: `request_init_config` + `request_apply_commit`) and _changing_ any agent's config (`request_apply_commit`) both go through the approval queue (requires `approvals` tool group). The operator only signs off on changes; you run the day-to-day. Messages from sender `system` are hyperhive helper events (JSON body, `event` field discriminates): `approval_resolved`, `config_ready`, `spawned`, `rebuilt`, `killed`, `destroyed`, `container_crash`, `needs_login`, `logged_in`, `needs_update`, `question_asked`, `question_answered`. Use these to react to lifecycle changes: @@ -63,6 +63,6 @@ Keep messages short โ€” a few sentences each. For anything big (file listings, l When your inbox has a message, handle it and stop. Don't narrate intent โ€” act. -**Turns are your checkpoint.** The harness runs one claude turn per inbox message; when you stop, it acknowledges that message and your `--continue` session is saved to disk. Ending the turn is how you commit progress โ€” both the session and the inbox acknowledgement. If the container restarts *while a turn is still running*, the message that drove it was never acknowledged, so it gets redelivered on the next boot, prefixed `[redelivered after harness restart โ€” may already be handled]`. A long single turn that does step after step widens the window where a restart loses work and forces that redelivery, so prefer short turns: do a unit of work, write anything durable under `/agents/{label}/state/`, and end. +**Turns are your checkpoint.** The harness runs one claude turn per inbox message; when you stop, it acknowledges that message and your `--continue` session is saved to disk. Ending the turn is how you commit progress โ€” both the session and the inbox acknowledgement. If the container restarts _while a turn is still running_, the message that drove it was never acknowledged, so it gets redelivered on the next boot, prefixed `[redelivered after harness restart โ€” may already be handled]`. A long single turn that does step after step widens the window where a restart loses work and forces that redelivery, so prefer short turns: do a unit of work, write anything durable under `/agents/{label}/state/`, and end. -**To keep working without waiting for a new message, call `request_next_turn()`** before you stop. The harness immediately starts a fresh turn with `from: "self"`, `body: "continue"` โ€” the supported way to run multi-step work (long builds, sequential edits) as a series of checkpointed turns rather than one monolithic turn. Don't busy-wait inside a turn for a condition to resolve: end the turn and let the next wake drive the continuation โ€” a `remind` you scheduled, an external event, a backgrounded bash task's completion, or `request_next_turn()`. (A long-poll `recv(wait_seconds: โ€ฆ)` blocks *within* the current turn โ€” it parks for new inbox messages but does not end the turn or checkpoint, so it isn't a substitute for ending the turn.) +**To keep working without waiting for a new message, call `request_next_turn()`** before you stop. The harness immediately starts a fresh turn with `from: "self"`, `body: "continue"` โ€” the supported way to run multi-step work (long builds, sequential edits) as a series of checkpointed turns rather than one monolithic turn. Don't busy-wait inside a turn for a condition to resolve: end the turn and let the next wake drive the continuation โ€” a `remind` you scheduled, an external event, a backgrounded bash task's completion, or `request_next_turn()`. (A long-poll `recv(wait_seconds: โ€ฆ)` blocks _within_ the current turn โ€” it parks for new inbox messages but does not end the turn or checkpoint, so it isn't a substitute for ending the turn.)