Second split under docs/swarm/, following the shape docs/turn-loop/ and
docs/web-ui/ already use.
The README keeps the `## Swarm CA` heading rather than deleting it: an
existing cross-reference targets that anchor, and a heading is the
cheapest thing to preserve. It now carries the one-paragraph summary and
a pointer, so a reader who lands on the anchor still arrives somewhere
that answers the question.
The CA page also absorbs three things that were true but undocumented —
why the trust bundle rather than the bare intermediate (openssl will not
terminate a chain at a non-self-signed anchor without -partial_chain),
that autoConfigure derives from enableAllLocalDefaults, and that the
name constraint excludes both IP families because a permitted-DNS-only
constraint says nothing about IP SANs.
Both worked from the flat docs/swarm.md and resolve one directory
short from docs/swarm/README.md: snapshot-store.md stayed in docs/,
and swarm-controller/README.md is at the repo root.
`docs/swarm.md` becomes `docs/swarm/README.md` and the shared-services
material moves to `docs/swarm/services.md`, following the shape
`docs/turn-loop/` and `docs/web-ui/` already use. The README keeps a
pointer so the reading path is unbroken.
Every referrer moved with it — five docs pages, two option descriptions
in swarm.nix, and CLAUDE.md's reading path. A pointer to a file that
moved is worse than one to a file that was deleted: the content still
exists, so the reader concludes the note is wrong rather than the path.
Both the option description and docs/swarm.md listed the forge among the
services whose `enable` defaults from `swarm.enableRequiredServices`.
It has no `enable`: it is the canonical store for the meta flake and
every agent's config repo, so it deploys with hyperhive itself.
The distinction is worth stating rather than deleting — a reader who
sees matrix and authelia listed and the forge absent would reasonably
wonder whether the forge is per-hive. It isn't; it's mandatory.
Names the one toggle and what derives from it, and the authelia shape:
container where the swarm's services live, client pointer everywhere,
users database written by swarm-controller rather than by hand.
A swarm's services and its hives can live on different hosts, and a host
has no way to tell whether it is the one holding the root — so setting
the swarm CA up is an operator action, not something a host infers.
`swarm.ca.autoConfigure` is off by default and is the only thing that
issues a hive sub-CA, which is also the only case that *can*: signing
one needs the root's private key, and that key is deliberately not on a
hive host otherwise.
With the flag off, a hive self-signs its CA exactly as it always has.
That keeps a plain hive working out of the box; what it gives up is
membership of a swarm's trust hierarchy, which is the right thing to
give up for a hive nobody has federated.
Replaces a default derived from `swarm.peers == { }`. That read "no
peers declared" as "everything is local", which is not the same claim —
a hive can belong to a swarm it has not declared yet, or to one whose
services run elsewhere entirely.
Cross-hive trust was O(n²) hand-pinning: every hive had to name every
peer's CA. A swarm root makes it O(1) — trust the root once and every
present and future peer validates.
The root is generated by a new `swarm-ca` unit on a single-host swarm
and operator-provided otherwise; `swarm.ca.autoConfigure` picks between
them and derives its default from `swarm.peers` being empty, so "all on
one host" is read off the deployment rather than remembered. Both modes
produce the same artifacts in the same places, so splitting hosts later
is moving the service dirs, not switching code paths. The root key never
enters the nix store, and the root is never regenerated automatically —
replacing it invalidates every peer at once.
Each hive CA carries `nameConstraints` pinned to that hive's domain, so
a leaked hive CA can only mint names inside its own subdomain, enforced
by verifiers rather than by convention.
`ca.pem` was serving as both the issuer and the anchor consumers trust;
those are the same file only while it is self-signed. openssl will not
terminate a chain at a trusted cert that isn't self-signed (rustls and
Go will), so the promotion would have broken some consumers and not
others. `hive-tls-ca` now also writes `trust-bundle.pem` — the hive CA
plus whatever it is rooted at — and every anchor consumer reads that:
agents, the CI and forge containers, and the peer-config recipe. On a
hive with no swarm root the bundle is just that CA, so nothing consuming
it needs a mode to branch on.
Neither has a fallback worth having: a guessed swarm domain is a wrong
hostname that evaluates cleanly and deploys, which is worse than an
eval failure naming the one line an operator has to write. Upgrading
past this sets both, once.
Requiring them also makes the hive domain fully derived rather than
merely derivable — `<hiveName>.<swarm.domain>` now always resolves, so
an operator writes the swarm's address and this hive's label and never
writes the hive domain at all.
`hiveName` stops being display-only in the process: it is the leftmost
label of the domain the hive is addressed by, which the option text and
docs now say.
Each of the three required options asserts separately, so a missing one
names itself. A missing `swarm.domain` legitimately fails two of them —
its own, and `domain`, which can no longer derive — and nix reports all
failing assertions together, so the operator sees the whole set rather
than one at a time.
The required-domain assertion in hive-network.nix could not be reached:
`forge.<domain>` and `matrix.<domain>` are evaluated while the
assertion list is, so an unset domain threw `cannot coerce null to a
string` naming one of those options instead of printing the message
that says which option to set.
Both defaults now fall back to a name under the reserved `.invalid`
TLD, which the assertion refuses to let out the door.
Every hive in a swarm occupies its own sub-domain of the swarm's, so
the hive domain is derivable rather than something each hive restates.
`services.hyperhive.swarm.domain` is new and nullable; the hive's own
`domain` keeps its existing required-ness and its existing assertion,
and gains a default of `<hiveName>.<swarm.domain>`.
Deliberately a default and not a rename: an alias would reinterpret the
domains hives have already deployed, while a default only fills in the
ones that never set one. Same reason there is no new assertion — the
existing message just names the derivation, so this adds a way to stop
failing rather than a way to fail.
Per review: crate READMEs will be served on the docs page, so the same
technical content in docs/swarm.md and the repo map is redundancy, not
thoroughness.
docs/swarm.md keeps only what is operator-facing and specific to it --
the option, and why enable is not derived from services.hyperhive.enable
-- and points at the README for the socket-directory constraint. The repo
map keeps the one-line warning and the pointer, not the argument.
This is the same correction as the AgentWindow comments: I had written
the socket rationale into five places and called it coverage. Correcting
every copy is what preserves the cause.
The crate was a workspace member with no entry in CLAUDE.md, which is the
index that auto-loads into every turn -- a member missing from it is
invisible to everyone who comes after.
Both entries carry the socket-directory constraint rather than just
naming the daemon, because that is the one thing about this service a
reader can get wrong from a position that looks correct.
Every prose and error-message reference to the moved options, including
two in hive-c0re/src/matrix.rs that omitted the services. prefix and so
read as the per-agent option. An error that names an option the operator
cannot find is worse than no hint.
Two sites spelled it as a brace group, services.hyperhive.{hiveName,
swarmName}, which no anchored rewrite can handle correctly now that only
one of the two moves; both are written out separately. One of them is an
MCP tool description, so it is rendered into every agent's system prompt.
Seven places described an authoring flow that does not exist: the
manager editing and committing a child's config in the bind-mounted
config dir. mara, on the snapshot issue: "the pr workflow is the main
one, anything else is legacy … the config dir in agent container is
read only copy to see own config … changes need to go through local
clone and via forge".
Two of the seven are the request_init_config MCP tool description and
its args struct, which are rendered into the system prompt of every
agent holding the approvals tool group. A wrong tool description is not
a stale comment; it is an instruction the whole hive reads as fact,
which is why the claim kept being repeated back.
The init tool creates the repo and seeds it. Nothing else. Tailoring
the seeded template is not a separate mechanism - it is the ordinary
config-change flow, a PR reviewed like any other.
Prose only; the code already implements the PR flow (an approval's
commit_ref is a PR number). Where a doc justified the parent's
read-write mount on the child's config dir with the authoring model,
the justification is removed rather than replaced: that mount is a
defect tracked on its own issue, and it should not read as intentional
while it waits.
21 of 28 non-test call sites now insert directly. power.rs compiles.
The only remaining errors are server.rs's 5, which are blocked: those
sites feed the returned id into HostResponse::queued -> `queued_dags`,
a wire field hivectl polls via QueueDag. Removing the container without
answering that breaks hivectl's wait/progress loop; asked on the issue.
Also swept the deleted symbol out of prose, not just code:
- docs/coordinator.md: "the submit layer (job_queue/submit.rs)" ->
the power layer (job_queue/power.rs), and "submits" -> "inserts".
- templates.rs module doc: points at super::power for the power ops.
- lifecycle_ops.rs module doc: says which path each op takes now.
- mod.rs's insert_group comment restated the open issue verbatim
("a DAG is addressed by its container node, which submit inserts
itself"). Replaced with what is actually true for that path.
Dashboard behaviour deltas worth review: insert failures are now
logged per agent instead of swallowed, and UPDATE-ALL emits one queue
snapshot after the loop rather than one per agent.
- Drop the concrete incident reference; the doc should stand on its
own without pointing at one PR.
- Reframe the three-part gate as per-repo branch-protection settings,
not universal fact — CI-required, stale-review-dismissal, and
auto-merge are each configurable, not hive law.
- Reframe what auto-merge signals: the arming human already judged
the PR sound at a coarse level and expects only minor tweaks past
that point, not zero human review ever. A reviewer's bar is
substantive doubt, not any nit.
Prompted by hyperhive/hyperhive#3050: a reviewer held a formal
approve/request-changes verdict for CI to go green, which delayed a
merge that was only waiting on the review. CI, review approval, and
auto-merge arming are three independent gate conditions — a reviewer
is only responsible for the second one.
Documents the asymmetry that follows from auto-merge sometimes being
armed before a review lands: an unnecessary request-changes costs a
round-trip, but an approve given on lingering doubt can ship
immediately with no further human check. Default to holding when
unsure.
argus caught `approvals.md` still describing the old serial chain under the old
node name. Grepping the name across *all* tracked files rather than just `*.rs`
turned up three more, all in `coordinator.md`: the node-inventory rows for `Swap`
and the bookkeeping tail, and the rebuild shape diagram.
Three of the four were in the file I had edited in the previous commit to add the
brace section, which is the point worth recording: I grepped the *concept* I had
changed (`lease`) and the *symbol* I had renamed, but scoped the rename grep to
Rust. Neither pass could see an old node name sitting in prose.
Also adds the missing `AgentWindow` row to the node inventory.
The pre-push comment-block lint rejected two 40-line doc blocks, correctly: the
module doc and `rebuild_subtree`'s now carry the trigger and a pointer, and the
reasoning lives in a new `#### Braces` section.
That move surfaced a third doc the resource change had falsified. The scheduler's
lease-acquirer list still named `StopForUpdate` / `Swap` / `Signal` / `Drain`,
all of which are now exempt. The list now separates container-affecting nodes
from braces, and says why the rebuild subtree's members are exempt for a
different reason than `MetaSync` / `Prebuild`: they do touch the container, but
their brace holds the lease above them.
It was written into gateway.md, which only owns half the story: that
doc describes the web.sock path, and before this branch it never
mentioned agent.sock at all. Putting a rule shared by hive-c0re, the
harness and nginx inside the gateway walkthrough means someone asking
"why can't c0re dial agent.sock" has no reason to look there.
boundary.md already covers who may touch what across the trust boundary
-- including the sibling case of hive-priv's socket getting its mode
from the unit rather than the process -- so the rule lives there now,
with the three principals as a table. gateway.md keeps a two-line note
about the one fact it needs (nginx traverses via o=--x) and links out.
Both the gateway doc and the tmpfiles comment said "a directory without
the sticky bit lets any user unlink files in it". True of the old 0777,
but it names the wrong lever: write permission on a directory is what
confers the right to unlink its entries, and the sticky bit is only a
restraint on that -- it was never set here, so it is not what 0751
changes. Dropping o=w removes the permission outright.
The fix is unchanged; this is so a future reader doesn't go looking for
a sticky bit that was never there. Caught in review by argus.
/run/hive-agent/<name> was 0777 root root in steady state, not just during
first spawn. A directory without the sticky bit lets any user unlink files
in it, and the gateway container has all of /run/hive-agent bind-mounted
in, so anything that could reach the path could delete an agent's
agent.sock, bind its own, and receive that agent's todos from hive-c0re.
Two mechanisms were writing the dir and undoing each other: the tmpfiles.d
entry wrote 0777 root root, then hive-c0re round-tripped through hive-priv's
ChownSocketDir to narrow it. `d` re-asserts mode and owner on every apply
and the file is regenerated on any agent's spawn or destroy, so every such
event reset every agent's dir back to world-writable.
SyncAgentTmpfiles now carries each agent's container uid/gid and the entry
declares the answer: 0751 <uid> <gid>. Three principals need the dir and no
two share a group -- the harness binds its sockets (owner rwx), hive-c0re
dials agent.sock and the gateway's nginx dials web.sock (both only need
traverse, and both sockets are already 0666).
Deletes ChownSocketDir and ChmodSocketDir, both priv_client wrappers, the
either/or in host_config with its two swallowed warn!s, and the now-dead
socket_dir_path -- two verbs off the privileged helper's surface and one
round-trip off every agent spawn.
Also makes the two tmpfiles rules for /run/hive-agent itself agree: the
gateway module said hive-core, the generated file said root, and which won
depended on the order systemd read them in.
The disk_watch todo producer isn't turn-loop-shaped -- it's disk-space
state tracked in persistence.md's State dirs (per agent) section, right
next to hyperhive-todos.sqlite's own description. Leave a lean pointer
in the boot-wiring narrative instead of duplicating the detail.
Fixes#2727
The deletion PR removed the types but left ~10 sites still describing
them. Two are real breakage rather than staleness: rustdoc intra-doc
links to deleted items ([NodeView::kind] and [Self::snapshot] in
job_queue/mod.rs). Neither clippy --all-targets -D warnings nor cargo
test resolves intra-doc links, so the tree was green with both already
dangling.
The rest reassert facts the deletion made false: docs/coordinator.md
documented the event as RebuildQueueChanged { seq, queue: [DagView...] }
with a per-node field list, and three sites pointed at the removed
/api/state.rebuild_queue endpoint.
One is pointer rot rather than a rename, and no grep for a deleted name
finds it: SchedulesChanged justified itself as "same snapshot-shape
rationale as RebuildQueueChanged" -- which the deletion turned into the
one event that is not a snapshot. Repointed at TombstonesChanged /
MetaInputsChanged, in both the Rust doc and the dashboard doc.
Two are pre-existing and strictly out of scope, swept under the
pfadfinderregel because the same grep surfaced them: hive-sh4re/README
advertised a jobs module that crate has not had since the host-sock
split, and hive-host-sock/README claimed its own payload types live in
hive-sh4re.
Docs and comments only -- no behaviour, no API, no test changes.
hyperhive#3033 (jobq rollup endpoint) merged, unblocking hyperhive#3036.
The banner (removed on PR#3031 rather than ship it on an interim
GET /api/jobq/graph client-side derivation) is back, now reading
GET /api/jobq/rollup — hive-jobq-wire::state_rollup's pre-tallied
Vec<StateCount>, not the full graph. running sums the Running and
Finishing entries' roots (Finishing = own work done, subtree still
going); queued reads the Pending entry's roots. roots specifically,
matching the banner's established "N whole operations" meaning, not
the endpoint's parallel nodes count (~7 nodes per rebuild, 1 root).
Re-adds the rebuild_queue_changed SSE subscription dropped alongside
the banner, wired as a payload-less refetch trigger — confirmed with
atlas on the DagView-deletion tracker that this is the intended final
shape (keep the event, drop the payload) rather than deleting it and
falling back to polling.
Verified the running/queued derivation against constructed
StateCount[] fixtures (running+queued mix, Finishing-counts-as-running,
settled states never contribute, multi-state sums) before touching
swarm.js — 6/6 checks passed. npm run build clean, tracker-tag +
comment-block pre-push lints clean. docs/web-ui/dashboard.md updated
to describe the restored banner + the two prior shapes it went
through.
Third and last of #2860's agent-facing URL fallbacks. The operator's
ruling was "any special casing is done on the nix side - same binaries,
no hard coded fallback", so the default is deleted rather than replaced.
Every layer guessed the same wrong thing, and each guess was only ever
correct for a process sharing the host netns:
- nix/agent-modules/matrix.nix: matrixUrlDefault = localhost:8008, both
as the option's default and as a sentinel the daemon unit compared
against to decide whether to write HIVE_MATRIX_URL. Now nullOr str,
default null, the guard is != null, and the doc says what forge.url's
already says: null means "no matrix", not "guess one".
- nix/host-modules/hive-c0re/environment.nix: forwarded
http://127.0.0.1:<port> when no gatewayHost was set. hive-c0re shares
the host netns so it reads as harmless, but the value is handed to
agents, which do not -- there it names the agent itself. Now forwarded
only when there is a gateway vhost to name, matching the guard
HIVE_MATRIX_PUBLIC_URL already uses twelve lines below.
- hive-matrix-mcp: paths::DEFAULT_HOMESERVER was the same address
compiled in, so dropping the nix defaults alone would have left the
daemon dialling loopback inside the agent's own netns -- the very bug,
one layer down. homeserver_url() is now Option, and an account with no
homeserver is skipped with a log, exactly as one with no token is.
discover_token_accounts already refused to guess for the same reason.
Two comments taught the assumption back to the next reader ("shared host
netns means every agent container resolves localhost to the same
machine"); both now say which side of the netns boundary they describe.
MATRIX_HTTP keeps its value -- hive-c0re really does share the host
netns -- but no longer claims agents do.
Gated with nix eval against the extended agent-base config, as a pair:
with no url set the daemon unit carries no HIVE_MATRIX_URL, and with one
set it carries exactly that. Either check alone passes on a broken guard.
mara, on the already-approved PR: "dont replace one legacy thing with
another. then we will have to either wait with this pr or split it
into what can and cannot be done now."
Splitting: the transients-only per-agent badge fix is real, correct,
and fixes a live regression (the old DagView fields it read no longer
exist) — nothing about it depends on job-queue data at all, so it
ships as-is. The queue-summary banner is the part that doesn't belong
in this shape: it was reading GET /api/jobq/graph directly and
deriving counts client-side as an interim stand-in for the dedicated
rollup endpoint mara separately asked for — exactly the kind of
stopgap-on-a-stopgap her comment is calling out, since the endpoint
that should serve it doesn't exist on main yet.
Removes jobqNodesState, refreshJobqGraph(), the rebuild_queue_changed
SSE subscription, and the banner's render block from swarm.js/tabs.js
entirely — swarm.js now reads no job-queue state of any kind, fully
satisfying "swarm.js should not need to pull in the jobq to do its
job." The banner comes back once the rollup endpoint
(hyperhive#2985's follow-up) exists, reading that directly instead of
the full graph. Until then the per-agent transient pills still show
what's actually running on each card; only the hive-wide "N running /
M queued" summary line is temporarily gone.
CSS classes for the banner (.queue-summary/.queue-summary-link) kept
in dashboard.css rather than deleted-then-restored — commented as
currently unused, expected to come back unchanged.
docs/web-ui/dashboard.md updated to match (Container-row pending-
badge section, the removed Build-queue-summary-banner section, and
the BU1LDS-page note that used to describe SW4RM's now-removed
parallel fetch).
mara, on review: "swarm.js should not need to pull in the jobq to do
its job" followed by "remove the per agent pending stuff - only show
what is running."
Deletes queuedOpsByAgent() entirely — no more per-agent badge derived
from Pending-state job-queue nodes. A card's pending badges are now
driven exclusively by transientsState (i.e. actually-running work);
queued-but-not-started work shows nothing on the card until a node
starts. jobqNodesState + refreshJobqGraph() stay, now feeding only
the queue-summary banner (a separate, still-open question — mara
separately asked for a dedicated rollup endpoint for that, tracked
apart from this PR).
Collapses the now-always-coincident `pending`/`pending-running` row
classes into one (`pending-running`) — there's no more queued-only
row state to visually distinguish it from.
docs/web-ui/dashboard.md's Container-row section rewritten to match:
the two-store priority-fallback description is gone, replaced with
"transients only."
hyperhive#2822/PR#3026 moved swarm.js's per-agent in-flight status off
the rebuild queue. Two other reads of the same rebuild_queue field
survived that PR by design (a different feature, atlas flagged it on
#2985) and are the last DagView/NodeView consumers on the frontend:
queuedOpsByAgent()'s pending-row fallback and the SW4RM queue-summary
banner. Both now read GET /api/jobq/graph (hive-jobq-wire's generic
GraphNode shape) instead, matching the pattern builds.js already
established for <hive-jobq-graph>.
Along the way: DagView no longer carries state/kind fields (removed
in an earlier refactor that pushed roll-up derivation client-side),
so both migrated functions were silently reading undefined fields and
had become permanent no-ops — the pending-badge fallback never lit
and the queue-summary banner never rendered. This restores real
behavior rather than porting broken logic forward.
The queue-summary banner's node-count-vs-group-count question (flagged
on hyperhive#3028 as needing a decision) resolves cleanly: a GraphNode
group root (parent: null) is an ordinary node whose own state already
IS the group's roll-up per hive-jobq-wire's contract, so counting
roots by state is a direct filter, not a parent-chain walk or a
client-side rollup calculation.
Verified the derivation logic against constructed GraphNode fixtures
(multi-step chains, settled history that must not count, Finishing
roots, multi-agent single-DAG groups) before wiring it in — 13/13
checks passed.
docs/web-ui/dashboard.md's Container-row + BU1LDS sections updated to
match.
mara, PR #3026 review: "drop queuedLabelFor - just show what the
backend sends".
`queuedLabelFor` translated the rebuild-queue entry's `kind` field
through a curated English-phrase table ("meta_update" -> "meta-update
queued", "graceful_stop" -> "stop queued", etc.) for the one fallback
case where no transient exists for an agent. Removed the lookup
entirely -- `queuedOpsByAgent()` now returns the raw `kind` string
directly, and the render loop uses it as-is, the same opaque-string
treatment a transient's own `kind` already got (never run through a
lookup, per docs/web-ui/dashboard.md's existing "treat it as an opaque
display string" note). The queued-vs-running visual distinction still
comes entirely from the row's CSS classes (no ring/tint for queued,
amber ring + tint for running) -- the text was never carrying that
signal on its own, so nothing is lost by not reformatting it.
Updated docs/web-ui/dashboard.md's Container-row section to match.
npm run build clean, standalone verification re-run (17/17 checks,
one updated for the new raw-string expectation).
Fixes#2822.
`swarm.js` had two independent per-agent "is this in flight" sources:
`transientsState` (operator/worker-initiated ops the backend chose to
flag) and `inFlightOpsByAgent()`, a separate derivation straight from
`rebuildQueueState` covering everything else. Since #3010/#3016,
`running_transients()` is a status-only test — any `Running` job-queue
node naming a non-empty agent lights a transient pill, not just a
curated subset — so the second source's Running-state handling is now
provably redundant: a Running node with an agent always already has a
transient by the time `queuedOpsByAgent()` (renamed from
`inFlightOpsByAgent`) would be consulted.
## What changed
- `transientsState`: `Map<name, {kind, since_unix}>` (one pill per
agent) -> `Map<name, Map<kind, since_unix>>` (several pills per
agent). `applyTransientSet`/`applyTransientCleared` now add/remove
by `(name, kind)` rather than overwrite/delete by name alone, using
`TransientCleared`'s `transient_kind` field (landed in #3016) to
know which pill cleared. `syncTransientsFromSnapshot` groups the
now-flat `TransientView` list by name instead of assuming one row
per agent.
- `inFlightOpsByAgent()` -> `queuedOpsByAgent()`: trimmed to the
`Pending` (queued, not yet started) case only. The `Running` branch
and its "running beats queued" priority logic are gone entirely —
dead weight now that transients cover every running case
unconditionally.
- Render loop: an agent's transients win outright whenever any exist
(rendered as **one badge per pill**, not collapsed into one label —
mara: "show all running nodes that name the agent"); the queued
fallback only applies when a agent has zero transients. `opRunning`
simplifies to "does this agent have at least one transient".
- `docs/web-ui/dashboard.md`'s Container-row section rewritten to
match — it described a "transient, then in-flight-queue, in
priority order" model that's no longer accurate now that the second
source only ever fires for the one case the first can't represent.
## Verification
`npm run build` clean for both packages (dashboard + agent). Standalone
re-derivation of the transient-map + queued-fallback logic
(`/tmp/verify-swarm-transients.mjs`, not part of this diff) run against
constructed event sequences: single-pill lifecycle, two simultaneous
pills on one agent with independent clear-by-kind, clearing an unknown
kind is a safe no-op, a flat snapshot with duplicate agent names groups
correctly, the queued fallback only fires when no transient exists and
steps aside the instant one arrives, and a Running-state rebuild-queue
entry produces no queued badge (confirming the Pending-only trim is
correct, not just assumed). All 17 checks passed.
Verified directly against the merged backend rather than trusting
summaries: `job_queue/mod.rs::running_transients()` filters
`State::Running` only (not Pending — an earlier note of mine claiming
otherwise was imprecise paraphrasing), and `NodeView.agent` /
`running_transients()`'s agent both resolve through the same
`payload.agent()`, so a Running node's presence in `rebuild_queue`
and its presence as a transient are guaranteed consistent, not just
usually so.
#2985 (DagView/NodeView deletion) unblocks once this merges — atlas is
waiting on a ping.
`build_tombstone_views` folded `transient_snapshot`'s keys into its `live`
set, so an agent with in-flight transient work was treated as not-a-tombstone.
Since #3010 that set is derived from the running job graph, which made a
page about on-disk state a function of the scheduler.
Dropping the filter exposes what was always true underneath: nothing records
a destroy. Every definition-side artifact — state subvolume, proposed +
applied repos, `deployed/0`, meta registration, topology entry — is written by
`Provision` before the container exists and survives `lifecycle::destroy`. So
a mid-spawn agent is byte-identical on disk to a tombstone.
Per mara on #3020: remove the filter, warn on the page, keep the issue open
for the swarm-controller / snapshot-storage rework where the problem shape
changes anyway.
- dashboard/tombstones.rs: drop the param + the chain; document the real
semantics
- core.js: amber caveat banner above the rows; row badge `destroyed` ->
`offline`, which is what an absent container actually proves
- dashboard.css: `.tombstone-warn`, modelled on `.port-conflict` but amber and
without the pulse — a permanent banner that pulses trains you to ignore it
- docs/web-ui/dashboard.md: the pane was described as "destroyed-but-state-kept
agents", now the exact wrong claim
Fixes hyperhive#3014.
docs/README.md is a genuine hand-written index for the docs/ tree -
task-oriented reading paths grouped by topic, covering every top-level
doc plus the three subdirectories that already have their own landing
page (web-ui/, tools/, turn-loop/). Adapted from CLAUDE.md's existing
"Reading paths" section (already curated and kept current) rather than
written from scratch, reorganized into headed groups since this is a
landing page, not a flat reference list. Explicitly covers
docs/tools/matrix.md and docs/github.md, per the note on hyperhive#3014
about the content that moved out of README.md's deleted section on
PR #3006 not going undiscoverable.
Top-level README.md's reading-path table (enumerating every doc)
replaced with a single prominent docs-site link, per mara's "replace
docs links table in readme md with a prominent link to the docs
(public host and git relative path)" - both forms present (rendered
site URL, git-relative docs/ path).
Verified every link in docs/README.md resolves to a real file (33
files checked). nix fmt clean, tracker-tag grep clean.
Companion to hyperhive/website#47/#48 - this is what that PRs "index.html
half" needs to exist before it can render, per atlas's note on #48.
Part of hyperhive#1898 (b), the other missing docs subdir. Unlike
turn-loop/ this one has no single existing file that already covers
the whole directory - hivectl.md/hivectl-cli.md are genuinely
operator-facing (the operator's own host CLI), while bash.md/forge.md/
lifecycle.md/matrix.md/scheduling.md document the agents' own MCP tool
surface (a different audience: what the agent can do, not what the
operator does). Writes a new README.md rather than moving one,
splitting the link list along that line so the operator-relevant half
leads.
Part of hyperhive#1898 (b): every docs subdir should have a top-level
README.md link, achieved by moving/renaming where an existing file
already fits the role.
docs/turn-loop.md already served as the hub + index for the three
sub-pages under turn-loop/ (claude-invocation.md, config.md, mcp.md),
so it moves wholesale rather than leaving a redundant top-level
pointer stub. Fixes every inbound/relative link across the repo
(top-level README.md, CLAUDE.md, docs/persistence.md,
docs/tools/scheduling.md, the sub-pages own back-link, hive-agent
README + doc comments, hive-agent/Cargo.toml, .prettierignore per-file
exemption entry) - grepped the whole tree for both turn-loop.md and
turn-loop/ to find every reference rather than trusting a partial
list.
nix fmt clean, cargo check -p hive-agent clean.
Per mara on hyperhive#1898: the top-level README should not go into
details about impl details or specific subsystems. Trims the
Multi-account Matrix support and GitHub account sections down to a
one-line pointer each.
The matrixAccounts option detail that was only ever documented in the
README (checked: docs/matrix.md is entirely about the host hive-matrix
container, not this per-agent option) moves to docs/tools/matrix.md
instead, next to the account parameter every matrix tool already
takes - the natural home, not a link to a page that never covered it.
Per mara's explicit steer on hyperhive#2812 ("also replace the build
queue tab with this component" + "graph fetching should live in the
component, not build.js" + "dont replicate the grouping by dag"):
R3BU1LD QU3U3 is now a mounted <hive-jobq-graph endpoint="/api/jobq/graph">
element. builds.js no longer renders the queue itself, does its own
fetch, or hand-rolls a per-root tree/roll-up/cancel-button — all of
buildNodeTree/topoSort/entryFingerprint/renderQueueEntry/
firstFailedNode/rebuildQueueRowCache/QUEUE_STATE_GLYPH/rollupState is
gone.
builds.js's remaining job is listening for the component's
hive-jobq-graph-update event (added to the component in the prior
commit) to keep a flat jobqNodes array in sync, and using that for the
two things the generic view doesn't render: the count-pill and the
live-log panel. On the rebuild_queue_changed SSE tick, calls the
mounted element's .refresh() instead of doing its own fetch — that
event still carries its own queue payload on the wire (tabs.js/SW4RM
still reads it for the badges, untouched), this page just ignores it
now.
Also removed, now genuinely dead: the two elapsed/finished-time
tickers (nothing produces the .rqe-when spans they targeted anymore),
stateSlug and isoToSecs (no callers left), fmtElapsed import (no
callers left).
New @hive/shared/jobq-graph.js export entry in packages/shared's
package.json, alongside the existing hive-tab-strip.js/hive-menu.js/
etc. pattern.
docs/web-ui/dashboard.md's R3BU1LD QU3U3 section rewritten to match:
mounted-component shape, no source/reason/cancel-button/deep-link on
rows (generic component has none), settled entries show their full
step tree (Done nodes aren't filtered off this wire, unlike the old
DagView projection).
Verified against real production data again (this hive's own live
/api/jobq/graph, now settled — no in-flight build at test time) plus
a synthetic running-build case to exercise findLiveBuild's happy path:
correct live-node detection (build_log_id gate), correct in-flight
root count. Confirmed the built dist bundle actually registers
customElements.define("hive-jobq-graph", ...) — the new shared
package export resolves correctly through esbuild.
Branch reused per mara's explicit "dont rework #3000 - continue
working on _this_ pr [#2996], it already has the component that
replaces 90% of build.js" — this ships as part of PR #2996, not a
separate PR.
Adds services.hyperhive.forge.publicUrl (defaults to the gateway vhost
URL when behindGateway=true, null otherwise). HIVE_FORGE_PUBLIC_URL is
now sourced from it instead of hardcoding https://${forge.domain}
whenever behindGateway is on.
The 4 frontend call sites that built a forge link from
state.forge_public_url now hide the link when that's absent, rather
than guessing http://<browser-hostname>:3000 — a guess that's only
correct by accident once the operator isn't on plain localhost. Fixes
the dashboard H0M3 tile, per-agent-row forge links + agent menu, the
approval-queue PR link, and the per-agent page's own meta-nav forge
link (found during this pass, same defect, not in the original
3-site inventory).
Docs + doc-comments updated to match.
ci.md mixes a genuinely short operator bootstrap step with deep
auto-registration/container-design internals and a full security
writeup an operator does need to read once, but not first.
Adds a short synthesis at the top: enabling is one nix option, the
unsandboxed-build trust tradeoff (fine for a trusted team, gate it if
you take fork PRs), and the disk-maintenance recipe (CI shares the
host nix store with no auto-GC of its own). Every claim checked
against the existing detail further down in this same file.
Part of hyperhive#1898.
persistence.md is exhaustive schema/impl reference (sqlite table
columns, systemd unit internals, marker files, btrfs subvolume
mechanics) with the one thing an operator actually needs - what
happens to my stuff when I destroy or purge an agent - scattered
across several sections rather than answered up front.
Adds a short synthesis at the top: destroy keeps everything
(revivable, no re-login), purge wipes it all (no undo), approvals and
questions never age out, message history vacuums acked rows at 30
days, an agent's own /state/ and claude login survive every restart/
rebuild, and the root agent auto-recreates if it's ever destroyed.
Every claim checked against the existing detail further down in this
same file before writing it.
Part of hyperhive#1898.
argus review: the submitting agent is always notified when its request
is denied (HelperEvent::ApprovalResolved fires unconditionally); only
the reason text is optional, and only from the dashboard prompt (not
the CLI, which has no reason argument at all). Also note that
cancelling the dashboard prompt aborts the whole deny, not just the
reason.
approvals.md (655 lines) is genuinely dense implementation reference
(webhook mechanics, DAG phase names, db column names, git-ref rollback
internals) with nothing written for an operator deciding whether to
click approve. Unlike web-ui/, this is a single top-level file with no
subdirectory to give a separate landing page to, so the fix here is
in-file: a "For operators" section right after the intro, covering
what actually shows up on the dashboard (or hivectl CLI) and what each
approval kind does when you click approve or deny, before the existing
implementation detail.
Every claim in the new section was checked against the rest of this
file plus dashboard.md and hivectl-cli.md rather than assumed - caught
one real error before pushing (hivectl approvals list doesn't exist,
the verb is `pending`) and one incomplete claim (denial reasons are
dashboard-only with an optional prompt; the CLI deny has no reason
argument).
github.md mixed operator content (enabling, provisioning, security)
with deep implementation detail (the gh wrapper/credential-helper
mechanics, the notification poller's internals) in file order, so an
operator reading top-to-bottom hits internals before finishing the
part they actually need.
Pure reorder, no rewrite: Enabling -> Provisioning -> Security (all
operator-facing) now come first: How the agent uses it and
Notifications (both pure impl detail) move to the end, with a one-line
marker between them. Every word of existing content is unchanged, only
section order moved - lowest-risk shape for a file like this with no
subdirectory to split into (see hyperhive#1898).
argus review on hyperhive#2986: the README listed Stats/Peers/Settings
alongside Permissions/Schedules as dashboard tabs. Checked the actual
frontend rather than trusting dashboard.md prose (which is internally
inconsistent on this - some section headers say "tab" for things that
turned out not to be):
- stats.html and settings.html are real separate bundles
(frontend/packages/dashboard/src/{stats,settings}.{html,js}) - same
shape as builds.html/core.html/logs.html, moved to the "own page"
list.
- Peers is neither a tab nor a page - swarm.js::renderPeerHives
confirms it is a card list rendered inside the SW4RM tab
(#peers-block/#peers-section), gated on state.peer_hives being
non-empty. Folded into the SW4RM bullet instead of listing it as its
own item anywhere.
The dashboard tab strip is genuinely just four: SW4RM, Y3R C4LL,
P3RM1SS10NS, SCH3DUL3S.