`age_seconds` is documented on the LooseEnd enum as saturating to zero on
any clock anomaly, but the derivation was in three places: hive-c0re had a
named `saturating_age` helper with tests, and the in-agent socket server
hand-rolled the same two lines twice, untested.
Move the helper to hive-sh4re::inbox, beside the enum whose contract it
implements and inside the one crate both producers already depend on. Its
three tests move with it (not dropped) and gain two arms: the whole-i64
range, where the saturating_sub is what stops the subtraction overflowing,
and a far-past control so those zeros are the clamp firing rather than the
function bottoming out on large inputs.
The two clamps are not redundant, which is what `to_loose_end`'s doc got
wrong: it credited "saturating" for the zero, but saturating_sub bottoms
out at i64::MIN, still negative. The try_from is what yields 0.
Also cover the two projections themselves, which is the part the shared
helper cannot: that a reminder ages from created_at rather than due_at,
and a todo from updated_at, with a future timestamp reading 0 through
both and a past-timestamp control on each.
`nix/templates/harness/` is cited from `send_allow.rs` and twice from
`mcp_config.rs`, in the present tense, as where the NixOS module writes
the send allow-list, writes the extra-MCP spec, and defines the MCP http
port default. There is no such directory — `nix/templates/` holds only
`agent.nix` and `ruth.nix`.
All three describe things that live in `nix/agent-modules/mcp.nix`:
send-allow.json is written at :230, extra-mcp.json at :228, and the
`hyperhive.mcp.httpPort` option is declared at :143.
These are the mechanical half of #3923, which explicitly carved the
repointing out from the question of whether the reference gate should
widen. Nothing here changes that question.
Mutation testing found this arm untested: classify_tool_result has its
OWN `<= 120` deciding icon-vs-body, separate from the one in
summarize_tool_result that picks the summary text. The existing case
used 2 and 500 characters, so neither arm went near the boundary and
moving it to 119 changed nothing.
Verified the new test fails under exactly that mutation.
clippy::format_collect (pedantic, denied) fires on
`(0..40).map(|i| format!(...)).collect::<String>()`. The test only needs
40 non-empty lines, not numbered ones, so `"line\n".repeat(40)` says the
same thing without the lint.
`r.find(':')` returns a BYTE offset; it was being passed to
`r.chars().take(colon.min(9))` as a CHARACTER budget. For a multi-byte
room-id local part the two disagree, and the extra characters come out
of the server half:
fmt_room("!ÄÖÜ:server") == "!ÄÖÜ:se" // want "!ÄÖÜ"
The doc comment above it already said the `chars().take()` was there to
handle non-ASCII, so the intent was recorded and the implementation was
half of it. Split on the colon first, then take 9 characters of the
local part.
ASCII behaviour is unchanged and pinned by the existing case:
`!abcdefghijkl:server` -> `!abcdefgh` before and after.
Found by the tests in the previous commit — written red, then fixed.
Eleven doc comments pointed at `docs/` files as markdown links. Ten of
them render as broken hyperlinks in the docs rustdoc CI builds, and
nothing in the tree can tell.
Rustdoc renders a page at `target/doc/<crate>/<module…>/`, so a relative
link resolves against that directory and not against the source file it
was typed in. Every one of these except the single crate-root `//!` was
written for a reader resolving from the source tree, which is one `../`
short at module level and two short one directory deeper.
Two measurements on a throwaway crate, same build and same
`RUSTDOCFLAGS="-D rustdoc::all"`:
* a bogus intra-doc link `[`no_such_item`]` is a hard error, so the
`docs-rustdoc` check in nix/checks.nix works for its class;
* a relative link to a nonexistent file in the same comment produces
no diagnostic at all and lands in the html verbatim as
href="../../../docs/does-not-exist.md".
So the class is invisible to the one gate whose stated purpose is to
stop a doc pointer dangling — and it is worse than the plain-text
failure that gate's comment describes, because a broken href still
looks clickable.
Fixing the depths was the other option and is rejected: the correct
depth is a function of how deeply the module is nested, so any module
move silently breaks it again, and no check we have would notice.
The link text was already the canonical pointer — `docs/x.md::Section`,
the same repo-root-relative form used everywhere else in the tree and
the form scripts/check-doc-refs.sh gates. Dropping the `[…](…)` wrapper
keeps every byte of information a reader uses and removes the only part
that was ever wrong.
Refs #3926.
Per mara's go-ahead on hyperhive#3902 ("getting started is good, but
terminal rendering does not go in there i think"):
Moved 21 top-level docs/*.md files into 7 new topic subdirectories
(existing web-ui/, turn-loop/, swarm/, tools/, crates/ untouched):
getting-started/ setup.md
agent-lifecycle/ agent-hierarchy.md, approvals.md, persistence.md
trust-boundary/ boundary.md, security.md
integrations/ forge.md, matrix.md, github.md, knowledge.md
networking/ gateway.md, network.md, snapshot-store.md
scheduler/ jobq.md, coordinator.md, ci.md, observability.md
process/ conventions.md, gotchas.md, pr-review-gate.md
web-ui/ terminal-rendering.md (moved into the EXISTING dir,
per mara's correction to the original getting-started
guess -- it's UI implementation detail, not onboarding)
The physical layout now matches docs/README.md's own topical headers,
which already amounted to this taxonomy -- see the scoping comment on
the issue for the two findings that motivated this (a genuine
duplication between CLAUDE.md's old "Reading paths" list and
docs/README.md's grouped one, since drifted out of sync with each
other; and the flat layout not matching the grouping we already had).
Fixed every cross-reference this moved across the whole repo (~120
files: docs/ internal links at every depth, Rust doc comments, nix
module option docs, crate READMEs) -- verified two ways: a grep sweep
confirming zero remaining references to any old path, and a script
that resolves every markdown link in docs/**/*.md + CLAUDE.md +
README.md against the filesystem and reports anything that doesn't
exist (zero broken links).
Collapsed CLAUDE.md's "Reading paths" section (the duplicate) down to
a pointer at docs/README.md, now the single index. Rewrote
docs/README.md itself to use the new subdirectory paths and added the
one doc it was missing that CLAUDE.md's old copy had (pr-review-gate.md).
Classified all 22 docs/*.md files first via a haiku subagent (mara's
suggestion) on two axes -- proposed grouping and operator-vs-
implementation focus -- before finalizing the taxonomy; spot-checked
the report and found internal inconsistencies (its classification
table disagreed with its own summary section for a few files), so this
taxonomy is my original proposal + the one correction mara gave
directly, not a blind application of the subagent's table. The
operator-focus data it gathered is still useful for a follow-up
content pass (docs skewing 'mixed' rather than pure operator-facing),
not addressed in this PR -- structure only.
nix fmt clean, both pre-push lints clean.
Don't state what a function/module doesn't do and where that
happens instead — just describe what it does. Cut the "not
something this function decides" / "not affected by this" /
"not a placeholder for a later commit" asides from the doc
comments touched in the last two commits.
ClassifyCtx's tool_use-id correlation gates markdown-vs-plain body
format for a recv result, not open/collapsed state — that's always
the operator's uniform preference now. 5 backend comments still
described it as controlling "default-open" rendering, contradicting
the actual render path and this PR's own rewritten docs.
Also fixed useAgentState.ts's stale comment promising an
SSE-triggered refresh model "in a later commit" — that's permanently
off the table now that the terminal stream's kind tag is gone by
design (argus flagged this as a drive-by, not blocking, but it's a
one-line cause-and-effect of this same PR so fixing it here).
Move terminal-row classification server-side into a new
hive-agent/src/term_msg.rs, replacing the old JSON-mutation
enrich()/stamped-field approach in stream_enrich.rs with one
uniform wire shape: {icon?, level: debug|info|warn|error, summary,
body?, body_format?: markdown|diff, coalesce_key?}. No more per-row
`kind` tag or raw claude-JSON passthrough — every row is the same
shape, with structural identity carried by icon + summary text
instead of a CSS class per row kind.
hive-agent/src/web_ui/stream.rs's history + SSE endpoints now both
call term_msg::classify() and serve TermEnvelope{ts, seq?, msgs}
frames; events that classify to zero rows (agent-state changes,
drop-noise) never reach the wire.
Frontend: classifyEvent.ts collapses from a large per-tool dispatch
tree to a thin TermMsg -> StreamRow adapter. streamRow.ts/Row.tsx
drop the now-dead meta/childText fields. terminal.css switches from
a dozen-odd per-row-kind classes to four level-based color rules.
Expand/collapse of a bodied row is now a uniform client-side
decision (the operator's preference), no server-side per-tool
override.
docs/terminal-rendering.md rewritten to match.
mara, on PR#3765: "follow up: the gear looks broken in browser too" —
bare gear (dropped the variation selector already, per argus's earlier
review) still doesn't render as color emoji in her font. Same failure
icons.tsx already documents for this exact codepoint. Landed on the
wrench instead: a supplementary-plane pictograph with
Emoji_Presentation=Yes, same category as the other three built-in
icons this thread already fixed (stats/screen/forge/dashboard), so it
doesn't need a variation selector to render as color emoji at all.
mara: 'i cannot see if sonnet became claude-sonnet-5 or something
else.' /api/state now carries resolved_model (bus.last_resolved_model(),
already used by serve_common.rs's turn-stats rollup for the same
alias-vs-actual reason) and the model badge shows it as a title
tooltip, same pattern as the ctx/cost badges.
argus: same failure shape as the screen icon, and icons.tsx's own
comment already names this exact codepoint as unreliable across
fonts. Bare gear, matching every other gear usage in the tree.
The variation selector was meant to force emoji presentation, but her
font renders it as a separate unrenderable glyph instead of being
silently consumed. Screen's bare codepoint was already a real emoji
before this PR touched it — didn't need changing at all.
mara, #3762: stats already used a real emoji, the rest used plain
unicode arrow/hexagon glyphs — mixed style, and some of those glyphs
don't even render reliably (hexagon showed as a plain circle in one
repro). Picked a small matching set: compass/bar-chart/desktop/
hammer/gear. Operator-declared dashboardLinks extras keep their own
emoji unchanged.
argus caught that the previous wording ("splitting a long-lived file
into dated pieces") suggested a remedy that increases top-level entry
count unless the pieces land in a subdirectory - wrong advice for the
exact metric this watch counts.
Also added mara's ask: explicitly note the agent can leave the todo
open and act on it later, no pressure to resolve immediately.
New state_entry_watch.rs, mirroring disk_watch.rs's shape exactly:
periodic in-process probe, Todos::upsert with a stable count-bucketed
summary (anti-nag - drifting inside one bucket stays silent, crossing
a bucket speaks up again), clears once back under threshold.
Top-level entry count only, deliberately - a large subdirectory (git
clone, build tree) counts as one entry regardless of what's inside it,
which is disk_watch's problem to catch on its own axis (bytes), not
this one's.
Wired into main.rs's spawn_todo_socket alongside disk_watch::run.
closes#3464
fmt_args_generic's generic array handling collapsed `ids: [4]` — the
count — since mark_todos_done had no dedicated match arm. Added one,
matching the file's existing per-tool pattern (extracted into its own
helper to stay under the 100-line clippy limit on fmt_hyperhive_tool).
The #2595 job-queue rework replaced hive-bash-mcp's direct broker send
with a todo push through the in-agent socket, so a bash-task-driven
turn now wakes with from == "todo", never "bash-task-<id>". This
branch has had nothing to match since, so it silently bucketed no
turns under the bash-task label; removed rather than left dead.
Closes#2256.
Nothing in the gate read doc-comments: clippy doesn't check intra-doc
links, cargo test doesn't, and no check built docs. So a [`Foo`] pointing
at a renamed, moved or deleted item rendered as plain text and had no
discoverer but a human happening to read the comment.
That matters here more than in most repos, because the convention is to
put a thing's authoritative description in one doc-comment and point at
it from everywhere else -- the design leans on the pointers being real,
and a dangling link is worse than no link since it names something and
sends the reader looking.
Adds `docs-rustdoc` to nix/checks.nix: craneLib.cargoDoc over
--workspace --no-deps --document-private-items, denying six rustdoc
lints. Listed explicitly rather than -D warnings so a new lint appearing
upstream cannot red the build on a class nobody has triaged.
--document-private-items is load-bearing rather than thoroughness for
its own sake: most of this workspace's doc-comments live on private
items and //! module headers, so without it rustdoc checks a small
fraction of the links and the gate sits green while the rot continues.
Then fixes every error it reports, 40 to 0 across nine crates. The
classes differ and so do the fixes:
- public item, wrong scope -> qualify. Node and Node::parent are both
public; the link failed only because scheduler.rs does not import
Node. Six sites become [`crate::Node::parent`].
- private item -> downgrade to backticks. Nothing was made public to
satisfy a lint; changing API surface to appease a doc check would be
the tail wagging the dog.
- genuinely dead -> [`JobBuilder::insert_into`] names a method that does
not exist. Insertion is Scheduler::insert_job.
- prose that looks like markup -> argv[0] parsed as a link, and
<args>/<hex>/<name> parsed as HTML tags.
Note for future fixes: pub(crate) resolves in an intra-doc link, a plain
private fn in a binary crate does not (wait_for_nodes resolved,
connect_hint did not, same crate, same shape).
The check does not ride the clippy/test artifact cache. It takes
cargoArtifacts, but rustdoc needs its own flavour of dependency
metadata, which cargo build does not produce, so a --no-deps docs build
still compiles dependencies it never documents. Measured at 6m47s cold;
that reasoning is recorded in the check's own comment so the next reader
does not re-derive it.
Verified by running the check's exact command against the pre-cleanup
tree first: 40 errors, build failed. A gate that cannot fail is not
evidence, and building it before the cleanup makes that proof free.
hive-agent's own web_ui module never exposed the agent's own paused
status to its own /api/state — the dashboard's cross-container view
knew it, but a per-agent page had no way to know it's paused. Added
StateSnapshot.paused (a direct stat of the same harness-local pause
marker hive-c0re's Coordinator::is_paused checks).
The per-agent page's ⋯ overflow menu now has a pause/resume item that
POSTs to hive-c0re's existing /api/pause/<name> / /api/resume/<name> —
the same endpoints the dashboard's <hive-agent-menu> already uses,
same cross-origin form-submit pattern the existing rebuild-container
item uses. The item's label tracks state.paused on every /api/state
refresh so a pause/resume triggered from the dashboard while this page
is open doesn't leave a stale action showing.
Per review: docs represent current state. Every "used to" / "no longer"
clause this branch introduced is gone — including the History section in
network.md, which was a whole subsection about a sync mechanism that
doesn't exist.
Where the removed clause was carrying a real constraint, the constraint
stays and is stated in the present tense instead of as a delta: nothing
narrows what the gateway's nginx can reach except the directory
permissions in front of a socket, and nothing bounds `ReloadGatewayNginx`
except the hard-coded unit name. Those read as rules now rather than as
the story of how they came to be rules.
The gateway's nginx + dnsmasq no longer run in their own nspawn container.
`nix/host-modules/hive-gateway/default.nix` loses the
`containers.hive-gateway` wrapper and everything that existed only to punch
holes in it: `privateNetwork = false`, `CAP_NET_ADMIN`, five bind mounts,
its own `stateVersion`, `networking.firewall.enable = false`,
`networking.resolvconf.enable = false`, and the `hive-gateway-resolv`
path+service pair. 465 -> 303 lines.
The container never bought isolation here. It shared the host netns by
necessity — nginx binds the host's :80/:443, dnsmasq answers on the bridge —
so each of those settings was undoing a boundary the gateway could not
afford in the first place.
Four things made it more than a deletion, none of them visible in the nix
diff:
- The self-signed cert service also imports the hive CA leaf, so removing it
with the container would have left nginx naming a missing cert file, which
it refuses to load at all.
- The nginx reload is a hive-priv verb. It still needs root, but no longer
for the reason its doc gave, and `--machine=` was both transport and
scope — so the unit name is now hard-coded in the helper as the
containment.
- The lifecycle verb named a container that stops existing.
- `journalctl -M hive-gateway` had no machine to enter.
Per the operator's ruling, the operator verb keeps working and agents lose
it. `InfraContainer` answered three questions that used to share an answer;
it now splits into `name()` (identity), `target()` (Container vs HostUnit),
`service_unit()` (the systemd unit), and `agent_restartable()`, which the
MCP restart path checks before the capability so the refusal cannot read as
"ask for infra_admin". `SIBLING_CONTAINERS` drops the gateway — it gates the
requests that name a container as a string — while `FromStr` still accepts
it, because that answers what a name is, not who may act on it. The
dashboard's gateway journal reads host journald filtered to `nginx.service`.
Prose was corrected where it only named a location, and re-argued where the
container was doing security work: a `0666` per-agent socket was safe
because only the gateway container had the directory bind-mounted. There is
no mount now, so the directory permissions are the whole of the access
control — the constraint holds, its mechanism doesn't.
Gate: nix fmt / clippy --all-targets -D warnings / cargo test all clean (710
tests); hivectl-cli.md regenerated from the clap tree. The nix eval was run
in both TLS shapes at this commit: every delta in the rendered
virtualHosts is one of the three intended path moves, dnsmasq settings are
byte-identical, and the absence probe flips true -> false with bindMounts
emptied.
Closes the #3110 split — lib.rs is now just the crate doc comment and
the pub mod list.
journal.rs's new doc comment fixes a pre-existing bug: the old
JournalPriority doc text in lib.rs was actually half Capability's doc
(a leftover from an earlier reorder that moved the code but not the
comment above it).
hive_sh4re::assets::branding_svg() resolved a server-side default
icon at runtime from HIVE_ASSETS_DIR — the only consumer was
serve_icon(), which fell back to it whenever the agent had no
`hyperhive.icon` override. Removed both the fallback and the
function: serve_icon() now 404s when /etc/hyperhive/icon.svg is
absent, and the per-agent web UI (app.js) picks up the existing
dashboard swarm.js pattern — swap the <img> src to the
frontend-bundled /favicon.svg on load failure, guarded against
looping if the fallback itself 404s.
Updated the doc/comment claims that said the server always returns
an image (docs/web-ui/agent.md, nix/agent-modules/default.nix, the
hive-c0re/forge/users.rs comment referencing the old shared-asset
set). forge-avatar-sync and the matrix avatar sync are unaffected —
both are gated on hyperhive.icon != null and never depended on the
removed fallback.
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.
hive-c0re pushes todos into each agent over the in-agent socket, and
every one of those dials has been failing with EACCES. The socket is
created by todo_server::bind with no mode set at all, so it lands at
0777 & ~umask -- typically 0755. connect(2) on a unix socket requires
*write* permission, and hive-core is neither the socket's owner nor in
its group, so it is locked out.
The tell is the sibling socket. web.sock is bound in the same
directory, by the same process, as the same user, and does set its mode
(0666) immediately after bind. Only the socket missing that call fails,
which is also why no ownership or chown theory explained it: both
sockets share every directory they live in, so anything at the
directory level would have broken them together.
Fix is the two lines web.sock already had. Access control for these
sockets is the containing directory's job, not the socket's -- the mode
here only has to not exclude the host daemon that is supposed to reach
it.
Observable effect: scheduled prompts and message wakes reach agents
again. An agent whose wake is dropped still sees its messages whenever
something else wakes it, so the failure presents as agents that look
healthy but answer late, or not at all if nothing else is waking them.