Commit graph

4,569 commits

Author SHA1 Message Date
atlas
a3b672d1d5 refactor(hive-c0re): drop the request_init_config tool and InitConfig approval
swarm-controller's `InitAgentConfigRepo` node already covers config-repo
creation, so this deletes a duplicate rather than a capability; old
`init_config` rows are skipped by `collect_lenient` with no migration, by
operator decision.

Refs #4398
2026-09-14 19:03:44 +02:00
flake-bot
3f086bc659 nix flake update 2026-09-14 15:54:18 +02:00
atlas
20e211f904 fix: resolve unresolved rustdoc link and quote-punctuation lint
- swarm_agent_state.rs: HEADROOM in swarm_term.rs is private, so the
  intra-doc link can never resolve; switch to plain backticks.
- docs/swarm/README.md: move closing period inside the quoted phrase
  per Microsoft.Quotes.

Refs #3802
2026-09-14 15:37:09 +02:00
atlas
1ea3d87d7a swarm: publish each agent's turn-state header on its own subject
The swarm can already tell whether an agent is alive — the `agent-status`
KV bucket republishes once a minute — but not what it is doing right now.
A header bar wants the second thing, and a minute-old answer to "is this
agent thinking" is the wrong answer most of the time it is read.

`hive-agent` now publishes a turn-state header to
`$SWARM.agent-state.<hive>.<agent>`, a core subject beside the terminal
rows it already sends. It goes out **on transition, not on a timer**: the
publisher watches the event bus, rebuilds the header, and sends only when
the serialised result differs from the last one it sent — so a second
periodic writer, which is the problem this exists to fix, is not what
replaces the bucket.

The payload is the published contract a swarm-level renderer is written
against, so the test asserts on the serialised JSON keys rather than on
Rust field names. Two fields deliberately depart from the per-agent web
UI's `StateSnapshot`: `turn_state_since` is an ISO 8601 UTC string rather
than unix seconds, matching the sibling `$SWARM.term` subject's stamp, and
`agent_state` carries the swarm's own `AgentState` vocabulary rather than
a `paused` boolean, so a reader can compare actual against wanted without
translating. `turn_state` and `agent_state` stay two separate fields:
neither vocabulary contains the other's values.

Swarm-side, `GET /api/agents/{name}/state/stream` relays the subject as
SSE, resolving the agent's hive at request time exactly as the terminal
stream does and passing the bytes through without parsing them.

The broker grant is a second `--agent-publish-subject` rather than a
widening of the existing one, so the terminal family and the header family
stay independently revocable, and a `module-eval` arm pins the rendered
flag and its argument together — the doubled dollar included, since a
single one expands to nothing in `ExecStart` and yields a grant that
matches nothing.

Refs #3802
2026-09-14 15:12:23 +02:00
atlas
065f93f037 fixture: give otelNoIdentity a hand-delivered forge SSO secret
otelNoIdentity points swarm.authelia.url at a remote authelia while
deploy.authelia.enable stays false, so ssoLocal is false and nothing
mkDefaults the forge's deploy.forgejo.sso.clientSecretFile. The forge
module's config is gated only on services.hyperhive.enable (there is
no deploy.forgejo.enable to opt out of), so its SSO assertion fired
against every fixture that lacks the secret — entirely orthogonal to
what this fixture tests. Supply the by-hand path, mirroring
otelRemoteAuthelia's own hand-delivered secret.

The swarm.otel.journaldUnits = [ "nginx" ] line added in the previous
commit targeted an assertion that was never firing: journaldUnits
already defaults non-empty, so the effective list just contained
"nginx" twice. Removed as redundant.

Refs #4374
2026-09-14 00:58:58 +02:00
atlas
30b9955ad3 fixture: repair 4 CI failures uncovered on rebase
Refs #4374

- otelNoIdentity: name journaldUnits so the fixture trips the store-
  identity path it's testing instead of swarm-otel's unrelated
  journald-safety assertion (an empty list with log collection on is
  refused as "collect everything", not "collect nothing").
- otelNoStores: give it a bao client identity. The secret gate moved
  from deployCfg.authelia.enable to a real client cert/key pair, so a
  fixture meaning "no telemetry stores" now needs its own secret
  identity to keep exercising the exporter/authenticator wiring it
  was written for.
