Commit graph

4,318 commits

Author SHA1 Message Date
iris
b628af81de docs: fix the one passive-voice hit in docs/crates/README.md
Second doc-directory of hyperhive#4042's Passive pass (see #4098 for
the first batch's full read-every-hit reasoning). Only one hit in this
whole directory: "Every page is generated at build time
(nix/packages/reference-docs.nix, hyperhive#3051)" -- a genuine catch,
same shape as #4098's swarm-SSO rewrites -- the actor is already named
parenthetically in the same sentence, so naming it as the grammatical
subject too is strictly clearer: "nix/packages/reference-docs.nix
(hyperhive#3051) generates every page at build time".

Verified: vale docs/crates/README.md before/after -- 1 -> 0 warnings.
2026-09-08 12:15:53 +02:00
iris
bfb9636d52 docs: fix genuine passive-voice hits in getting-started/setup.md
First doc-directory of hyperhive#4042's Passive pass (705+ hits across
docs/, genuinely mixed unlike TooWordy -- needs a real per-hit read,
not a dictionary shortcut, so this is going doc-directory by
doc-directory in small PRs, per the plan posted on the issue).

Read all 13 flagged hits in this file in context, not just the flagged
word. 3 were genuine catches with a real active-voice improvement and
a nameable actor:

- "the human operator's own forge/matrix account is created via swarm
  SSO" -> "swarm SSO creates the human operator's own forge/matrix
  account" (x2, identical sentence shape for both accounts) -- swarm
  SSO is a nameable, specific actor already named later in the same
  sentence, so naming it as the subject too is strictly clearer, not
  just different.
- "Do this before anything is pointed at it" -> "Do this before you
  point anything at it" -- matches the doc's own established
  second-person imperative voice used throughout ("Do this", "Put the
  token's value", "Delete the file"); the passive here was the odd one
  out, not the house style.

The other 10 are legitimate passives, left alone:
- Generic/unspecified-actor statements ("is needed", "is bound", "is
  issued", "been run") where forcing a subject would either invent an
  actor the doc never established or read worse than the original.
- Security/architecture invariants ("No forge admin token is stored in
  any agent state dir", "Telemetry ingest is authenticated per hive")
  -- "No X is Y" / "X is Y" is the standard idiom for a guarantee
  statement in security docs, not a clarity problem to fix.
- "operator-only surfaces ... are gated on that group" -- borderline
  (could name the group as subject), judged idiomatic access-control
  phrasing rather than genuinely clearer active, but flagged as the
  closest call in this batch.

Verified: vale docs/getting-started/setup.md before/after -- 13 -> 10
write-good.Passive hits (exactly the 3 rewritten), the pre-existing
unrelated alex.Suicide hit on "hang" (line 51, untouched) still
present and correctly out of scope for this pass.
2026-09-08 12:15:39 +02:00
iris
63f3afae3e swarm-ui: portal the filter popover past the table's own clip
argus's review (reproduced, not speculative): .ui-table-scroll sets
overflow-x: auto with overflow-y left unset. Per the CSS overflow
spec, an axis left visible computes to auto too once the other axis
isn't visible -- so this box silently clips vertically as well as
horizontally. The popover was position: absolute; top: 100% under its
header <th>, itself inside .ui-table-scroll -- on a table shorter than
header-plus-popover (a small hive's roster, or any table narrowed by
an existing filter), the popover got cut off at the scroll box's own
bottom edge, with a stray vertical scrollbar as the visible symptom.

Computes the popover's viewport position from the anchor <th>'s own
getBoundingClientRect() and renders it via a portal onto
document.body, position: fixed -- escapes .ui-table-scroll's clip the
same way any position: fixed element escapes an ancestor's overflow
(neither .ui-table-scroll nor .ui-table establishes a new containing
block). Recomputes on scroll (capture-phase window listener, the
standard technique for detecting scroll on a nested scroll container
without binding to every ancestor by hand) and resize while open, so
the popover stays anchored rather than only positioning once at click
time. createPortal comes from preact/compat, already resolvable
through the existing preact dependency -- no new package.json entry.

Verified against argus's own repro shape: a 1-row table, scripted
click on the filter icon (same real-DOM-event technique as this PR's
first round), screenshot shows the popover rendering fully rather than
clipped, no stray scrollbar.
2026-09-08 12:14:56 +02:00
iris
e234afa26a swarm-ui: table filters move to a per-header icon + popover
Replaces the permanent filter-row under Table's headers with a small
filter-icon button in each filterable column's own header cell. The
icon fades in on header hover/focus via a CSS opacity transition, or
stays visible outright once that column actually has a filter set
(mara: "instead of a filter row, add little filter icons on header
hover with fade in out animation ... when a filter is set, the filter
icon does not disappear"). Clicking it opens a small anchored popover
directly under the header holding the exact same filter control
filterMode already provides (text input or select) -- the underlying
filter mechanism from hyperhive#4088 is unchanged, only where the
control lives moved. Close-on-outside-click/Escape mirrors the
contract Dropdown already gives its own popover, adapted to a shared
listener across every column instead of a ref per column since only
one popover is ever open at a time.

New FilterIcon in @hive/shared's icons.tsx (a plain inline SVG funnel,
same Feather/lucide-style shape as the existing GearIcon/LinkIcon) --
found and reused that pattern rather than reaching for an emoji glyph,
matching the documented reason those two exist as SVG in the first
place (mara, on the old emoji icons: inconsistent size/weight across
platforms).
2026-09-08 12:14:56 +02:00
iris
17d22f9e16 swarm-ui: filter the issue report by title, assignees, blocked
Three columns gain Table's filterValue/filterMode (the mechanism
hyperhive#4088 added): title (text, substring search -- there was no
way to search by title text at all), assignees (text, not select --
a row can carry more than one assignee and Table's select mode
matches one whole string per row exactly, so substring search over
the joined string is the shape that actually fits multi-value data),
and blocked (select, synthesized "blocked"/"not blocked" strings --
distinct from the existing "hide blocked" toggle, which only hides
blocked issues and has no way to show only them).

Deliberately not touched: repo (redundant with the existing repo
SelectField), labels (redundant with the existing label chip
multi-select -- chips are the better UI for a bounded label set
anyway), the three numeric columns (no clean filter shape, already
sortable). The existing hand-rolled sort (SortHeader, useLocalSetting-
persisted) is untouched too -- migrating it onto Table's own sortBy
would drop the localStorage persistence this page specifically wants,
and Table doesn't expose controlled sort state to a caller today. The
two layers compose without conflict: Table's own filter/sort runs
over whatever rows it's handed, which is already this page's own
filtered+sorted array.

Scoped on the issue first, including this exact reasoning, before
writing any code.
2026-09-08 12:14:42 +02:00
iris
8d8ac72d98 swarm-ui: size dialogs to their own content width
Dialog previously forced every caller to a fixed width: 90vw; max-width: 44em
shell regardless of content — fine for the create-agent form's wide
two-panel layout (the only caller until now), but a ConfirmDialog's short
paragraph then wrapped at its own narrower max-width while the shell stayed
the wide default, leaving a dead gutter before the close button (mara filed
a screenshot showing exactly this).

First pass added a narrow prop/second CSS class for ConfirmDialog to opt
into a smaller fixed width. Review pointed at the actual root cause one
level up: width: 90vw is a forced width, not a cap — a native dialog's own
UA default is width: fit-content. Switching .ui-dialog to
width: fit-content; max-width: min(90vw, 44em) lets each caller size to its
own content naturally: the create-agent form still hits the 44em cap (same
rendered width as before, confirmed via screenshot), ConfirmDialog's
paragraph settles at its own intrinsic width with no extra prop, no second
CSS class, and no second hardcoded number to keep in sync with the first.
2026-09-08 11:08:01 +02:00
iris
4544c458eb swarm-ui: bounded-domain table filters become dropdowns
Table columns whose value only ever comes from a small closed set
(freshness, wanted, hive) get a <select> in their filter-row cell
instead of a free-text input, populated from the distinct values
present in the currently-loaded rows plus an "any" option, matched by
exact equality instead of substring. Free-text columns (name, the
agent's own status message, config PR, hive domain) are unchanged.
2026-09-08 00:49:36 +02:00
atlas
a204b5f457 grafana: show which sources are shipping, not just that the store is up
The log-store board reports rows ingested, disk size, free disk and errors —
every one of them a fact about VictoriaLogs itself. None of them can say
whether a particular unit or host is contributing, so aggregate ingestion
reads healthy on host-tier units while a whole tier ships nothing, and there
was no way to tell those apart from a dashboard.

Add four panels to that board rather than a second one. Every other board here
is per service — agents, authelia, openbao, forge, queue, the two stores — so
a second board about the same service would have made an operator guess which
of the two answers their question.

Nothing in the new panels names a unit: both breakdowns discover their rows
from the data, so a source that starts shipping appears without an edit, and
one that never existed is simply not there.

The ungrouped total is a control, not a summary. An empty breakdown renders
the same whether the query is malformed or the source genuinely never shipped;
with the total beside it, nonzero-and-empty is a broken query and
zero-and-empty is an empty store. Being on one board buys a second reading it
could not have alone: that total and `Log rows ingested` are the same quantity
measured by querying and from the store's own metrics, so the two disagreeing
means rows arrived that no query can reach — which is the failure this
pipeline actually had once.

The two breakdowns are bargauges rather than tables. A `stats` query returns
one frame per group, and a table panel renders one frame at a time behind a
series picker; bargauge consumes multi-series natively, for the same reason
the timeseries beside it always did. The alternative was a table plus
labelsToFields plus merge plus organize — three assumptions where this needs
none, in a spot I cannot render to check.

Series are named by their label rather than `rows{_SYSTEMD_UNIT="x.service"}`,
which also cleans up the timeseries legend. The datasource supports
legendFormat; its query editor's own placeholder is `{{label}}`.

These are the first panels to query the logs datasource at all; the other
eight boards are prometheus, including this one until now, which reads
VictoriaLogs' self-metrics out of VictoriaMetrics. So `renderDashboard` grows
a `@logsDatasourceUid@` substitution alongside the metrics one. The binding it
points at already existed, and its comment claiming a dashboard panel named it
only becomes true with this commit.

The uid is unchanged, so existing links and bookmarks still resolve; only the
title widens to match what the board now covers.

The query model was read out of the plugin in the store rather than guessed:
`queryType` is one of hits/instant/logs/stats/statsRange, and Stats/StatsRange
are the two that consume `expr`. The stream field names come from the module
that builds them — swarm-otel.nix's `_stream_fields=_HOSTNAME,_SYSTEMD_UNIT`.
The queries themselves are confirmed against the live store: mara ran the
by-unit one over seven days and it returned rows.

Refs #4084
2026-09-08 00:43:23 +02:00
atlas
0dd807062c hive-c0re: deliver a matrix credential through hive-priv, not by writing it
`deliver` read the value out of the secret store and wrote it itself, as the
`hive-core` user, at 0600. The file lands in a directory owned by the agent,
so it arrived owned by `hive-core` — the agent's matrix daemon woke on it
appearing and could not read its own credential. `priv_client::write_agent_
matrix_token` already existed and already had two callers; this was the one
path that did not use it.

hive-priv now owns the filename too, so the name the daemon's path unit globs
for is decided in one place instead of being built identically in two.

That move exposed a disagreement worth fixing rather than routing around.
The secret store accepts `[A-Za-z0-9_-]` for an account name; hive-priv's
`validate_name_chars` accepts lowercase, digits and hyphen only. An account is
an attribute name in `hyperhive.matrixAccounts`, typed `attrsOf` with no
charset constraint, so `Ops_Relay9` is a key an operator can already have
written — and it would have read out of the store and then failed to land.

So hive-priv grows `validate_account_name` rather than widening the existing
one: an agent name is an `Ident` and lowercase by design, an account name is an
attrset key, and one validator serving two name domains is what let them drift.

The test that caught this came from `credential.rs`, which used to build the
path. It moves to hive-priv with both of its controls intact, because the
controls are the point — they assert which names must be ACCEPTED, and a
validator narrower than the store's passes every rejection case. A second
moved test pins the `matrix-token` prefix where the name is now built; the
old one would have kept passing while asserting a function that no longer
decided anything.

Refs #3726
2026-09-07 23:58:11 +02:00
damocles
66c3138dd1 hive-c0re: grant hive-admin group a polkit rule for choom 2026-09-07 23:27:15 +02:00
atlas
f49eef299b hive-priv: publish an agent's credential by rename, not in place
`write_agent_dir_file` opened the final path with O_TRUNC and filled it, so
the file existed empty before it held anything. Several of these paths are
watched, and the kinds differ: `nix/agent-modules/matrix.nix` starts the
agent's matrix daemon on `PathExistsGlob = ".../matrix-token*"`, which fires
on the file *existing* — the O_CREAT moment, ahead of the content.
`nix/agent-modules/forge.nix` uses `PathChanged` and has no such window.

Write to a temp in the same directory, chown that, then rename it into
place. The chown stays ahead of the publish for the same reason the write
now does: the file must never be visible under its final name while still
root-owned.

The temp is dot-prefixed rather than suffixed, because `matrix-token-x.partial`
matches the daemon's own glob — a suffix would wake it on exactly the empty
file the rename exists to hide.

`write_state_file_nofollow` is deliberately unchanged. Its O_NOFOLLOW and
fchmod/fchown-on-the-fd properties are what make a root write into an
agent-owned directory safe, and its existing tests are the control on them;
the caller is the part that needed to change.

That does move the leaf validation, though: the helper now only ever sees the
temp name, which is a plain component whatever the caller passed. So
`ensure_plain_filename` is extracted — it was already duplicated in
`delete_agent_state_file` — and the caller's own name is checked with it.

Whether the empty-file window is reachable in practice is not measured;
`matrix.nix`'s documented skip condition is "missing" rather than "empty", so
a first provision could plausibly lose one. This makes the question moot
rather than answering it.

Refs #3726
2026-09-07 22:54:35 +02:00
iris
6f57b1f57c swarm-ui: one badge+dropdown for wanted state, not multiple buttons
mara, hyperhive#4079: "agent wanted state is multiple buttons insteaf
of a badge with dropdown ... same pattern as agent term badges with
dropdowns". The wanted column used to be a toggle badge plus a
separate quiet destroy badge, stacking under each other in the narrow
column. Replaced both with one WantedMenu badge that opens a Dropdown
with the three explicit states (up/offline/destroy) -- the exact
badge-triggers-a-dropdown shape the per-agent terminals StatusChips
already uses (and swarm-uis own ComponentsPage already demos with
sample data), built from the same shared Badge/Dropdown components.

"up" still declares straight away with no confirmation; "offline" and
"destroy" still go through the existing ConfirmDialog modals unchanged
-- only the trigger moved, the confirm behavior for the two directions
that already had one is untouched.

Explicit dropdown options also fix a real bug the old toggle had:
mara also asked "when no state is declared, i want to set it to
online" -- the old toggle inferred a target as the opposite of
snapshot.running for an undeclared row, so a click on an
undeclared-but-running agent silently declared it offline rather than
making its actual state explicit. The dropdown just lets "up" be
picked directly regardless of any inference, which is what she is
asking for -- flagging this reading explicitly in case an actual
one-time migration (auto-declaring every currently-undeclared agent
up) was intended instead, which this does not do.

Added a shared .ui-dropdown-anchor utility class to Dropdown.css --
this is the third near-identical "position: relative wrapper for a
badge that opens a Dropdown" (after agents own StatusChips.css and
swarm-uis ComponentsPage.css), so a new caller should not reinvent a
fourth copy. Left the two existing ones alone rather than migrating
them as a drive-by.

Verified with a local esbuild build + a throwaway mock /api/agents/status
server, screenshotted headlessly: the wanted column now shows exactly
one badge per row instead of stacked badges.
2026-09-07 22:31:44 +02:00
atlas
c18aee8f74 bao: mint the controller's leaf, and point the controller at it
glue-bao-tls.nix signs a third leaf. It is minted whether or not a
controller runs here, because the case it serves is the one where it
does not: a controller elsewhere needs a leaf from this CA and cannot
sign one, so issuing it here turns "obtain a certificate out of band"
into "copy this file".

glue-controller-bao-identity.nix holds the pairing and nothing else --
which paths this host's controller reads. Gated on the leaf existing
rather than on deploy.bao.enable, so a controller on the store's host
and one three networks away with an out-of-band leaf get the same
wiring; gating on the store would have made the co-located case the
only supported shape. The directory comes from deploy.bao.clientCertFile
rather than repeating glue-bao-tls.nix's literal, so moving the PKI
moves both.

module-eval gains three cases and two fixtures, because nothing asserted
the PKI script before: an earlier commit added a leaf to that rendered
unit and left the derivation unchanged. The fixture's CN is deliberately
a value no default could supply, so "the role and the leaf both carry
it" says they read one option rather than that both happen to say
swarm-controller.

Gates: 62 module properties hold (59 before, plus these three), on a
derivation hash that actually moved -- this suite is a cache hit when
only fixtures change, so an unchanged hash would have meant the cases
never ran. nix fmt clean, all three scripts/check-*.sh exit 0.
2026-09-07 22:24:42 +02:00
atlas
a5dc62cecd bao: make the controller's CN an option, and give it cert options of its own
Two halves of one interface that currently exists in one file only.

The CN was a `let` binding in swarm-bao.nix. Whatever mints the
controller's leaf has to spell it identically, and that lives in another
file, so a literal in one place is an interface with no name. It becomes
`deploy.bao.controllerCommonName` -- under bao because it is a property
of the role this module writes, not a credential belonging to the
controller.

The certificate gets `deploy.swarm-controller.baoClientCertFile` /
`baoClientKeyFile` rather than reusing `deploy.bao.clientCertFile`. That
one means "this host as a reader" and carries the hive's name, while the
controller's policy lets it create roles for every hive; one certificate
serving both would hand that power to whatever else reads the store
here.

Both default to null. The glue that mints the leaf sets them with
`mkDefault`, the same way glue-bao-tls.nix already supplies the reader's
paths -- so the controller module names no path of bao's, and deleting
the glue leaves a controller that takes operator-provided ones.

Nothing reads the new options yet; the minting half is the next commit.
`module-eval`'s derivation is unchanged, which is the honest result for
a change that adds options without altering any asserted value -- it
proves the tree still evaluates, not that anything behaves differently.
2026-09-07 22:12:27 +02:00
atlas
6de51b7be4 setup: drop two vale errors my own callout introduced
`Microsoft.Contractions` fires on "is not" and "it is", and both were in
the sentence I added about the role having no consumer yet. Reworded so
neither appears, and the passive construction goes with them.

The vale job is red on every PR by design -- ci.yml says so, the docs
tree has a standing backlog against these rules -- which is exactly why
"vale is red" was not a safe thing to wave through. Checking per line
rather than per job is what found these.

Verified with the invocation ci.yml uses:
`XDG_DATA_HOME=$PWD/.vale-data vale sync && vale docs/getting-started/setup.md`
-> synced 3 packages, 0 errors, 14 warnings. The 2 errors CI reported at
103:39 and 104:7 are the presence control: same rule, same file, prior
commit.
2026-09-07 22:02:33 +02:00
atlas
f88c0d09ce module-eval: move a stranded comment back to the case it describes
Four lines about "no store here, so no bind mount and no unit" sat
above the swarm-ui case, which they have nothing to do with. The case
they describe -- a bootstrap token on a host that runs no store -- had
no comment at all, so this is a move rather than a deletion.

Reworded on the way, because it opened with "the arm that makes the one
above mean something". A comment that names a POSITION retargets when
the file is reordered, which is how it came loose in the first place;
it now names the granting-unit cases.

Noticed while adding a case directly above it, which opened with the
same sentence.
2026-09-07 21:56:20 +02:00
atlas
8bd41fd84f bao: enable cert auth and give the controller a role for its policy
The policy the granting unit already writes grants paths under
`auth/cert/certs/*`, and nothing in the tree creates that mount. Every
certificate login therefore fails against a path that is not there --
the controller's own, and the per-hive ones it is meant to issue
against the same mount.

Same unit, same bootstrap token: check whether cert auth is mounted,
enable it if not, then write a role binding CN `swarm-controller` to
the `swarm-controller` policy.

Idempotency is a read rather than a tolerated error. `auth enable`
fails on an existing mount, and recognising that would tie a rebuild to
an error string no run of this store has ever produced, so the unit
asks `bao auth list` and mounts only on absence. That read is why the
token policy in setup.md gains `sys/auth`.

Each grant came from `bao <cmd> -output-policy`, which prints what a
command requires without running it -- the same way controllerPolicyText
was derived. Enabling an auth method needs `sudo` on `sys/auth/cert`,
which the documented token did not have.

Gated on `clientCaFile`, not on the token alone: `swarm-bao-certs`
installs `client-ca.pem` only under that condition, and a role's
`certificate=` has to name a real CA. The policy write, which needs no
CA, is unchanged in that case.

Nothing can present a certificate for this role yet -- the only client
leaf the tree mints carries CN = the hive's name -- and none of this has
been run against a live store. Both are stated in setup.md.
2026-09-07 21:52:06 +02:00
atlas
df18d3d4d7 forge: move the forgejo package to deploy — slice 10 complete
The last of the twelve. `swarm.forge.package` becomes
`deploy.forgejo.package`, beside the `behindGateway` / `mirrors` /
host-secret options that were already there.

⚠️ `deploy.forgejo`, not `deploy.forge`. The forge joins the controller
and the UI as a mover where the two sides simply do not share a name,
so the shim comment says which one.

Four readers, and one of them is why an anchored grep was the wrong
instrument: `:678` reads `cfg.package.data`, a sub-attribute, which
`cfg.package}` and `cfg.package/` both miss.

Three copies of the same sentence claimed the package stays under
`swarm.forge` — `deploy.nix`, this module's own `deploy.forgejo`
header, and `docs/integrations/forge.md`. The docs one is outside
`nix/` entirely. A namespace split gets described wherever the split is
explained, which is not only where the option is declared.

`forgeOldPath` gains the old path, and its comment's count of the
host-side options goes five → six with it.
2026-09-07 20:46:38 +02:00
atlas
ce4d5187a9 deploy: two comments still say a package stays under swarm.*
Both are regressions from earlier commits in this same slice, not
pre-existing drift:

- the nats comment listed "the responder package" among what stays
  under `swarm.nats`; `authPackage` moved in a17286c4
- the matrix comment listed "its package" among what `swarm.matrix`
  keeps; both matrix packages moved in 1b2da54c

Each move edited the prose in the module that declares the option —
`swarm-nats.nix` carries the same "responder package" sentence and it
was corrected there — but deploy.nix keeps its own one-paragraph
description of every service, next to that service's shims. It is a
per-module prose site that does not live in the module, so a sweep
scoped to the module cannot reach it.

The counts in both comments were checked and are correct as written:
the matrix block has six shims and the nats block five. The `*.package`
entries append to the separate block further down, which already has a
comment saying what it is, so no count moved. Each fix is one clause.
2026-09-07 20:46:38 +02:00
atlas
cc82badd56 swarm: move the controller's two packages to deploy
`package` (the daemon build) and `swarmctlPackage` (the operator CLI
installed beside it) are host decisions, so they join the
`deploy.swarm-controller` block that already holds `enable`, the socket
path and the three local-disk credentials.

⚠️ The target namespace is spelled out in the shim comment because
getting it wrong here does not fail. `deploy.hive-controller` also
exists — it is hive-c0re's, carrying `tls.*` and `statusPublish.*`, and
is referenced from four modules. A rename pointing at it would land on
a live, unrelated option and evaluate cleanly. The module already warned
about this above its own deploy block; the warning now sits next to the
entries it governs too.

`swarm-ui.nix`'s package description cited `swarm.controller.package` as
the precedent for "wired from flake.nix rather than defaulted to pkgs".
That was true when written and stops being true here, so it moves with
it — it lives in a module this slice already finished, which is exactly
where a per-module sweep stops looking.

These were the last two `services.hyperhive.swarm.*` writers in
flake.nix; that file now has none.

Riding along, and not a defect fix: `controllerOldPath` spelled `enable`
the NEW way while every other entry used the old one, so six of the
seven controller shims were exercised and the seventh was not. It is
named for testing old paths, so it now uses the old path throughout.
Unlike the nats fixture there was no comment claiming otherwise — this
is coverage the fixture was already shaped to provide.
2026-09-07 20:46:38 +02:00
atlas
889a46729b swarm: move both authelia packages to deploy
`package` (which authelia build runs in the container) and
`bridgePackage` (which `swarm-authelia-bridge` build writes the users
file) are both host decisions, so they join the `deploy.authelia` block
that already holds `enable`, `usersFile` and the two host-side paths.

Six readers, and one of them is why this commit touches a second module:
`swarm-controller.nix` reads `autheliaCfg.package` for
`SWARMCTL_AUTHELIA_BIN`. A per-module sweep never sees that — it is a
cross-reference between two movers, and whichever landed second would
have inherited a dangling alias. The alias itself stays: it still has
three live uses for `url` and `bridgeUrl`.

Two comments needed real edits rather than a rename:

- The `deploy.authelia` header said "all three of these are the same
  kind of thing — a filesystem path". It is five options now and two
  are packages, so the sentence distinguishes the builds from the
  paths, and keeps "a hive that does not run it has nothing at any of
  those paths" scoped to the paths it is true of.
- `module-eval.nix` said "Only `usersFile` has a rename entry",
  explaining that authelia's other slice-7 movers are `readOnly` and a
  rename module cannot contribute a definition to those. That reasoning
  is worth keeping, but the claim stopped being true one line above the
  two entries this commit adds.

The fixture gains both old paths; no new case. `autheliaOldPath`'s
existing arms already fail the eval if either shim goes missing.
2026-09-07 20:46:38 +02:00
atlas
e061e4b446 module-eval: cover the last nats shim the nats fixture missed
`natsOldPath` exists to prove every old `swarm.nats.*` path still
resolves through its rename shim, and it defined six of the seven —
`enable` was spelled the NEW way (`deploy.nats.enable`), so nothing in
the suite ever exercised that shim. Deleting it would have gone
unnoticed.

The comment above the fixture claimed more than the fixture delivered:
"dropping any single shim entry fails the eval" reads as a guarantee
over all seven. Closing the gap rather than narrowing the sentence, so
the claim becomes true instead of merely careful.

`autheliaOldPath` keeps `deploy.nats.enable` deliberately — it needs the
queue on, it is not an old-path test for nats — and the gate controls
both spellings so a future edit cannot silently swap them.

Found by asking the fixture whether a planned mutation could fail before
spending an eval on it: no fixture defined the old path, so the arm was
guaranteed to survive. A grep answered in a second what a mutation would
have taken ~6 minutes to report.
2026-09-07 20:46:38 +02:00
atlas
0fe0df8d63 swarm: move the queue's responder package to deploy
`swarm.nats.authPackage` is the callout responder's build — a host
decision like every other package in this slice — so it joins the
`deploy.nats` block that already holds `enable`, the seed paths and the
auto-mint toggle. Its one reader is the `ExecStart` in this file.

Three comments moved with it, and none of them survived the move
unedited:

- The "deliberately NO `package` option" note now says *anywhere* —
  it argued against a `package` under `swarm.nats`, and after the move
  a reader's next question is why there isn't one under `deploy.nats`
  either, where every other service's build now lives.
- The description's "see the note above" stopped resolving: the note
  is ~80 lines up and in a different option block, so it names
  `options.services.hyperhive.swarm.nats` explicitly.
- The split-rationale comment listed "the responder package" among
  what the queue IS to every hive. It's what the host running it
  decides, so it moved to that half of the sentence.

`flake.nix` writes this option (`lib.mkDefault`), so the writer is
repointed too — three of those left, all in the same block.

The eval fixture gains the old path rather than a new case: the shim
is what a dropped rename would break, and `natsOldPath` already fails
the eval if any single entry goes missing.
2026-09-07 20:46:38 +02:00
atlas
190763b3a2 swarm: move the swarm UI's package to deploy, emptying swarm.ui
Same rule as the four movers before it. What makes this one different is
that `package` was the LAST option under `services.hyperhive.swarm.ui` —
`enable` moved in an earlier slice — so the namespace now holds nothing
and survives only as its two rename entries.

Two consequences worth naming:

flake.nix set `swarm.ui.package` directly. Left alone the shim would
have kept it working, which is exactly the trap: this repo's own flake
would then be setting the option through its own deprecation shim and
warning on every eval. It is repointed here. Four sibling assignments in
that file name movers this commit does not touch; they move with theirs.

The `cfg` alias is deleted. With the one code reader repointed it bound
nothing but an empty attrset — which still evaluates, so nothing would
have failed; it would just have sat there implying `swarm.ui` still held
something. Three comments naming `cfg.package` are repointed with it.

`uiOldPath` sets both old paths, so dropping either rename fails the
eval. Its case reads the vhost's rendered `root` rather than the option,
because that is the half a resolving-but-unwired shim would break.

Not touched: swarm-ui.nix's description says its default is wired "the
same way `swarm.controller.package` is", which is true until the
controller moves. It belongs to that commit, not this one.

Refs #3772.
2026-09-07 20:46:38 +02:00
atlas
e091add37a swarm: move both metric stores' package to deploy, and cover their shims
Same rule as matrix and grafana: which build a service runs is a decision
of the host that runs it. Both stores already had a `deploy.<store>`
option for retention, so the package joins something rather than opening
a namespace.

The prose in both modules claimed the package as part of "what the store
IS from any hive's point of view" — a client hive needs the domain and
the port to reach a store, never the build it runs. deploy.nix's own
comment made the same claim about the pair and is corrected with them.

Separately, and the reason this commit adds a fixture rather than a line:
NEITHER STORE HAD AN OLD-PATH FIXTURE AT ALL. `swarm.victorialogs.` and
`swarm.victoriametrics.` had zero hits in module-eval.nix, so the
`enable` shims from the first slice and both `retentionPeriod` shims have
been uncovered since they landed — the suite would have gone green with
any of them deleted. That is precisely what the wireguard fixture's own
comment warns about: a missing shim reads as a clean tree and breaks
every existing operator config.

`storesOldPath` therefore sets all six old paths, not just the two this
commit moves. The case reads the package the CONTAINER renders rather
than the option, so a shim that resolves but stops reaching the module
fails too.

Refs #3772.
2026-09-07 20:46:38 +02:00
atlas
0002c15dc3 swarm: move grafana's package to deploy, beside the rest of its host knobs
Same rule as the matrix pair: which build a service runs is a decision of
the host that runs it, so `package` follows `enable` out of the namespace
every hive must agree on byte for byte.

Grafana already had a `deploy.grafana` block holding the datasources, the
plugin list and the socket directory, so this is the package joining
options it belongs with rather than a new namespace.

Prose: the swarm-side header listed "its package" among what stays; the
deploy-side header listed what the host decides without it. Both edited,
which is the fix — a substitution on the first alone would have left the
second silently incomplete.

`swarm-grafana.nix:219` was on the sweep list and is NOT edited: it says
what stays above is what the service is to every hive, and that stays
true once the package leaves. The stale half was the sentence after it.

Refs #3772.
2026-09-07 20:46:37 +02:00
atlas
d6f23ff371 swarm: move the matrix packages to deploy, where their enable already lives
Which build a service runs is a decision of the host that runs it, so
`*.package` belongs with `enable` rather than in the namespace every hive
must agree on byte for byte.

The `gui` pair was the sharpest case and the reason this starts here:
`gui.enable` moved to `deploy.*` in an earlier slice while `gui.package`
stayed behind, so one web client's two halves sat in two namespaces —
and hive-matrix.nix said so, in a comment directly above the option.
The diff removes a stated inconsistency rather than introducing a rule.

Both old paths keep working through `mkRenamedOptionModule`; both are
settable (neither is `readOnly`), so both get a shim. The `matrixOldPath`
fixture in module-eval.nix now sets eight options through their
pre-rename paths instead of six, so dropping either new shim fails the
eval rather than silently dropping a definition.

Prose swept in the same commit: three comments claimed the package was
swarm-wide or named the pre-rename path, and docs/web-ui/dashboard.md
named `swarm.matrix.gui.package` two lines above
`deploy.matrix.gui.enable` — the same asymmetry this commit exists to
remove, displayed in the docs. A shim keeps the old spelling valid, so
no build, lint or broken-reference search would have found that line.

Refs #3772.
2026-09-07 20:46:37 +02:00
damocles
ded23b711f swarm-controller: 409 instead of 400 for a terminal-state rejection 2026-09-07 20:31:20 +02:00
damocles
79937a1934 swarm-controller: refuse a state transition off destroyed 2026-09-07 20:31:20 +02:00
damocles
4fae3f13cb add per-agent url to host.sock agent status rows 2026-09-07 20:30:38 +02:00
iris
29c94ea9e6 swarm-ui: disable the wanted-toggle badge on a destroyed row
argus, review: the primary wanted-toggle badge still had no
wanted === "destroyed" guard, so it stayed clickable on a destroyed
row and would PUT {state: "up"} on click -- directly contradicting
the destroy confirm dialogs own "not reversible from here" copy.
Missed this in the previous fix-up (which only addressed the
ConfirmDialog-for-stop ask). Added the guard to both the click handler
and disabled, plus a title explaining why on a destroyed row.
2026-09-07 19:11:50 +02:00
iris
2874badd4c swarm-ui: use ConfirmDialog for the stop confirmation too
mara: "use the new component where we already have confirm dialogs."
The stop/offline direction of the wanted toggle used a native
window.confirm -- the reasoning at the time was that a hand-rolled
Dialog felt heavy for a reversible action, with the real Dialog confirm
reserved for the irreversible destroy direction. Now that
ConfirmDialog is a one-line-per-caller shared component, that argument
no longer buys consistency anything, so both directions confirm the
same way.
2026-09-07 19:07:21 +02:00
iris
82a4324b17 swarm-ui: extract ConfirmDialog, use it for the destroy confirm
mara, reviewing the swarm-ui destroy-trigger PR: "why the extra
styling? shouldnt there be a component that does this already?" There
wasnt one -- Dialog is deliberately content-agnostic (see its own
file-top comment), so the destroy confirm had grown its own
page-scoped .agents-destroy-confirm/-actions CSS for what is really a
generic "message + cancel/confirm button row" shape.

Extracted ui/confirm-dialog/ConfirmDialog.tsx: wraps Dialog, owns the
button row, leaves the message body to the caller via children.
AgentsPage now uses it instead of a bare Dialog + bespoke CSS; deleted
the now-unused AgentsPage.css.
2026-09-07 19:02:52 +02:00
iris
becc025f13 swarm-ui: add a destroy trigger to the agents page
Closes #4067. Backend half (Wanted::Destroyed + reconcile) shipped in
#4065 with no swarm-controller API changes needed -- SetAgentStateRequest
already accepted {"state": "destroyed"}, it just had nothing in swarm-ui
sending it.

Destroy is a separate `quiet`-variant badge next to the existing
start/stop toggle (#3988's "wanted" column), not a third state folded
into that same click target -- one wrong click on a shared toggle
would be irreversible, where a dedicated badge only fires from its own
confirm dialog. That confirm is a real `Dialog`, not the native
`window.confirm` the reversible stop direction uses -- the wanted
column's own comment called this out as the case that would justify
one when it was first written.

Shared the PUT-declare/pending/error/patch-rows logic between the
existing toggleWanted and the new destroyAgent (declareState) rather
than duplicating it -- confirmation and target-state selection are the
only parts that differ between a toggle and a one-way declaration.

Verified: `tsc --noEmit` clean, `nix fmt` reports the expected
formatting-only diff, wire shapes (state string "destroyed",
AgentDeclaration response) checked against swarm-queue-client's
AgentState::as_str and swarm-controller's actual handler rather than
assumed from the issue description.
2026-09-07 18:53:55 +02:00
iris
a8fb08a7e8 docs: fix Microsoft.UIVerbs findings (click -> select)
Part of #4041's forgotten remainder (UIVerbs was scoped as pure
execution work, no decision needed -- see comment 71586).

29 genuine input-device-neutral rewrites ("click"/"clicking"/"clicked"
-> "select"/"selecting"/"selected", a few reworded where a direct verb
swap didn't read naturally: "one click from the hub" -> "one step from
the hub", "click straight from X into Y" -> "go straight from X into
Y", "the operator never clicked" -> "the operator never triggered").

Left 5 hits unfixed, each read in context and genuinely not a
device-assumption problem:
- docs/process/gotchas.md:439 -- "click-to-unlock screen" names a
  specific OS lock-screen pattern, not an instruction to the reader.
- docs/web-ui/agent.md:377 -- "clicks" describes literal RFB pointer
  events forwarded by hive-screen-mcp's sendPointer, genuinely
  mouse-specific.
- docs/web-ui/agent.md:78 -- "ctrl/middle-click" names a specific
  keyboard-modifier + mouse-button combination with no "select"
  equivalent.
- docs/web-ui/dashboard.md:741 -- explicitly contrasts "click (or
  Enter/Space)" as two named input methods; replacing "click" with
  "select" would conflate the two the sentence is distinguishing.
- docs/web-ui/shape.md:253 -- "swipes in from the right" describes the
  panel's own slide animation, not a user touch gesture.

Verified: fresh `vale --filter '.Name == "Microsoft.UIVerbs"' docs`
after the fix shows exactly those 5, `nix fmt` reports 0 changes.
2026-09-07 18:46:16 +02:00
damocles
3871749da9 host.sock: push a live agent-status stream instead of poll-only 2026-09-07 18:43:47 +02:00
atlas
2f7d3e02e9 bao: write the swarm controller's policy from inside the store
Cert auth answers a role, so nothing in the swarm can authenticate to
the store until some role exists. Creating the first one therefore
cannot itself use a certificate — the credential has to come from
outside that cycle, and an operator places it.

`deploy.bao.bootstrapTokenFile` names that token. A one-shot unit inside
the store's container reads it and writes the `swarm-controller` policy;
`local-defaults.nix` supplies the path (never the file) on an all-local
deploy, since co-location makes only the location derivable. `bao
operator init` stays an operator action in every shape.

The unit runs in the container rather than on the host because writing
the first grant is a store-side operation: it reaches the store locally
and needs no client certificate. Gating it on the store being here is
therefore not the co-location assumption glue-matrix-bao-token.nix
warns about — a reader has to work from anywhere, the first write never
does.

Policy only, deliberately: a cert-auth role binds a certificate and the
controller has no bao identity yet — it holds no leaf and contains no
bao code at all. The two certificates that do exist are both wrong to
bind. `clientCertFile` is the store host's own reader leaf rather than
the controller's, and the CA that signed it also signs every other
reader's, so binding that would let any reader authenticate as the
controller. Whoever gives the controller an identity writes the role.

The grants are scoped to `hive-*`. Without the prefix the controller
could rewrite the policy that constrains it.

Two things the module-eval arms pin: the unit renders inside the
container with the token path in both its script and its
ConditionPathExists, and a host that names a token while running no
store gets neither the unit nor the host-side directory.

The capabilities were derived with `bao write -output-policy` rather
than written from memory, and the setup.md commands were run against a
real binary for shape before being written down.

Refs #3726.
2026-09-07 18:43:09 +02:00
damocles
6fd5bc1c4f wanted: converge a destroyed declaration by tearing the container down 2026-09-07 18:10:15 +02:00
iris
e82a735745 docs: fix write-good.So/ThereIs/Weasel lint findings
Fixes the "obvious ones first" slice of #4042 (mara: do the obvious
ones first) -- 81 hits across write-good.So, write-good.ThereIs, and
write-good.Weasel, all in docs/. Each is a genuine sentence rewrite
(lead with the real subject instead of "There is/are", drop a
sentence-initial "So ", replace a vague intensifier), not a blind
regex substitution -- read every hit in its real file context before
touching it.

3 of the 81 hits were in CI-generated CLI docs (docs/tools/{hivectl,
swarmctl,forge}-cli.md) -- fixed at the clap #[arg(...)]/doc-comment
source in hivectl/src/cli.rs, swarmctl/src/main.rs, and
hive-forge/src/verbs/repo_add_collaborator.rs, then regenerated via
each crate's `markdown-docs` subcommand so CI's freshness check stays
green.

Verified: fresh vale re-run shows 0 remaining So/ThereIs/Weasel hits
and no new hits introduced (983->982, exactly the one incidental fix
this pass also picked up at docs/scheduler/observability.md:48).
cargo fmt --check and clippy clean on the three touched crates.

Remaining write-good backlog (Passive: 726, TooWordy: 207) is
judgment-heavy and left for a follow-up slice of #4042, not bulk-
rewritten here.
2026-09-07 17:49:27 +02:00
atlas
6d4e0e5fa1 docs/ci: say to measure the GC window before relying on the stagger
The guidance says to stagger two hosts' collections, and never says how
long one takes — so an operator picking `dates` has no way to know
whether their stagger is wide enough.

The number that matters is theirs, not mine: a collection's wall clock
is dominated by the sweep over unused `.links` entries, which scales
with the size of the store rather than with what the run deletes. So a
store's size, not its churn, decides the window, and `randomizedDelaySec`
spreads only the start.

Measured here for scale, and deliberately kept out of the doc since it
does not transfer: 11 runs over two weeks ranged 5min 16s to 51min 25s,
inversely — the 51-minute run deleted 732 paths, a 12-minute run deleted
21,901.

Refs #4019.
2026-09-07 17:05:19 +02:00
atlas
01ce968fb6 nix: split statusPublish and the otel secret into deploy.*
Slices 8 and 9 of the swarm/deploy split, and the last two.

statusPublish had three coordinates under one namespace. Two of them
are this machine's — where the queue listens *as seen from here*, and
where its client secret sits on this disk — so they move to
`deploy.hive-controller.statusPublish.*`, the namespace of the daemon
that is their only reader. `tokenEndpoint` is the swarm's one address,
so it stays. That leaves `swarm.statusPublish` holding a single option:
a legitimate split, not a botched move.

The all-or-nothing assertion now spans both namespaces. It is repointed
in both its condition and its message, and the message spells all three
paths in full so an operator is never told to set two options under a
path that only has one. `environment.nix`'s guard and the value beside
it likewise read different namespaces on purpose.

The collector's secret moves the same way, for the same reason, to
`deploy.swarm-otel.*` — `enable` already lives there. That also retires
one of the eight cross-namespace assignments tracked in #4048: the
delivery unit set a `swarm.*` value under a `deploy.*` gate, and now
sets a `deploy.*` value under one.

module-eval gets a fixture per slice. `otelRemoteAuthelia` already set
the collector secret through its pre-rename path, so it becomes slice
9's old-path case as it stands — left spelled that way deliberately,
with a comment, so it is not read later as a missed site.

That fixture also turned out to be describing an impossible hive: it
said authelia lives elsewhere without saying where, so the
authenticator interpolated a null `swarm.authelia.url` into its
`token_url`. Nothing to do with the rename, and invisible to the
existing case over the same fixture, which reads `? auth` and `elem` —
both stop at names and never force the extension's value. Given the
address a remote-IdP deployment has.

Verified: 49 -> 51 properties, all holding.
2026-09-07 16:54:23 +02:00
atlas
2e1c15dc98 matrix: derive the gateway's body cap from maxRequestSize
Two limits governed a matrix upload and nothing kept them in
agreement: the documented option reached the homeserver, while the
nginx location every client request traverses carried its own
`client_max_body_size 50M`. Raising the option past 50M changed
nothing — nginx returned 413 at the proxy, naming neither matrix nor
the option that had just been raised.

The cap is not set equal to the option. Equality would regress the
default: at 20 MB a 25 MB upload is currently rejected by the
homeserver, with a matrix error a client can act on, and equality
turns that into a gateway 413. The proxy must never be the party that
rejects, so it tracks the option with headroom. This is the shape the
forge vhost already uses for git, where a generous proxy cap lets the
application own the real limit.

The module-eval arm sets a deliberately odd size so the number it
looks for cannot have come from a default, and asserts the rendered
location. Its control names the whole directive rather than the bare
old value: nginx comments render into the config, so the comment above
the directive mentions 50M and a looser arm matched itself.
2026-09-07 16:53:43 +02:00
damocles
451b461afb docs: matrix gateway vhost defaults to chat.<swarm-domain>, not matrix.<domain> 2026-09-07 16:53:22 +02:00
iris
17554ea563 fix: sync generated hive-forge CLI docs with their clap source strings
Same bug as the swarmctl/hivectl fix, a third instance argus's review
didn't name but nix/checks.nix's hive-forge-docs freshness check (same
pattern as hivectl-docs/swarmctl-docs) caught in CI: the earlier
Contractions/Foreign/Auto batches edited docs/tools/forge-cli.md
directly instead of the clap #[arg(...)]/doc-comment strings in
hive-forge/src/main.rs and hive-forge/src/verbs/*.rs.

Applied the same 13 wording changes to source that the earlier commits
already made to the generated .md, matched 1:1 against
'git diff origin/main HEAD -- docs/tools/forge-cli.md' rather than
guessed. Several source doc comments feed two rendered sections each
(e.g. reaction.rs's one Add-variant doc renders under both
'issue reaction add' and 'pr reaction add', since both subcommands
share the same enum) -- one source fix, two generated-doc fixes.

Regenerated from the now-fixed source and confirmed byte-identical to
what's already committed (diff exit 0) -- source and generated output
are back in sync, same as the swarmctl/hivectl fix.

cargo clippy -p hive-forge --all-targets -- -D warnings and
scripts/check-doc-refs.sh both clean.
2026-09-07 16:28:06 +02:00
iris
bc8f1b3cb8 fix: sync generated CLI docs with their clap source strings
docs/tools/swarmctl-cli.md and docs/tools/hivectl-cli.md are generated
(nix/checks.nix's swarmctl-docs/hivectl-docs freshness checks diff a
fresh 'markdown-docs' run against the committed copy). The earlier
Contractions/Foreign fixes in this branch edited the generated
markdown directly instead of the clap #[arg(...)]/doc-comment strings
in swarmctl/src/main.rs and hivectl/src/cli.rs -- so CI's freshness
check would regenerate the docs from unfixed source and silently
undo the fix (found by argus's review).

Applied the exact same 13 wording changes to source (5 in
swarmctl/src/main.rs, 8 in hivectl/src/cli.rs) that the earlier commits
already made to the generated .md, matched 1:1 against
'git diff origin/main HEAD -- docs/tools/{swarmctl,hivectl}-cli.md'
rather than guessed. Regenerated both docs from the now-fixed source
and confirmed byte-identical to what's already committed (both
'diff <(binary markdown-docs) docs/tools/X-cli.md' exit 0) -- source
and generated output are back in sync.

cargo clippy -p swarmctl -p hivectl --all-targets -- -D warnings and
scripts/check-doc-refs.sh both clean.
2026-09-07 16:28:06 +02:00
iris
ec9ee866db docs: fix merge-conflict fallout + a fresh Contractions/Auto typo from the merge
Resolving the 5 branches (contractions/foreign/auto/quotes/hyphens) onto
current main surfaced a few things beyond plain conflict markers:

- docs/scheduler/coordinator.md had 'auto-noops' turned into the
  malformed 'autanoops' by the auto-hyphens batch -- fixed to
  'autonoops'.
- main had moved (new prose, some option renames) since the batches
  were generated against an older commit, so a handful of merge
  conflicts needed both sides combined (keep main's updated content,
  reapply the lint fix on top) rather than a plain pick-one resolution.
- A few genuinely new Microsoft.Contractions hits had appeared in the
  moved-forward main and in content pulled in wholesale during
  conflict resolution -- fixed the 2 real ones
  (docs/integrations/forge.md, docs/turn-loop/claude-invocation.md).
- The other 'new' Contractions hits vale reports now
  (docs/integrations/forge.md:281, docs/tools/forge.md:260,
  docs/tools/hivectl.md:239, docs/tools/swarmctl-cli.md:28,
  docs/web-ui/dashboard.md:457) are a side effect of the Foreign
  batch's own 'i.e.' -> 'that is' substitution: vale's Contractions
  rule can't tell 'that is' used as a copula (contractable) from 'that
  is' used as a parenthetical clarifier (not contractable, would break
  the sentence -- 'that's it raced the merge' isn't English). Left
  those alone; noted in the PR description.

Final vale count on this branch: the only Microsoft.Contractions/
Foreign/Auto/Quotes/Hyphens hits left are the known legitimate skips
(ALL-CAPS/bold emphasis, vale's own false positives, and this
Foreign/Contractions interaction) -- verified with a fresh vale run.
2026-09-07 16:28:06 +02:00
iris
3f41e7a09c docs: drop redundant hyphens from -ly adverb + adjective/participle compounds
Fixes 22 of 23 Microsoft.Hyphens hits: an adverb ending in -ly doesn't
need a hyphen to the word it modifies (minimally scoped, currently
running, deliberately stopped, ...) -- the -ly ending already
unambiguously signals it's modifying the following word, so the
hyphen is redundant per Microsoft style.

Left one hit alone: docs/web-ui/dashboard.md's 'checked-not-originally-
active' is a single 4-word compound designator (parallel structure
with 'unchecked-was-active' right before it in the same sentence,
describing a diffing algorithm's two possible states), not an
ordinary -ly-adverb modifier -- dropping just the one hyphen vale
flagged would have broken the compound into a dangling
'checked-not-originally active' fragment. Also rewrote
docs/scheduler/coordinator.md's 'still-fully-queued' as a predicate
('DAGs that are still fully queued') rather than leaving a similar
broken partial-hyphen chain, since 'still' and 'fully' are both plain
adverbs with no reason to hyphenate at all once dehyphenated
consistently.

Verified via vale: only the one deliberately-left hit remains.
2026-09-07 16:28:06 +02:00
iris
c20496604c fix Microsoft.Quotes vale findings - move punctuation inside quotes
Moved 27 instances of periods and commas from outside closing quote marks
to inside, following American-style punctuation placement. Changes span
across ~15 documentation files including integration, process, scheduler,
trust-boundary, and web-ui docs.
2026-09-07 16:28:06 +02:00
iris
05cf8e8276 docs: remove hyphens from auto-X compounds per Microsoft.Auto style
Fixed 72 hyphenated 'auto-X' compounds across the docs by deleting
the hyphen to match Microsoft style guide (autogenerated, autoregistration,
autodiscovery, etc). Preserved original capitalization throughout.

Covered 26 documentation files in docs/, including README.md, agent-lifecycle,
integrations, scheduler, networking, process, tools, trust-boundary,
turn-loop, and web-ui sections.
2026-09-07 16:28:06 +02:00
iris
4f2d1d281a docs: fix Microsoft.Foreign hits (e.g./i.e. -> for example/that is)
Substitutes 'for example' for 'e.g.' and 'that is' for 'i.e.' per
Microsoft style, with no comma after the replacement -- this repo
never puts a comma after e.g./i.e. either, so the substituted phrase
matches that house style instead of vale's literal 'for example,'
suggestion.

Covers all 77 real hits (72 from the initial vale pass + 5 more of the
same pattern found on a follow-up vale run). None of the flagged
instances were sentence-initial, so there's no capitalization concern
here.

Verified via a fresh vale run: 0 remaining Microsoft.Foreign hits.
Full accounting in /agents/iris/state/haiku-batches/foreign-report.md.
2026-09-07 16:28:06 +02:00