The TLS section read as universal — "the gateway always terminates TLS",
"listens on httpsPort on every vhost". Measured against the rendered
config of an all-local host: ten of eleven vhosts carry two listens and
addSSL/forceSSL, and `grafana-metrics` carries one plain-HTTP listen on
127.0.0.1 with a single `= /metrics` location proxying grafana's unix
socket for the local collector.
The exception is right; a doc with no room for it is not. Someone asking
"is anything here serving plain HTTP" was told no, when the useful answer
is "no, except an internal class that is loopback-bound and deliberately
exempt" — which tells them what to look for on the next host.
One sentence where the modes are introduced rather than a patch to each
sentence that inherits the assumption, plus "every vhost" -> "every
routable vhost" where the self-signed default repeats it.
Two neighbouring claims were checked at the same time and hold: /_matrix/
proxies to 127.0.0.1:8008, and .well-known/matrix/client returns the
homeserver base_url with no port suffix on 443.
Closes#4191
the module doc pointed at docs/integrations/forge.md::Sources, a section
that was never written. the actual rationale + host differences it
promises already live in docs/integrations/github.md's Notifications
section (auth scheme, cadence) -- point there instead of duplicating
content across two docs.
fixes#4189
`docs/tools/forge-cli.md` is generated from the clap tree, so changing an
argument or its doc comment changes that file too. The freshness check in
`checks.hive-forge-docs` is what says so, and it is what failed here —
after 8m44s, on a PR that was already approved.
Regenerated, no hand edits. Two hunks, both the `pr status` section, and
`nix fmt` leaves the result byte-identical so the formatter and the
generator are not fighting over it.
Part of #4182.
Every other PR-scoped verb takes the number as a positional — `pr show
42`, `pr comments 42`, `pr assign-reviewer 42 argus`. `pr status` alone
required `--pr 42`, so whichever form you learn first is wrong for the
other, and clap's error for the mistake suggests `-- --pr`, which would
pass the literal string on as the next positional.
The number is positional here too now. `--pr` stays, because it was the
only spelling this verb had; `--sha` keeps its flag because the two are
alternatives rather than one required argument.
Two error strings in this file also named `pr-status`, a form that has
refused to run since the subcommand rename. One of them is the message
you get for passing neither argument — i.e. exactly when you are already
unsure what the verb is called.
The four new cases cover both halves: that clap accepts each spelling,
and that something reads it. `target_pr` is named rather than inlined
for exactly that reason — dropping the positional from the selection
passed every parse-only case while leaving `pr status 42` reporting
"pass a PR number". The conflict case earns its keep the same way: clap
accepting an argument and clap ignoring it are indistinguishable from a
passing parse, so `42 --sha abc` and `42 --pr 42` both have to be
rejected.
Closes#4182.
observability.md scoped the whole feature to Claude Code statistics, so a
reader asking what leaves an agent container would have concluded
counters and nothing else.
That page also documents the trust properties of the agent->collector
hop, and those are what needs restating for a richer payload: same
destination, same credential, same already-accepted ability to push
arbitrary OTLP. What changes is how much detail leaves by default.
docs/swarm/services.md needs no change. It already said each hive ships
the journals of "the harness units inside every agent container" — which
was not true until now.
Part of #3940.
Nothing in this suite evaluated nix/agent-modules/ at all: every fixture
was a host, so a typo in a rendered container config surfaced on a real
deploy and nowhere else. This adds an `agent` constructor beside `hive`,
off the same `nixosModules.agent-base` the meta flake hands a container.
It also adds the suite's first two-hive fixture. Every existing one
declares `swarm.hives.h1` alone, so a per-hive arm written against those
passes on a hardcoded literal — which is exactly what the new per-hive
logs pipeline needed covering.
Eight cases, each paired with the control that makes it mean something:
the absence arms with a presence half, the per-hive arm with a roster
length check, because `lib.all` over an empty roster holds vacuously.
Each was then shown to fail. Eight mutations across the three files —
`directory` back to the runtime default, a pipeline naming no receiver,
an exporter aimed at a loopback literal, an exporter name that stops
reading `protocol`, a missing hive-tier logs pipeline, the metrics-only
processor inside it, a per-hive pipeline hardcoded to one hive, and logs
pipelines exporting to the metrics store — all caught, none survived,
none skipped, each run's baseline green.
Part of #3940.
An agent container writes a complete journal — 991 MB and nine days deep
on this hive — that nothing outside it can read: the host-side
per-container journal directory is an id-mapped bind mount, and journald
writes nothing into it. So the reader has to run inside the container,
and the path it would push to did not exist.
Three tiers, one vertical slice, because any two of them alone are
silent:
- the agent container gains an `opentelemetry-collector` with a
`journald` receiver aimed at its own journal and an exporter aimed at
the same base address every in-process producer already exports to.
- the hive collector gains `service.pipelines.logs`. Without it the
`otlp` receiver answers 404 on `/v1/logs` — measured, and
indistinguishable from a route that was never meant to exist.
- the swarm collector gains a per-hive `logs/<hive>` pipeline beside
`metrics/<hive>`. Without it the push is accepted, answered 200, and
routed nowhere.
The receiver's `directory` is stated rather than inherited, and that is
the load-bearing line: its default is the RUNTIME journal
(`/run/log/journal`), which in an agent container is empty. Left at the
default this whole path validates, starts, reports healthy and forwards
nothing. The assertion beside it covers the same silence from the other
end — a `volatile` or `none` journald storage empties the directory the
receiver reads.
Attribution follows the tier that can prove it. The forwarder stamps
`agent`, which no host-side reader could supply; `hive` is deliberately
left to the swarm tier, which upserts it from whichever receiver
accepted the sample, precisely so the label comes from something the
sender cannot write.
No `units` allowlist, unlike the swarm tier's journald receiver. That
one needs one because the host's journal also holds an operator's own
session; a container's journal is the harness and what the harness
spawns. Measured volume is 20827 entries / 6.3 MB per agent per day,
with nothing logging below `info` — so the receiver's `info` default
filters nothing and there is no bill to justify a knob.
Agent containers only, per the ruling on the issue: swarm services need
one forwarder per service container and get re-measured once this works.
Part of #3940.
The read policy and cert-auth role for each hive were written once, at
startup. On the deploy that surfaced this, the store was still coming
up, the pass logged its warning and moved on, and no hive could log in
until someone restarted the daemon — while cert auth answered "no chain
matching all constraints", which reads like a certificate problem
rather than a role that was never created.
The bootstrap unit in swarm-bao.nix lost the same race and won on its
retry 30s later. A daemon that boots alongside its store loses that race
routinely; on a normal boot it is the ordinary case.
The two passes fold into one `provision()` that logs in once instead of
twice for two loops over the same list, keeping policy before role since
the role names the policy. `ensure_hive_access` still awaits the first
pass, so a store that is already up leaves nothing deferred, and only a
pass that could not reach the store at all spawns the retry.
The retry is `config_pr::spawn`'s idiom from this same crate: an
interval task whose first tick is immediate. Its cadence and bound match
the bootstrap unit's — 30s, ~a day — because the two halves of one race
should not disagree about how long a wait is worth.
`Error::MissingEnv` is what keeps it from spinning forever: no `BAO_*`
set means a deployment that runs no store, where asking again changes
nothing, so it returns Ok. Everything else is retryable, including an
authority file that is not placed yet — the unit that writes it starts
alongside this one. Both cases previously landed in the same "not
managed here" line, so a store that was late looked exactly like one
that was never configured.
Per-hive failures keep their old behaviour: logged, skipped, Ok. A store
that refuses one hive's write refuses it again, so the next start really
is the right retry for those, and the module doc still says so.
Closes#4176.
mara (#4170): swarm-ui's wanted-state dropdown could only ever declare
up/offline/destroy, with no way to swarm-declare the existing hive-local
turn-loop pause (`hivectl agent pause|resume`).
`AgentState::Paused` is not a fifth peer of Up/Offline/Destroyed on the
power axis this enum otherwise answers — it's Up plus an orthogonal
turn-loop pause. `hive-c0re`'s `workers::wanted` reconcile loop now
decides the two axes independently (`decide` for power, the new
`decide_pause` for the marker), so a stopped agent declared Paused
converges with both a Start and a Pause in the same pass.
Known, deliberate limitation: a Paused declaration on an agent this
hive has never deployed only reaches Deploy this pass — writing the
pause marker into a harness dir that may not exist yet was judged not
worth the risk, so it converges on the next pass once the agent is
present instead.
swarm-ui's WantedMenu gains a fourth "paused" option (warning-tone
badge). No separate "resume" entry — selecting "up" from a paused row
already clears the marker via the same decide_pause path.
Pause/resume marker writes go through one shared
Coordinator::set_paused_by_name helper, used by both the interactive
dashboard pause/resume handlers and this reconcile loop, instead of
each duplicating the parse-name/write-marker/track-rescan shape.
swarm-ui's "offline" and "paused" confirm dialogs share one
confirmTarget state and one ConfirmDialog instead of two near-identical
copies.
Closes#4170
`write_policy` called `vaultrs::sys::policy::set`, which targets
`sys/policy/<name>` — the deprecated alias the store gates as a path of
its own. Every grant in this tree names `sys/policies/acl/hive-*`, so a
correctly-scoped controller was refused with a 403 and no hive read
policy has ever been written, on any deployment. The doc comment three
lines above the call already named the modern path; the code is what
moves to meet it.
vaultrs has no endpoint for that path (`grep policies/acl` over 0.8.0:
zero hits, against 8 for `sys/policy`), so this defines one over its own
endpoint machinery — which keeps the client's token header and `/v1`
prefix middleware rather than re-deriving them.
The alternative was to widen the grant to cover the legacy path. This
way needs no policy change at all: the deployed grant already permits
the write, so no store is re-bootstrapped and nothing is entrenched on
an alias upstream has deprecated.
Two tests pin the path and the body shape. The reason this survived
deployment is that nothing in the tree ever asserted either one.
Closes#4177.
mara (#4172): the components page was out of date against its own
stated rule ("a new primitive gets a section here the same day it's
added"). Dialog and ConfirmDialog are real standalone exported
primitives (AgentsPage uses both directly) with no demo section.
FormField deliberately stays excluded — its own comment already says
it's not a reachable primitive, just TextField/SelectField's shared
internal wrapper, so it was never meant to get one.
Also gave the Table demo's "detail" column a multiselect filter, so
the checkbox popover and reset-filters button — real, daily-used Table
behavior — are actually shown rather than an unfiltered grid.
The `prose lint (vale, errors)` CI gate fails on Microsoft.Contractions
at `setup.md:147`, in prose this branch adds.
Verified against the repo's own styles rather than assumed: with
`XDG_DATA_HOME=$PWD/.vale-data vale sync` (the ini's own note — sync
ignores `StylesPath` and writes under `$XDG_DATA_HOME`), the file
reports 0 errors, and a copy of it with the word put back reports that
line and nothing else. Without the sync, vale exits 2 on a missing
styles dir, which is a config error rather than a pass.
The bootstrap unit writes a policy granting `secret/data/swarm/agents/*` and
nothing creates that engine. A fresh OpenBao has no `secret/` — only a dev-mode
one does — so `swarm-controller`'s first credential write answers `no handler
for route "secret/data/swarm/agents/<agent>/matrix/<name>". route entry not
found.` Measured on the live host at 21:27:27Z; #4171.
`git grep` for `secrets enable`, `kv-v2`, `kv_v2` and `sys/mounts` returned zero
across the whole tree. Control, so the zero means something: `auth enable` in
this same file returns 2 — the same defect was already found and fixed once, for
the cert auth mount, with a comment that states the principle. This is the other
half of it.
The mount name is now bound once and interpolated into both the policy text and
the new step, because a grant and a mount that disagree is exactly the failure
being fixed.
Placed outside the client-CA block: the controller writes *through* this mount
regardless of whether anything can log in by certificate. `module-eval` asserts
that, since one indentation level decides it.
Grants, measured against a real openbao 2.6.2 rather than derived:
`-output-policy` asks for `sys/mounts/secret` create+update, and a token holding
exactly `sys/mounts` read + `sys/mounts/<path>` create/update enabled the engine
— **no `sudo`**, unlike `sys/auth/cert`. Negative control: the same token on an
ungranted path got 403, so the grant is what made it work. `setup.md`'s
documented policy gains those two.
Also from that session, each deciding how this is written: re-enabling an
existing path errors (exit 2), so this asks first like the auth mount does;
`secrets list -format=json` keys look like `"secret/"`, so the `case` idiom
ports over; and `kv put -mount=<p>` reports `<p>/data/...`, confirming v2 — the
prefix the policy grants and the client writes.
setup.md also drops a check that cannot work: it told the operator to confirm
with `bao read auth/cert/…`, which 403s because the host wrapper carries no
token. `systemctl status swarm-bao-controller-policy` needs no credential and
names the three success lines. The first-attempt-after-rebuild race is now
written down too — the store is still coming up, and the 30s retry is what
lands.
Refs #4171.
Both claims are load-bearing prose, and both are wrong in a way nothing in
the tree reads (#4168).
swarm-nats says the queue is "reachable from every agent container on the
hive" because the container shares the host netns — in a comment, and again
in the operator-facing `calloutUserPublicKey` description, which renders into
the options doc. Agent containers do not share it: `PRIVATE_NETWORK=1` is
written unconditionally (hive-priv/src/main.rs, and hive-priv-sock says
"isolation is the only supported mode"), and hive-network.nix states the
shared-netns mode was removed. The bridge firewall opens 53/67/80/443 plus
`exposeHostPorts`, whose only consumer tree-wide is otel — the queue's port is
in none of them, and no gateway route exists either (`grep -c nats` in
hive-gateway/default.nix -> 0; control `forge` -> 3, so the zero means
something). Its actual clients are host-side: HIVE_C0RE_NATS_URL and
SWARM_CONTROLLER_NATS_URL, both 127.0.0.1 on a single-host swarm, plus each
remote hive dialling a routable address.
swarm-victorialogs says the ingest endpoint has "no authentication of its
own". Upstream offers Basic Auth via -httpAuth.username / -httpAuth.password
(and -metricsAuthKey / -deleteAuthKey / -pprofAuthKey); this module sets none
of it. "The software offers nothing" and "we configure nothing" send a later
reader to different places, so the wording now says the second one.
Neither conclusion changes. The queue must still refuse everyone until the
callout responder exists, and the logs endpoint must still be pinned to
loopback — only the reasons were false.
Checked while here: swarm-authelia's identical "no authentication of its own"
is TRUE (upstream's telemetry.metrics has exactly enabled, address, buffers,
timeouts), and otel.nix's "reachable from agent containers and nowhere else"
is true and better-founded than it claims — the receiver binds the bridge IP,
not just a firewall hole.
Refs #4168.
The bootstrap block still described a one-shot unit "inside the store's
container". That stopped being true when the unit moved, and a merged
change that leaves the docs asserting the old topology is worse than one
that never moved it.
Also retires the "only run once, didn't go all the way through" note. It
has run twice against a live store now and failed both times — but for
its own reasons, not the grants': it could not reach the store from where
it ran, so neither attempt exercised a single one of them. The grants are
still derived from `-output-policy` rather than proven, and the warning
now says that directly instead of implying a partial test.
`swarm-bao-controller-policy` creates the `swarm-controller` policy and
cert-auth role — the credential every hive logs in with. It has never
succeeded on any deployment, and the reason is where it ran.
Inside the container it had neither of the two things the store demands.
Its `BAO_ADDR` was the public DNS name, which from that netns resolves to
the hive bridge: `dial tcp 10.42.0.1:8200: connect: connection refused`.
And every API listener carries `tls_require_and_verify_client_cert`, while
`tlsDir` holds the server's leaf and the CA that signs clients — no client
identity at all. Fixing only the address moves the failure one hop.
The comment above the unit asserted the opposite — that in there the store
is "reachable without a client certificate at all, which is the point". The
listener config decides that, and says otherwise. That belief is what put
the unit in the container, so it goes with it.
On the host all four coordinates already exist: `baoCli` carries the
address, the CA, the certificate and the key, so the unit needs no
`environment` block at all. `bootstrapTokenFile` was always a host path —
the container only saw it through a bind mount. Nothing new crosses the
boundary; the mount gets no wider.
The retry bound is resized with it. 10 attempts at 30s is five minutes,
and under `seal = "shamir"` an operator unseals by hand, so it would give
up before a human arrived — permanently, because `start-limit-hit` does
not self-heal. That is the same silent no-bootstrap this issue is about.
2880 × 30s covers a day, inside a 25h window.
module-eval follows the unit to the host and gains an arm asserting it is
NOT rendered inside the container: the move is the fix, so the side it
landed on is worth pinning.
`StartLimitBurst` sat in `serviceConfig`, so it rendered into `[Service]`,
where systemd silently ignores it — the unit retried every 30s forever.
Measured on a live store: the journal reports `restart counter is at 18`
against a burst of 10.
This repo already states the rule and pins it with a test:
`hive-priv/src/main.rs` renders its drop-in with `StartLimit*` under
`[Unit]` and says why — "systemd silently ignores them under `[Service]`,
so a bound that moved sections would look configured and do nothing".
That is exactly what happened here, in another module.
Moving the burst alone would not have fixed it. systemd's default window
is 10s while `RestartSec = 30`, so at most one restart falls inside it and
a burst of 10 is unreachable; the interval has to exceed `RestartSec` times
the burst. 600 matches the value hive-priv already uses.
Uses the NixOS service-level options rather than a hand-written
`unitConfig`: nixpkgs renders `startLimitBurst` / `startLimitIntervalSec`
into `unitConfig` itself (`nixos/lib/systemd-lib.nix`), and `hive-ci.nix`
already sets `startLimitIntervalSec` that way.
The module-eval case asserts placement where nixpkgs puts it, and that
`serviceConfig` does not carry it — so moving it back fails the build.
The host now has a wrapped bao (#4154, deployed) carrying the store's
address, CA and client cert, so the init step is a bare command there
instead of a root-login hop with an SNI override. The container route
stays as one sentence explaining why it needs the two extra pieces.
The delete step said "once that has run" without saying what -- the
rebuild, or the unit succeeding. It now names the check that settles it,
because deleting the token before the role exists leaves the unit
skipping forever and looks exactly like a store nobody bootstrapped.
Closes#4162
The doc-comment sweep changed --help text, so the generated reference went
stale and hive-forge-docs-fresh caught it. Regenerated with the command the
check itself prints; the diff is five lines, all propagations of the sweep.
Kept as its own commit rather than an amend: argus approved d090a9fb, and a
force-push clears both the verdict and the review request.
Refs #4155
docs/tools/forge.md already listed the nine renamed verbs as removed, then
used them ~30 more times in pasteable blocks. Sweeps every occurrence a
reader would type, including three runtime messages that told the user to
run a verb the same binary rejects.
The renamed-verb list itself keeps the old names; it is what documents them.
Refs #4155
mcp.md called it `hivectl choom`; the verb is `hivectl agent <name>
choom`. observability.md cited `hivectl shell`, which has never existed
at any level — the sentence only needs a shell, so it names one instead of
guessing which command an operator reaches for.
Found by subtracting the generated hivectl-cli.md's command headings from
every hivectl citation in docs/. Re-running it after the fix drops the
candidate list from 16 to 14, losing exactly these two.
Closes#4159
Reading a role out of the store took four round-trips of environment
guessing: the certificate has no IP SAN so loopback cannot verify, the
DNS name resolves to the bridge from inside the container, and the CA
the client needs lives on the host, which had no `bao` at all.
The wrapper carries the address, the server CA and — where the PKI glue
minted one — the reader's own leaf, so cert-auth login needs nothing
typed. `--set-default` throughout: an operator pointing BAO_ADDR
elsewhere still wins.
Only the wrapper reaches PATH. `wrapProgram` renames the real binary,
so there is no unwrapped `bao` to reach by accident, and the module-eval
case asserting the package's absence is what keeps a later "install it
too" from undoing that.
"On the host that RUNS the store" is ambiguous in the deployment we
actually ship: openbao runs inside the `swarm-bao` container, so the
host and the container are different vantages with different working
addresses, and the instruction reads as either.
From inside the container neither obvious address works — the server
certificate is issued for the store's DNS name and carries no IP SAN,
while that name resolves to the bridge rather than to the container. The
fix is an SNI override: verify the name, connect on loopback. The host's
own address is stated beside it so the reader can pick a vantage instead
of discovering the constraint from a TLS error.
`clientCn` fell back to `cfg.domain` when `hiveName` was unset. That branch
cannot run: `hive-network.nix` asserts `hiveName != null` under
`mkIf services.hyperhive.enable`, and this file's `config` is gated on the same
predicate, so any host that evaluates the conditional has already failed the
assertion.
Worse than dead, it read as a second supported spelling of a hive's identity —
which is what a cert-auth role matches on. It was not even the hive's own
domain: `cfg` here is `services.hyperhive.swarm.bao`, so the fallback resolved
to the store's address, one string shared by every hive in the swarm and the
same CN the server leaf carries.
Reading the option directly matches what other modules needing the name already
do (`hive-c0re/environment.nix`).
A hive holds an mTLS pair and a policy naming what it may read, and still
cannot log in: nothing creates the role that maps its certificate to that
policy. The one pre-shared credential in the system therefore buys no
access.
Minting happens here rather than in nix, which was the first plan. Nix
mints from the store's own container, and that path is gated on the
bootstrap token -- so onboarding a hive later would mean placing the one
genuinely pre-shared secret again. Doing it from the controller costs a
public certificate authority as an input and makes the bootstrap token
one-time.
A startup pass, not a hook: the hive list is loaded once and a config
change means a redeploy, so the roles are as static as the list. Only the
policy is derived from something that moves.
The subject is the hive's name because glue-bao-tls.nix mints a hive's
client leaf with its name as the CN, and cert auth matches on that.
Per-hive failures are logged and skipped, matching the queue, bridge and
forge connects above it: a controller whose store is unreachable still
serves everything else, and the next start retries.
Not covered by a test: ensure_hive_roles is IO from end to end, and the
seam that would make it assertable is the one the read-grant sink already
has. Said here rather than implied by a green suite.
Creating a hive's cert-auth role means writing the authority into the role
by value -- the store matches a presented certificate against the role's own
copy -- and nothing gave this daemon that file.
Named separately from deploy.bao.clientCaFile rather than read off it: that
option is the store's, saying which readers the store trusts on the host
that runs it, while a controller runs anywhere. The glue module supplies it
where the two are co-located, which is the same split baoClientCertFile
already makes against the hive reader's leaf.
Gated on the identity as well as the CA. Without a leaf there is nothing to
write a role with, so the file would reach a daemon that cannot act on it.
The module-eval arm needed a fixture of its own: a deployment that
self-signs both ends points clientCaFile and serverCaFile at one file, so on
the existing fixture the two authorities are the same string and wiring
either into the other's slot passes. controllerTwoCas is where they differ.
Per mara's screenshot report (agent-terminal icons not aligned in the
first column) — and her explicit follow-up steer on the first version of
this fix: 'dont do it by offsets at all, it should be part of the layout
that they align.'
Root cause traced first, not guessed: .row-glyph relied on inheriting
text-indent: -1.4em from .live .row to paint its glyph in the reserved
prefix slot; details.row > summary separately reset text-indent: 0 for
its own hanging-caret needs, which also zeroed the inherited value for
the icon nested inside it (indent inherits by computed value, not by
rule). Two independently-computed offsets that had to agree by hand,
and silently didn't.
Replaced the whole offset scheme with a real structural fix: every row
is a 2-column CSS grid (icon column, width from one shared
--row-icon-col custom property, then content column), and a details
row grids its own <summary> with the exact same grid-template-columns
value instead of griding itself (so its <pre> body still stacks full
width below, not squeezed into column 2). Icon and content are placed
by explicit grid-column, not auto-placement inference, so an icon-less
row's lone child still lands in the content column. .row-content is a
new wrapper class (Row.tsx, terminal.js's row()/mutableRow()/
placeholder()) giving that content an element the grid can target by
class - a DocumentFragment (what linkify() returns) doesn't persist as
a node once appended, so without an explicit wrapper there was nothing
for the grid to place.
Caught and fixed a second real bug while building this for real (not
just reasoning about the CSS): the first draft's details.row { display:
block } had lower specificity than .live .row's display: grid and never
actually applied, squeezing <summary> into the row's own 1.4em icon
column and wrapping its text one character per line. Needed
.live details.row to out-specify it.
Verified with a headless-chromium render of all 6 row shapes (flat
icon / flat icon-less / flat markdown-body / details icon / details
icon-less / a long-wrapping flat row) - all align and wrap correctly.
Also verified via the earlier Range.getBoundingClientRect() measurement
(glyph paint position, not just the element's own box): diff 0.00 for
both this and the previous fix, but only this one is structural rather
than two numbers that happen to still agree today.
Per mara's issue: tables should remember their filters (with a reset
button), the labels filter should be multi-select, and any filter should
support negation (search vs exclude).
All in the shared Table component (ui/table/Table.tsx), used by
AgentsPage/HivesPage/IssueReportPage:
- filters now persist via the same useLocalSetting hook IssueReportPage
already used for its own state, keyed by a new required storageKey
prop (required, not optional, so no caller can forget it and every
table gets persistence for free)
- a small 'reset filters' button clears every column's filter at once,
shown only when at least one is active
- new filterMode: "multiselect" (+ a filterValues extractor, alongside
the existing single-value filterValue) renders a checkbox list and
matches on any overlap - IssueReportPage's own bespoke label-checkbox
sidebar is folded into this instead of staying a second, separate
filter mechanism
- a negate toggle ('exclude') sits under every filter mode's control,
applying uniformly to text/select/multiselect
Verified: tsc --noEmit and the esbuild bundle both clean.
hive-rules.md: no #NNN tags in code, prose only. The two mentions in
the new prose-lint-errors job's comments were exactly that -- reworded
to describe the split without a ticket number.
mara, on #4128: add a CI step (separate from the existing one) that
only flags error-level vale hits, so it can be made a required check
without also gating merges on the pre-existing warning/suggestion
backlog. The full job stays as-is, advisory, for that backlog.
Also brought docs/scheduler/ci.md's CI-checks table back in sync with
reality (it was missing the doc-pointer lint job already; now lists
both vale jobs and their required-check status).
mara disagreed with exempting this section from #4041's ruling.
'Backend' here meant 'which LLM API provider', and the doc already
uses 'provider' for the same concept a few lines down ('provider-
specific model string') - so reword to that instead of carving out
an exception. Drops the inline vale suppression comment entirely.
Per #4128 (mara: allow-everywhere false positives go in a central
list, otherwise fix in source). Testing surfaced better fixes than
the plan posted on the issue:
- 5x Microsoft.Contractions 'that is' idiom false positives: adding
the missing comma ("that is, ...") both reads better and satisfies
the rule's own negative-lookahead, so no suppression is needed at
all. Fixed in docs/integrations/forge.md, docs/tools/forge.md,
docs/tools/hivectl.md, docs/web-ui/dashboard.md, and
swarmctl-cli.md's generated source (swarmctl/src/main.rs, doc
comment regenerated via markdown-docs).
- persistence.md's 'is not' matching inside 'is nothing': reworded to
'there'\''s nothing' rather than add any exception -- dodges the trap
and is a genuine contraction besides.
- ca.md's 'it is' matching inside the already-correct 'it isn'\''t':
tried a central .vale.ini TokenIgnores entry first per the
allow-everywhere framing, but testing against the real file (not
just a synthetic snippet) found it silently fails to suppress
whenever markdown emphasis syntax appears earlier in the same
file -- an offset-drift bug in how Vale applies TokenIgnores, not
a config mistake. Reworded to "it'\''s not" instead, same fix
shape as persistence.md.
- config.md's 3 genuine Microsoft.Avoid 'backend' exceptions (already
flagged and accepted on #4139 -- an actually-pluggable LLM API
provider, matching the nix option's own name, not one internal
system to name): scoped inline vale suppression around just that
section, since this one really is context-specific rather than a
rule bug.
Verified: fresh 'vale docs/ --minAlertLevel=error' is 0 errors AND
0 warnings (was 10 errors). nix fmt 0 changed beyond the edits
themselves. pre-push lints (tracker-tag/comment-block/doc-pointer)
clean. cargo clippy -p swarmctl -- -D warnings clean. Diffed the
regenerated swarmctl-cli.md against the old copy to confirm only
the intended line moved.
Per mara's ruling on hyperhive#4041 (Microsoft.We): keep the rule enabled,
same treatment as the 'backend' rewrites. Traced all 21 genuine hits (4
gateway.md 'Let's Encrypt' hits are a substring-match false positive, left
alone) to their actual referent: some name a specific component already
established nearby in the same doc (forge_notify, hive-github-notify's
poller, hive-forge, hive-agent/the harness, colors.css, the dashboard),
others were pure filler that adds nothing once dropped.