- docs/swarm/secrets.md: two vale fixes — a contraction, and drop a
  condescending "simply".
2026-09-14 00:58:58 +02:00
atlas
0ff5c8110b swarm-otel: deliver the OIDC client secret through the secret store
The swarm collector's OIDC client secret only existed where authelia
did: `swarm-otel-oidc-secret.service` copied the minted plaintext out
of authelia's container tree, reachable only because the two share a
host's network namespace. A swarm that placed authelia elsewhere
delivered nothing, and the option's own description said so —
"a deployment that places authelia elsewhere points this at a file it
delivers itself." Same gap as #3853 and #4234, and this is the
swarm-otel twin of #4234's fix for Grafana.

Mirrors PR #4361 (Grafana) almost exactly:

- `swarm-bao-otel-oidc.service` reads
  `swarm/services/<client-id>/oidc/client` out of the store, in every
  deployment, replacing the co-located copy unit outright — one
  delivery route, not two, per the ruling that landed under #4234.
- Client registration moved out of `swarm-otel.nix`'s own `config`
  block (gated on this host running the collector) into
  `glue-swarm-otel-oidc-client.nix` (gated on this host running
  authelia), the same split `glue-grafana-oidc-client.nix` made. It
  was broken the same way: a split deployment registered the client
  nowhere at all, so authelia never minted a secret for the publisher
  to send on.
- The publisher's `services` prefix (write grant in `swarm-bao.nix`,
  hive read grant in `policy::render`) already covers any service's
  path — nothing to add there. `swarm-secret-publisher.nix` only grew
  `serviceClientIds` by one entry.

One judgement call, stated rather than buried: the store-reading unit
renders only where this host holds a client identity
(`deploy.bao.clientCertFile`/`clientKeyFile`), rather than asserting
it the way `swarm-grafana.nix` does. Grafana's local login form is
disabled unconditionally, so a Grafana with no OIDC secret has no way
in at all — that earns a hard refusal. This collector without a
credential still receives every hive's telemetry; only its own pushes
to the stores go out unauthenticated and get refused there, an
already-supported degrade the module's own `haveCollectorSecret` flag
named before this change. So the reading unit follows the shape
`glue-matrix-bao-token.nix` and `glue-queue-agent-credential.nix` use
for their own optional readers: no unit when the identity is absent,
not a build refusal.

Fixtures mirror #4361's: `otelBaoWithAuthelia`/`otelBaoRemoteAuthelia`
are the positive pair (co-located and split, both reading through the
store), `otelNoIdentity` is the negative — no reading unit, no
assertion firing, `clientSecretFile` left null.

Refs #4258
2026-09-14 00:58:58 +02:00
atlas
baab0f393e swarm-secret-client: a read policy scoped to one agent
The identity half of giving an agent container its own way into the store.
The operator's ruling: minting an identity the agent itself can authenticate
with takes the delivery from four pieces (mint -> store -> pass -> use) to
three (mint -> store -> use), and makes the agent that much less dependent on
the hive it runs on — but only if the grant behind that identity is scoped to
that agent's secrets, read-only. Otherwise it is a second way to be everyone.

So `render_agent` is a SIBLING of `render`, not a parameter added to it. A
hive's document is wide in two of its kinds on purpose (the module header says
why each of those is a decision), and none of that breadth transfers: an
agent's path names the agent, so scoping to it costs nothing and drifts
nowhere, and an agent has no business with a service's OIDC client secret,
another agent's credentials, a hive's, or the controller's. The tests say that
as an exhaustive check over `path::Kind` rather than as a list of paths, so a
kind added later cannot be granted here by a renderer nobody re-read.

`render`'s own text is untouched, and its byte-for-byte assertion is left
exactly as it stands: a hive can still read every agent's secrets, and closing
that is the separate decision its doc comment already prices.

The object name is `hive-agent-<agent>`, one string for the policy, the
cert-auth role and the certificate subject. Inside `hive-` rather than beside
it because `sys/policies/acl/hive-*` and `auth/cert/certs/hive-*` are the whole
of what the controller may create, and the controller is the only principal
that learns an agent exists — at no cost in authority, since it already holds
create/update on `secret/data/swarm/agents/*`. It still cannot collide with
`hive-<hive>`: that needs a hive named `agent-<agent>`, and a hive name may not
contain `agent` (nix/reserved-hive-fragments.nix, the reservation the queue's
`hive-<name>-agent` client ids already lean on). A test spells that collision
out and fails if the prefix moves somewhere that guard does not cover.

Rendering only. Nothing mints a leaf, writes a role or delivers a certificate
yet, and nothing calls either new function — because nothing at the module
layer knows which agents exist: hive-c0re creates them at runtime and renders
them into its own meta flake, which is why even the gateway's per-agent vhosts
are a generated conf rather than nix. Issuance therefore has to be runtime, and
that design is the operator's to approve before it is built.

Refs #4386
2026-09-14 00:58:48 +02:00
iris
d7a506defa swarm-grafana: fix leaked test title on the agents dashboard
The v2-conversion script carried over the local-test dashboard's own
title ('agents-full-test') instead of the real one, since it read
spec.title straight from the generated ground-truth file without
overriding it back. Caught on a diff review, not by CI — nothing
schema-level flags a wrong-but-valid string.
2026-09-14 00:21:23 +02:00
iris
ae31f30c43 swarm-grafana: rebuild the agents dashboard as real v2-schema tabs
Confirmed empirically (real Grafana 13.0.7, a file-based provisioner
identical in shape to swarm-grafana.nix's) that the deployed version
provisions v2-schema dashboards natively, tabs included, no migration
path needed. Supersedes the row-based split from the previous commit
on this branch: same four groups (activity/cost/health/subagents), same
17 panels with identical content (targets, fieldConfig, options,
descriptions all carried over unchanged — only the document shape
around them moved from a flat panels array + row wrapper panels to the
v2 schema's spec.elements map + spec.layout TabsLayout referencing them
by id), now rendered as real browser tabs instead of accordion rows.

Generated the panel-content half of this file mechanically rather than
hand-authoring the new schema: posted the pre-conversion (v1, flat,
17-panel) dashboard to a local Grafana 13.0.7 and read it back through
the v2beta1 API, which is Grafana's own conversion of the exact same
panel content into the v2 element shape — then replaced only the
generated layout (a flat GridLayout) with a hand-authored TabsLayout
grouping the same element references into the four tabs. Verified the
result end-to-end against the same local instance: file-provisioned
(confirmed via the loaded object's own grafana.app/managedBy annotation),
and a real screenshot of each of the four tabs rendering with its
correct panels.
2026-09-14 00:19:23 +02:00
iris
8cf1cb3ff8 swarm-grafana: split the agents dashboard into four rows
Cost, health (cpu/mem/disk), and subagents, plus an activity row for the
panels that fit none of the three (agent/turn counts, lines-of-code and
CLAUDE.md-size trends). Rows rather than tabs: Grafana's tab layout is a
v2/Scenes-schema feature, and this deployment's file-based provisioner
doesn't reliably load v2-schema dashboards, so classic-schema collapsible
rows are the closest available equivalent — one section open, the rest
collapsed. activity stays expanded so the dashboard opens on something
useful instead of a wall of accordion headers.
2026-09-13 23:17:16 +02:00
atlas
ef2dfbfb31 swarm-bao: reach the store through a TLS passthrough, not a vhost
An agent container cannot dial the store's loopback listener: the bridge
to-loopback DROP rule is there precisely to stop that, and the store
authenticates every reader by client certificate, so the usual answer —
a gateway vhost — is the one shape that cannot work. A terminating proxy
strips the certificate and bao sees nginx as the client for every hive.

nginx's stream module does not terminate. `ssl_preread` reads the SNI off
the ClientHello and splices the rest of the connection through byte for
byte, so bao completes the handshake itself and authenticates the client
it actually has. That is the no-vhost rule kept, not bent.

The listener binds the bridge IP rather than every address, because bao
already holds `127.0.0.1:<port>` in the same netns and a wildcard bind
there is EADDRINUSE — nginx would fail to start, taking the gateway with
it. Nothing moves as a result: the name already resolves two ways, so a
host-side reader still goes straight to loopback and an agent goes
through the passthrough, both on one `BAO_ADDR`.

Renders only inside the store's own `deploy.bao.enable` region; a host
that runs no store grows no listener and opens no port.

Per-agent certificates and per-agent policy are separate work.

Refs #4386
2026-09-13 23:08:38 +02:00
iris
cab6910cbb swarm-ui: make the shell header sticky and frosted glass 2026-09-13 22:13:07 +02:00
iris
08aee4b704 swarm-ui: fix agent term preview box-sizing causing content to bleed past its wrap 2026-09-13 22:01:40 +02:00
iris
8db1e61cd3 swarm-ui: fix agent term preview placeholder wrapping one char per line 2026-09-13 21:47:23 +02:00
atlas
6de28514cd fix(otel): let file_storage extension create its own directory
validateConfigFile runs `otelcol validate` at nix build time, in a pure
sandbox where systemd's StateDirectory= has not run yet, so the
directory named by extensions.file_storage.directory does not exist.
Setting create_directory = true lets the validator create it itself,
matching what happens at runtime once StateDirectory= has acted.

Refs #4375

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 21:43:14 +02:00
damocles
93f2988e1d docs: drop the unverified subagent-inheritance claim per mara 2026-09-13 20:29:39 +02:00
damocles
e4ee7b484b docs: mark subagent outputStyle inheritance as unconfirmed, not assumed 2026-09-13 20:29:39 +02:00
damocles
1dc097be15 claude-settings: set fleet-wide outputStyle to Concise 2026-09-13 20:29:39 +02:00
iris
e18619329d swarm-ui: fix treefmt formatting drift on the agent term files 2026-09-13 20:23:51 +02:00
iris
b3c242ea5c frontend: regenerate npm-deps-hash for the dompurify/marked workspace move 2026-09-13 20:23:51 +02:00
iris
727bfb775b swarm-ui: fold the agent terminal into AgentsPage's detail panel, not a route
mara's steer on the issue, right as the previous commit's plan was
posted: the swarm-level term isn't a separate page, it's part of the
same pwa -- selecting an agent should show a small preview (no header,
no input) below the existing detail fields. That's the whole MVP for
this issue; the full terminal (+ sending input back to the agent) is
explicit follow-up scope.

AgentTermPage -> AgentTermPreview: drops the Panel wrapper, the
"terminal" nav badge + wouter route, and the back-link -- just the
inline preview, height-capped at 12em (@hive/shared/terminal.css's own
default is a much taller 72vh/60em, sized for a dedicated page rather
than a peek).
2026-09-13 20:23:51 +02:00
iris
593923375c swarm-ui: read-only agent terminal page consuming the swarm term stream
Moves the TermMsg rendering pipeline (Row.tsx, termMsg.ts, linkify.tsx,
markdown.ts) from @hive/agent into @hive/shared, so swarm-ui becomes a
second consumer of it instead of forking a copy -- CSS was already
shared (@hive/shared/terminal.css). marked+dompurify move from
@hive/agent's deps to @hive/shared's; swarm-ui picks them up
transitively, no new direct dep there.

New swarm-ui route /agents/:name/term (AgentTermPage), linked from
AgentsPage's detail panel via a "terminal" badge next to "link matrix
account". Consumes GET /api/agents/{name}/term/stream: unlike
@hive/agent's own useLiveStream (TermEnvelope-wrapped, history/backfill
dance), the swarm relay forwards one bare TermMsg per SSE event with no
envelope and no history endpoint -- useSwarmTermStream is a much
smaller hook for that shape (EventSource -> parse -> coalesce, nothing
to buffer/dedupe/backfill against).

Verified against a live SSE mock (screenshots in /agents/iris/state/screenshots/
3801-agents-detail-panel-terminal-badge.png and
3801-agent-term-page-live-rows.png -- real rows rendering through the
shared Row component, not just a build/typecheck pass).
2026-09-13 20:23:51 +02:00
flake-bot
5dff508e79 nix flake update 2026-09-13 20:22:26 +02:00
atlas
276bf8531a nix: inline the swarm sub-path aliases at their use sites
autheliaCfg/forgeCfg/vmCfg/vlCfg were local re-exports of
hyperhiveCfg.swarm.<subpath> — a shape that hides the full option
path from grep, the same hazard #4356 fixed for the tls alias.
Removed the six let-bindings and spelled the full
hyperhiveCfg.swarm.<subpath>.<field> path at every use site instead.

None of the read fields (url, machine, hiveClientPrefix,
agentClientSuffix, domain, port) sit on the old side of a
mkRenamedOptionModule in deploy.nix, so inlining the alias's own
current path is correct as-is.

Refs #4363

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 20:02:20 +02:00
atlas
815f977d7c swarm-grafana: one delivery route for the OIDC client secret
The previous commit left two delivery paths and a three-way gate:
`swarm-grafana-oidc-secret.service` copied authelia's minted plaintext
out of its host tree wherever the two were co-located,
`swarm-bao-grafana-oidc.service` read the same value from the swarm
secret store wherever they were not, and `ssoConfigured && (ssoLocal ||
haveClientIdentity)` decided whether Grafana got an OIDC block at all.

Delete the co-located path. The store reader is now THE delivery unit,
in every deployment — the publisher on authelia's host writes
`swarm/services/<id>/oidc/client` whether the reader is a network away
or in the container next door. The ruling behind it: the store exists so
a host holds ONE out-of-band secret, its client certificate, and reads
everything else with it. Skipping the store when the producer happens to
be local saves a round trip and costs a second delivery unit, a second
way for the file to be wrong, and a gate to choose between them.

The gate goes too, and both of its questions become assertions, scoped
to hosts that run Grafana:

- `swarm.authelia.url` must be set. `auth.disable_login_form` is
  unconditional — Grafana ships an admin/admin account on a public
  vhost — so dropping the OIDC block when the swarm names no IdP
  produced a container with no SSO and no password box, silently. An
  eval-time refusal naming the option is the only report that reaches
  anyone, the shape swarm-nats.nix already uses for the same option.
- `deploy.bao.clientCertFile` / `clientKeyFile` must be set. This
  replaces a warning that nothing reads back, and its message names both
  options and where the leaf comes from.

Fixtures follow. `grafanaWithAuthelia` gains the cert pair, because a
co-located host is a store reader like any other. The old
`grafanaRemoteAutheliaNoIdentity` is kept rather than deleted, renamed
`grafanaNoIdentity`: the shape is still reachable, only its deliverable
changed from silence to a refusal, and an arm now reads that refusal
back. Its mirror `grafanaNoSso` covers the other assertion, each fixture
wrong in exactly one way so an arm can name which refusal fired. Every
positive keeps an explicit negative — the one-delivery-unit arm asserts
the deleted unit is absent in both topologies rather than merely that
the store reader is present.

Refs #4234

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 19:57:28 +02:00
atlas
4aa982cc2a swarm-grafana: deliver the OIDC client secret through the secret store
Grafana's OIDC client secret only existed where authelia did. One
`ssoLocal` gate — `grafana.enable && authelia.enable` — decided the
client registration, the minted secret's delivery and the whole
`auth.generic_oauth` block, so a swarm whose authelia runs on another
host got Grafana with no SSO wiring at all. The local login form is
disabled unconditionally, so that is no way in.

Split the one gate into the two questions it was conflating:

- `ssoConfigured` — does this SWARM have an identity provider
  (`swarm.authelia.url`, which is swarm-wide and whose own description
  makes null mean "no SSO configured"). With a delivery route present
  this is what emits Grafana's OIDC block.
- `ssoLocal` — is authelia on THIS host, now spelled as the forge and
  matrix modules spell it. It decides only which unit delivers the
  secret.

Where authelia is elsewhere, `swarm-bao-grafana-oidc.service` reads the
secret from the swarm secret store, shaped after
glue-queue-agent-credential.nix: cert login fails loudly because a retry
fixes every state it fails on, the read degrades quietly because no
retry turns "no value there" into a value, and nothing writes a
stand-in. The producer is the publisher that already runs on authelia's
host, which gains the swarm's service clients beside the per-hive ones
at `swarm/services/<id>/oidc/client` — with the write grant in
swarm-bao.nix and the hive read grant in `policy::render` to match.

Registration moved to glue-grafana-oidc-client.nix. It has to be
declared where authelia's config is rendered, and swarm-grafana.nix's
config block hangs off this host running Grafana.

Two judgement calls stated rather than buried: a hive's read policy now
grants the whole `services` prefix, because a service's path names the
service and nothing swarm-wide records which hive runs it (cost recorded
in docs/trust-boundary/security.md); and the client is registered on any
authelia host, because no swarm-wide "this swarm has a Grafana" fact
exists to gate it on.

Refs #4234

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 19:57:28 +02:00
atlas
fbd9afa7fa otel: persist journald cursor across collector restarts
The journald receiver runs with --lines=0, so every collector start
only ships what's written after it starts, and a restart silently
loses whatever landed while it was down. Point it at a file_storage
extension so the read cursor survives a restart; start_at stays at
its 'end' default since the cursor now covers everything after the
first run.

Refs #3818

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 19:55:00 +02:00
iris
74fb07c052 agent: fix non-interactive badges losing their pill shape in the status row 2026-09-13 19:33:55 +02:00
iris
e1858b277a swarm-ui: extend neon glow to the swarm brand name 2026-09-13 19:19:26 +02:00
iris
fefeaecbbf swarm-ui: neon glow on panel titles + the active nav tab
Ports the subtle text-shadow glow @hive/agent's and the dashboard's own
headline rule already use (both independently converged on
`text-shadow: 0 0 Npx color-mix(color, transparent)`), not their whole
uppercase+letter-spacing headline typography -- that's those UIs' own
look, swarm-ui never adopted it and this issue only asked for the glow.

- Panel titles: `currentColor`-based, since a panel title has no one
  fixed accent (some are dynamic, e.g. an agent's own name) -- a fixed
  --purple glow on plain --fg text would read as a mismatched halo.
- Active nav tab: each item's own `accent` (NAV_ITEMS), the same value
  its underline already uses, so the glow and the underline read as
  one accent rather than two that happen to agree.
2026-09-13 19:19:26 +02:00
damocles
16f86d24e7 ci: daily nix flake update workflow 2026-09-13 19:14:10 +02:00
damocles
6271d5db13 nix: inline the tls alias instead of a local let-binding 2026-09-13 18:55:19 +02:00
damocles
f09a942395 nix: point three modules' tlsCfg alias at the current hive-controller path 2026-09-13 18:55:19 +02:00
iris
9f8df505c6 swarm-ui: widen the agents page's shell body cap, same as issue report
AgentsPage's table view (and its list+detail split) hits the same
60em readable-line-length cap the issue report's wide table already
opts out of via shell-body-wide. Adds /agents to WIDE_BODY_ROUTES --
the allowlist's own comment already anticipated this, a future wide
page reaches for the same class rather than inventing its own cap.
2026-09-13 18:44:59 +02:00
iris
c43a457752 term_stream: drop hive from the URL, resolve it from agent_status
mara's review point on #4351: an agent isn't pinned to a hive forever
(it can move), so a URL naming one would go stale the moment it did.
Resolve the hive at request time from the agent-status bucket instead
-- the same source AgentStatusRow.hive already comes from -- rather
than trusting a caller-supplied value. Route is now
GET /api/agents/{name}/term/stream; a never-reported agent now answers
404 (no hive on record) instead of silently guessing.
2026-09-13 18:08:33 +02:00
iris
1d902a0992 swarm-controller: add GET /api/agents/{hive}/{name}/term/stream
hive-agent already publishes classified TermMsg rows to the core NATS
subject $SWARM.term.{hive}.{agent} -- live only, no retention, by
design. This endpoint subscribes that subject per request and relays
each row over SSE, opaque to this daemon (no TermMsg dependency, same
pass-through shape crate::status already uses for hive snapshots).

No replay/history: the publish side never grew JetStream retention, and
this route's own job (a live tail) never needed it.
2026-09-13 18:08:33 +02:00
damocles
77c3c656b2 subagents: add effort param, default to medium, document in skill 2026-09-13 17:45:42 +02:00
damocles
95898338fc hive-c0re: validate cascade agent names in meta_update_cascade_agents' fanout path too
extract validate_agent_names() and use it for both the parsed
agent-<name> inputs and run_meta_lock's pre-computed fanout list, so a
malformed name can't reach the new fast_forward_applied_main / lock_update
filesystem+git+forge-URL operations regardless of which of the two
sources it came from
2026-09-13 17:33:58 +02:00
damocles
c45d679a32 hive-c0re: fast-forward applied/<name>/main too, not just the one-shot relock
argus + mara (PR #4339 review): the previous commit's per-agent lock
relock is a one-shot effect on the single rebuild the cascade triggers
- applied/<name> never moves, so the next relock=true rebuild trigger
(the boot sweep, most notably) re-locks against applied/<name> and
reverts straight back to whatever it was stuck on. The fix didn't
outlive the transaction it ran in.

New forge::fast_forward_applied_main(name), sibling to the existing
reseed-only fetch_config_main_into_applied: for an applied repo that
already has a .git and just needs to catch up, force-set rather than
fast-forward-gated since there's no PR to review on this path either.
Called per cascade agent right after the relock, best-effort so one
unreachable agent repo doesn't block the others.

Once applied/<name>/main has actually moved, lock_update_for_rebuild's
override (always reads current applied/<name>/main, no ?rev pin)
naturally stays in sync on any later relock=true rebuild instead of
reverting.
2026-09-13 17:33:58 +02:00
damocles
f0ddbe49d0 hive-c0re: pull each meta-update cascade agent's own config-repo main
mara (hyperhive#4271): "whatever is on main is trusted and should be
pulled. dont make it periodic, just add it to the meta update when
choosing the agent."

A meta-input bump previously rebuilt every affected agent against
whatever applied/<name> was already locked to. Normally current, but
silently stale forever if a past deploy failed and nothing since
retried it — the manual meta-input trigger never special-cased that
either, since nothing wired it to check the config repo's live main at
all.

run_meta_lock now relocks each cascade agent's own input alongside the
originally-requested ones, against the input's declared source (the
forge URL) rather than the local applied/<name> mirror prepare_deploy
uses for a reviewed deploy — there's no PR to review on this path, so
nothing to gate. One combined lock_update call for the whole cascade:
simpler than per-agent isolation, at the cost of one broken/unreachable
agent repo failing the whole cascade relock rather than just that
agent (the separate top-level input bump above it is unaffected).

Known gap, not fixed here: applied/<name>'s own main/deployed/* tags
never advance from this path, only a real MergeConfigPr deploy does
that — so the audit trail stays as accurate as today, it just stops
being what actually got built.
2026-09-13 17:33:58 +02:00
atlas
e588b1803e hive-forge: comments total comes from the comments endpoint, not the issue object
fetch_total read issue_get_issue's comments field, Forgejo's running
counter on the Issue object that also increments for several
non-prose event kinds (review_request, issue_ref, comment_ref,
pull_push, ...) that issue_get_comments never returns. Diffing that
count against a fetched window reported drift that was never real: a
comment that would never be shown at any window size. Live repro on
PR #4333 (damocles's diagnosis on the issue): more_after: 2, both
events non-prose.

Fix: read the total straight off issue_get_comments's own
X-Total-Count header instead — the same endpoint that produces the
window, so the two populations can never drift apart again. Mirrors
timeline.rs's own fetch_total, which made the same move for its
endpoint first. fetch_head now captures the header from the request
it already makes (one fewer round trip in the --limit path); the
--tail/default path still probes once via page_size=1, same shape as
before but now scoped to the endpoint that actually produces the
window.

This also fixes the more serious half: fetch_tail derives its
pagination offset (which page holds the tail) from the same total, so
an inflated total didn't just skew the trailer's count, it could send
the offset math reaching for a page beyond the real thread's end,
returning fewer rows than requested or none at all when fetch_tail's
empty-page guard tripped. New test pins this against fetch_tail's
existing pagination-plan helper.

Refs #4335
2026-09-13 17:12:16 +02:00
iris
8464e50f3a swarm-grafana: surface subagent cost/turns on the agents dashboard
hive-subagent-mcp already stamps a subagent=<name> resource attribute
(alongside the parent's own agent=<name>) on every claude_code.* sample
a spawned subagent turn emits -- the label already existed, it just
wasn't plotted anywhere. Adds five panels: subagent cost, subagent
turns, subagent cost share (of all cost), cost by subagent name, and
cost by parent agent. No backend changes needed.
2026-09-13 16:40:20 +02:00
iris
ff97503367 swarm-grafana: add cost/tokens-by-agent panels to the claude usage dashboard
The dashboard already templated an $agent variable and broke turns and
active-time out by agent, but cost and token totals only split by
model, effort and query_source -- there was no panel answering "how
much did each agent spend/use", the aggregate-by-agent view mara asked
for.
2026-09-13 16:38:47 +02:00
damocles
3fad315ac3 hive-bash-mcp: drop the redundant output-path line from status's description
Same principle as the enum-enumeration fix, mara caught it in review:
the description said where full output lives, but format_task's own
answer already names the exact path when output was actually
truncated — the description doesn't need to say it too.
2026-09-13 15:56:10 +02:00
damocles
a54f711cc5 hive-bash-mcp: explain the status when returning it, not up front
Mirrors #4333's split on the sibling hive-subagent-mcp surface (mara's
ruling there, quoted on #4336: "dont explain all the possible states
that can be returned in the tool description. instead explain the
status when returning it").

status_explanation() carries what each TaskStatus means and what to do
about it, appended to every format_task answer. The status tool's
description shrinks to what it's for; a caller only ever sees one
state at a time and now learns what it means without cross-referencing
anything else.
2026-09-13 15:48:28 +02:00
atlas
ae84174e3a subagent daemon: explain the status when returning it, not up front
The `status` tool description enumerated all five states it can report, so
every caller paid for four answers it didn't get and read the explanation
in the wrong place. The description now states only what the tool is for
and that it costs nothing to call; each answer it returns carries its own
meaning and the caller's next move instead — running, starting and idle
were terse, the not-found error terser still, and they had been leaning on
the enumeration to be legible. The killed answer and the end-of-turn todo
are unchanged. `continue`'s description keeps its killed-resume sentence:
that describes what the tool does, not a state it might hand back.

Refs #4326
2026-09-13 15:23:25 +02:00
atlas
0bdee751b9 subagent daemon: address review on the killed-session docs
argus's review on #4333 flagged one real vale error (Microsoft.Contractions):
"did not" in the new "A killed turn" section reads as "didn't" instead. The
rest of the diff's new prose (docs/tools/subagent.md and the tool
descriptions in hive-subagent-mcp/src/mcp.rs) has no other instance of the
same expansion, confirmed both by a local `vale --minAlertLevel=error` run
(clean) and by grepping the added lines. Also tidies session.rs's module doc:
the two `//!` runs split to dodge the 30-line comment-block lint had a bare
trailing `//!` right before the blank separator; dropped so the first run
ends on content.

Refs #4326
2026-09-13 15:15:38 +02:00
atlas
f817e27d4c subagent daemon: report a killed session as killed, not idle
A subagent whose claude process died on a signal — the kernel's OOM
killer, a stopped unit, an `interrupt` — was indistinguishable from one
that finished its turn: its entry left the `running` map, `status` fell
through to "a session exists on disk" and answered `idle`, and the
end-of-turn todo said the subagent had "finished". The usual next move
on that reading is `continue`, which resumes work that was cut mid-turn
with nothing having recorded that it was cut.

The driver already preserves how the child ended — `RunningClaude::wait`
returns `Error::Exit` carrying the `ExitStatus`, whose `signal()` is the
whole answer — so this reads it rather than having to recover it:
`classify_end` turns the outcome into `Complete` / `Killed { signal }` /
`Failed`, and `State::finish_turn` remembers a kill against the name
(cleared by the next confirmed spawn under it).

What an agent sees as a result:

- `status` reports the session killed, naming the signal, instead of idle;
- the todo the daemon pushes without being asked says the subagent was
  KILLED mid-turn rather than that it finished;
- `continue` still resumes such a session, but its reply says the
  previous turn was killed, so no caller carries on from cut-off work
  believing it was complete.

Refs #4326
2026-09-13 14:58:45 +02:00
atlas
fde4a36b93 module-eval: pin the log store's machine query route and its absences
Three cases beside the existing ingest ones, since the read route has the
same trap and one property of its own.

- the route exists and carries `auth_request`;
- it has no login fallback — with the browser location as the positive
  control, so a pass means the two routes differ rather than that
  `error_page` and the login host are absent from the whole vhost;
- it injects no filter, on `extraConfig` and on `proxyPass` both, because
  VictoriaLogs takes its filters as request parameters and those ride an
  upstream URI as easily as a directive. A filter arriving later is then a
  visible diff here rather than a quiet change of rule.

Each absence arm leads with a clause that proves the location resolved and
that `hasInfix` finds what is really in that string, so "not there" cannot
be read off an unreadable path.

Refs #3870
2026-09-13 14:36:15 +02:00