The N3W SCH3DUL3 vertical form is gone. The schedules table now
carries an always-visible inline create row at the bottom — fill the
cells, click + to POST /api/schedules, the new schedule appears
above on the next refresh.
Per-column inputs match the display semantics:
- `next` → `<input type="datetime-local">` (defaults to now+5min)
- `every` → 4 mini d/h/m/s number inputs (blank/all-zero = one-shot)
- `body` → `<textarea rows=1>` expanding to 4em on focus-within
so multi-line prompts still fit (closes mara's "prompt may still be
multi-line" requirement)
- per-agent columns → checkbox (whole cell clickable via padded label)
- actions → + submit + ⌫ reset (clears the carry without POSTing)
`newScheduleCarry` module-scope object survives the paintAtomic
re-render so mid-typed values stick across schedule-list refreshes
(same pattern as `scheduleEditCarry` from #474).
H2 collapses from "N3W SCH3DUL3" + "QU3U3D SCH3DUL3S" down to a
single "SCH3DUL3S" since the section now does both. Empty-state
branch on `renderSchedulesList` is gone — the table always renders
because the create row is always there.
`#schedule-new-section` div + `renderScheduleNewForm` /
`readScheduleFormCarry` / `submitNewSchedule` and the
`.schedule-new-form` CSS selector all removed. The edit-form path
(#474 `renderScheduleEditForm` colspan'd row on `✎` toggle) is
unchanged — "edit mode is inline in the table" already shipped
there, this PR just adds the same mental model for create.
Validations match the old form: empty targets / empty body /
invalid datetime / non-integer interval each surface an `alert()`.
The + button shows a spinner while the POST is in flight, then
clears the carry + triggers `refreshSchedules()`.
The sticky bulk-action bar (#443) was visible on every tab whenever the
selection was non-empty. On Y3R C4LL / SYST3M / SCH3DUL3S the operator
sees a floating bar without the agent cards next to it for cross-
reference — which is what mara called out in #596.
Fix: gate the bar on `document.body.dataset.activeTab === 'swarm'` in
addition to the existing non-empty-selection check. Selection state
itself stays in memory, so the bar reappears on return to SW4RM if any
agents are still ticked. The `activateTab` hook now sets the data
attribute and re-runs `renderSelectionBar` so the toggle takes effect
on hashchange without waiting for the next SSE update.
mara on #563: 'we fixed the agent to not start turns in that
state (it fell back to online before), but this does not show on
dashboard properly'.
Root cause: the per-agent harness flips LoginState::NeedsLogin in
memory on three entry paths (cold-boot without a session, 401
mid-turn, /api/logout) and parks in wait_for_login. But
wait_for_login itself never called bus.emit_status('needs_login_idle')
at entry — only the /api/logout handler does that today. So:
- Cold boot: agent has no session, harness shows 'needs login'
on its own web UI (via LoginState mutex), but the dashboard's
needs_login field stays false because the
{state_dir}/hyperhive-needs-login sentinel was never written.
- 401 mid-turn: same — the 'after a turn failed' path in
hive-ag3nt.rs / hive-m1nd.rs flips LoginState directly without
emitting status, then calls wait_for_login, which now waits
silently with no sentinel write.
Fix: hoist the emit_status('needs_login_idle') call into
wait_for_login itself. All three entry paths get the sentinel
write for free; the /api/logout handler's explicit call (web_ui.rs
line 966) becomes redundant but idempotent — no behaviour change
there. The 'online' clear at session refresh stays exactly where
it was at the loop's exit.
Both hive-ag3nt and hive-m1nd binaries share wait_for_login, so the
manager harness benefits without a separate change.
Cargo's default test runner parallelises tests within a binary,
so the original 'tests run serially' comment was wrong — two
`with_env` calls running concurrently would race the
process-wide HIVE_LABEL / HYPERHIVE_HIVE_DOMAIN state.
Added a module-scope `static ENV_LOCK: Mutex<()>` and acquire it
at the top of `with_env` so each set / run / restore window is
exclusive. Poison recovery via `unwrap_or_else(into_inner)` so a
single test panic doesn't cascade through the rest of the module.
Lighter than pulling in serial_test for one module. No new deps.
First chunk of #589 v0 phase A: plumbing the hive-qualified
'name@hive' form through the per-agent surfaces that the harness
itself owns. Broker from/to + dashboard rendering + container_view
follow in subsequent PRs once damocles ships the HYPERHIVE_HIVE_DOMAIN
env var in harness-base.nix.
- new hive_ag3nt::identity module: label() / hive_domain() /
qualified_label() / qualify(label). Reads HYPERHIVE_HIVE_DOMAIN
(set by hive-c0re.nix module from hyperhive.domain) — when unset
or empty, qualified_label degrades to just the short label so
existing single-hive deployments are unchanged. Six unit tests
cover the set / unset / empty / arbitrary-label paths.
- prompt::render gains {qualified_label} substitution alongside
the existing {label}. system.md template uses both: the agent
intro now reads 'You are hyperhive agent iris (qualified:
iris@darkest.space) in a multi-agent system. ... When you're
talking to or about a peer on a different hive, use the
qualified form (name@hive) so the operator + the manager can
disambiguate'. Manager flavor gets the same treatment.
- /api/state gains qualified_label: String. Always present, equals
label when no domain is configured.
- frontend setHeader takes the qualified_label, drives the browser
tab title (so two tabs from different hives are
distinguishable in the tab bar) while the glyphic #title stays
short for the cinematic header.
Gated on env var presence — no behaviour change for single-hive
deployments. Pairs with damocles's upcoming harness-base.nix
HYPERHIVE_HIVE_DOMAIN ship; safe to land in either order.
Browsers may silently ignore autocomplete='off' on type='password'
inputs (UA override to help users save credentials). For an OAuth
code, the semantic value is 'one-time-code' which is honoured + has
the side benefit of suppressing the 'save password?' prompt that
would otherwise fire on form submit.
The login-in-progress screen's OAuth-code input was a plain text
field — anyone shoulder-surfing or capturing a screenshot of the
agent web UI would see the code in cleartext. Same risk applies
to dashboard share-screens during live demos.
Changes:
- input switches to type='password' so the pasted code renders
as bullets by default. Placeholder updated to '(hidden)' so the
operator knows the masking is intentional, not a browser quirk.
- new 'reveal' button (👁) next to the input flips the type back
to text on press, so the operator can sanity-check the paste
before submitting if she wants. aria-pressed reflects state.
- CSS for the reveal button mirrors the existing .btn-login amber
family — quiet by default, amber border/glow when pressed.
- spellcheck='false' on the input so browsers don't try to
underline the random-looking string as a typo.
The on-screen OAuth URL stays visible (the operator needs to
click it). The code is the secret leg — only the operator's
browser holds it, the URL is what was posted publicly to claude's
OAuth provider.
After PR #585 (closes#584) narrowed the backend wipe to just
.credentials.json + mcp-needs-auth-cache.json (preserving
projects/<hash>/*.jsonl session history + everything else under
~/.claude/), the dialog wording I shipped in 9d58ec3 (which
described a full-dir wipe to satisfy argus's review against the
PRE-#585 backend) is now stale.
Updated to match what the backend actually does:
- title + slash desc + both confirm dialogs now say 'rotate OAuth
credentials, --continue session history preserved'
- confirms explicitly name the two files deleted (.credentials.json,
mcp-needs-auth-cache.json) so the operator knows exactly what's
going + reassures that the conversation context survives
- 'agent picks up where it left off on the next turn after re-login'
tail makes the recoverability concrete
No code-path changes — just the wording. Backend behavior + endpoint
shape unchanged.
argus #583 review caught: the prior dialog wording said 'prior
--continue context is not affected (only the OAuth creds)', but
that's false — paths::claude_dir() is /root/.claude and
remove_dir_all wipes the projects/<hash>/*.jsonl session history
along with the OAuth creds.
Updated both the overflow-menu confirm and the /logout slash
command confirm to explicitly say the projects/*.jsonl session
history (--continue context) goes too, plus a 'no undo' tail so
the operator can't read past the consequence list. SLASH_COMMANDS
desc + overflow-item title also corrected so /help + tooltips
match.
Pairs with damocles PR #582 (POST /api/logout backend on the
per-agent web UI). Three additions to the agent's app.js:
- New `postLogout` helper next to postCancelTurn / postCompact /
postNewSession. Same postSimple shape.
- New entry in SLASH_COMMANDS so /help lists /logout.
- New /logout case in handleSlashCommand with window.confirm.
- New '🔓 logout' item in populateOverflowMenu's overflow popover,
mirroring the new-session item's pattern (confirm before POST,
disable button while in-flight, closeOverflowMenu before fire).
Confirm dialog spells out the consequences (SIGINT, creds wiped,
park in needs-login) so the operator doesn't accidentally drop a
production session. Tooltip on the menu item links the action back
to the credentials directory + post-logout state.
Wire-level: POST /api/logout, no body. Backend returns 200 with a
text body describing the wipe outcome — postSimple ignores it
(success → no terminal note; failure → red turn-end-fail row).
Backend route POST /api/rebuild-queue/{id}/cancel already exists
(refuses Running / terminal entries with {cancelled: false}). This
adds the operator-facing affordance:
- small circular X button on the right edge of each row whose
state === 'queued'. Running / done / failed rows don't render
it, so the operator never clicks a button that the backend
would refuse.
- uses the same data-async + data-confirm pattern as the
reminder cancel form — global submit handler does POST +
spinner + error toast for free.
- successful cancel flips the row queued -> cancelled via the
live RebuildQueueChanged snapshot, so the button disappears
on the next paint without an explicit refresh.
CSS keeps it quiet by default (muted border, transparent
background) and lights red on hover / focus, matching the
.btn-deny family without claiming a full button-width slot
that would push the row layout around.
Pairs with damocles PR #566 (broker primitive + dashboard
`POST /api/agent/{name}/mark-all-read` route). The agent's
per-container inbox side-panel now gets a header row with a
`✓ mark all read` button that:
- confirms via a one-line dialog (the action is destructive: any
pending broker message for this agent is acked, the harness
won't receive a wake-prompt for them)
- POSTs to the host dashboard (cross-origin, same pattern as the
existing operator-answer flow on this page)
- surfaces `{ marked: N }` in an inline status pill, then triggers
a `refreshState` so any state-derived surfaces re-read fresh
- stays out of the way when the inbox is empty (only renders above
a non-empty rows list)
Note: `recent_for` returns the most-recent-N messages regardless of
ack state, so clicking does NOT visually empty the rows list. The
status pill ("✓ marked N as read") is the operator-facing
confirmation; the next `turn_start` will show `0 unread` in its
badge. Tooltip on the button calls this out so the operator isn't
surprised the row list stays put.
CSS mirrors the existing answer-form button family (mauve hover on
bg-elev background) so it reads as a peer affordance, with a
border-bottom separating it from the message list.
Per mara's review on PR #561: the previous commit kept
`./hive-ag3nt/prompts` in `cleanSrc` because
`hive-ag3nt::prompt::tests` had a compile-time
`include_str!("../prompts/system.md")`. That meant a prompt edit
still busted the cargo cache.
This change:
- Replaces the test-side `include_str!` with a runtime read from
`$HIVE_ASSETS_DIR/prompts/system.md` (with a CARGO_MANIFEST_DIR
fallback for plain `cargo test` from a checked-out repo).
- Drops `./hive-ag3nt/prompts` from `cleanSrc` — it's now
`craneLib.cleanCargoSource ./.` (Cargo.* + *.rs only).
- Sets `doCheck = false` on `packages.default` and lifts
`cargo test` into a separate `checks.cargo-test` derivation
that carries the `hyperhive-assets` build input. That scopes the
asset rebuild blast radius to the test check — `nix flake check`
still exercises the suite, but the binary derivation no longer
carries the assets dep.
Verified cache-invariance matrix (via `echo '' >> <f>; nix eval
.#default.outPath`):
| edit | default | cargo-test | clippy |
|-------------------------|---------|------------|--------|
| README.md | stable | stable | stable |
| branding/hyperhive.svg | stable | CHANGED | stable |
| nix/modules/* | stable | stable | stable |
| prompts/system.md | stable | CHANGED | stable |
| hive-c0re/src/main.rs | CHANGED | CHANGED | CHANGED |
(`cargo-test` CHANGED on prompts/branding is correct — tests
read the production template + need the assets output.)
After the asset-split in the previous commit the rust derivations
have no compile-time dependency on `branding/*` and the only
remaining reference to `hive-ag3nt/prompts/` is a `#[cfg(test)]`
`include_str!` of `system.md` for the prompt-renderer tests. So we
can finally narrow the src input down from `./.` (the post-naersk-
port shape) to a fileset:
fileset = lib.fileset.unions [
(craneLib.fileset.commonCargoSources ./.) # *.rs + Cargo.{toml,lock}
./hive-ag3nt/prompts # cfg(test) include_str!
];
Same `cleanSrc` is fed into all three derivations
(`buildDepsOnly`, `buildPackage`, `cargoClippy`) so the input hash
stays consistent across the chain (no surprise cache misses
between stages of the same nix build).
Verified the cache-invalidation contract by `echo '' >> <file>`
and re-evaluating `.#default.outPath`:
README.md → unchanged ✓
branding/hyperhive.{svg,png} → unchanged ✓
hive-c0re/src/main.rs → invalidates ✓
hive-ag3nt/prompts/system.md → invalidates ✓ (cfg(test))
branding/agent-configs.svg → unchanged ✓
(assets derivation rebuilds
independently)
End state: a tweak to nix modules, frontend JS, docs, README, or
any branding asset rebuilds nothing rust-side. Only Rust source
changes and prompt edits invalidate the cargo cache — and the
prompt edit is gated to tests, so the production binary derivation
is invariant to it (a follow-up could move the `include_str!` into
its own test-only fixture if even that residual coupling matters,
but the operator-visible cost today is zero).
Closes#555.
Cuts every `include_bytes!`/`include_str!` of a non-rust path in
the workspace over to runtime file loads from `$HIVE_ASSETS_DIR`
(the `hyperhive-assets` derivation introduced in the previous
commit). After this commit the rust derivation has no compile-time
dependency on `branding/*` or `hive-ag3nt/prompts/*` anymore.
Call-site flips:
- `hive-c0re/src/forge.rs::CORE_AVATAR_PNG` /
`CONFIG_ORG_AVATAR_PNG`: were `include_bytes!` of
`branding/hyperhive.png` and `$OUT_DIR/agent-configs.png`. Now
`ensure_core_avatar` / `ensure_config_org_avatar` `tokio::fs::read`
via `hive_sh4re::assets::{core_avatar_png, config_org_avatar_png}`
at startup. The `agent-configs.png` is now rendered by the
`hyperhive-assets` derivation's rsvg-convert step (was
`hive-c0re/build.rs` + librsvg on the rust derivation's
nativeBuildInputs — both gone in the next commit).
- `hive-ag3nt/src/prompt.rs::TEMPLATE`: `render` now takes the
template as an argument; `write_system_prompt` reads it once from
`$HIVE_ASSETS_DIR/prompts/system.md` before calling render. The
test module still `include_str!`s the production template so
`cargo test --workspace` doesn't need `HIVE_ASSETS_DIR` set —
this is the only remaining compile-time reference to the file
from the rust workspace, gated to `#[cfg(test)]`.
- `hive-ag3nt/src/turn.rs::CLAUDE_SETTINGS`: was `include_str!`'d
and written via `tokio::fs::write`; now `tokio::fs::copy` from
`$HIVE_ASSETS_DIR/prompts/claude-settings.json` into the
per-agent socket dir.
- `hive-ag3nt/src/web_ui.rs::DEFAULT_ICON`: was `include_str!`'d;
now read on-demand from `$HIVE_ASSETS_DIR/branding/hyperhive.svg`
inside `serve_icon`. Falls back to an empty body if missing so
the endpoint never panics on a misconfigured container (matches
the existing "per-agent icon.svg override" fallthrough).
`HIVE_ASSETS_DIR` wiring:
- Inside containers: `nix/templates/harness-base.nix`
`environment.variables` sets it to
`${pkgs.hyperhive-assets}/share/hyperhive` (resolved through
the default overlay applied in `mkContainer`). Verified by
building `agent-base-toplevel` and grepping the resulting
`/etc/set-environment`.
- Host-side: `nix/modules/hive-c0re.nix` adds an `assets` option
defaulting to `hyperhive.packages.${system}.assets`, threaded
in from the flake's nixosModules wiring, and sets the same env
var on the `hive-c0re` systemd unit so the daemon's
`forge::ensure_*_avatar` startup hooks find the PNGs.
`hive-c0re/build.rs` deleted entirely; `[package].build` removed
from `hive-c0re/Cargo.toml`; rsvg-convert dependency lives in the
assets derivation only.
Validated: `nix build .#default .#checks.x86_64-linux.clippy
.#agent-base-toplevel .#manager-toplevel --fallback` all succeed.
`/etc/set-environment` in the toplevel shows
`HIVE_ASSETS_DIR="/nix/store/.../hyperhive-assets-0.1.0/share/hyperhive"`.
Hoists the project's branding/* + hive-ag3nt/prompts/* out of the
rust derivation's src set. Lives as `packages.<system>.assets` (also
exported as `pkgs.hyperhive-assets` via the default overlay).
Output layout:
$out/share/hyperhive/branding/{hyperhive,agent-configs}.{svg,png}
$out/share/hyperhive/prompts/{system.md,claude-settings.json}
`agent-configs.png` is rendered at build time from its SVG via
rsvg-convert — same shape as the old `hive-c0re/build.rs` rasteriser,
just hoisted into nix so the librsvg dependency stays *here* instead
of in the rust derivation.
No consumer change yet — the rust binaries still `include_bytes!`
their copies from the in-source paths; later commits in this PR cut
those over to runtime loads from `$HIVE_ASSETS_DIR/share/hyperhive/`.
Why split: `src = ./.;` on the crane derivation invalidates the
cargo cache on every edit to anything in the repo, including
branding tweaks + prompt edits + docs. Splitting these out is the
first step toward dropping the rust src input down to
`craneLib.cleanCargoSource ./.` (the eventual end-state in the
final commit of this PR).
The naersk → crane swap in the parent commit flips clippy from
silently passing to actually failing on `-D warnings` (naersk's
`mode = "clippy"` mangled the `--` separator so the deny never took
effect). This commit clears the surfaced lints so the workspace
builds clean under the new enforcement — every fix is mechanical and
preserves behaviour. Tests still pass (160 across the workspace).
Auto-fixes via `cargo clippy --fix`:
- `doc_markdown` (19 sites): bare identifiers in doc comments
wrapped in backticks
- `format_in_format_args`, `explicit_into_iter_loop`,
`redundant_closure_for_method_calls`, `useless_conversion`, and
a few more — mechanical rewrites of the kind cargo can apply
safely.
Hand-fixed:
- `match_same_arms` (forge_notify::is_atx_heading): two arms returning
`true` collapsed into a single `matches!` pattern.
- `cast_sign_loss` + `format_push_string` (mcp.rs status formatter):
guarded `i64 → u64` through `u64::try_from(…).unwrap_or(0)` (status
timestamps are always positive in practice; clamp the skew edge to
0) and swapped `out.push_str(&format!(…))` for `write!` into the
buffer with an infallible-writer `let _ =`.
- `doc_lazy_continuation` in turn.rs + manager_server.rs + sh4re/lib.rs:
doc paragraphs that the markdown parser was treating as list-item
continuations got either a separating blank line or a `/`-for-`+`
word swap so the parser stops seeing a list.
- `unused_async` (manager_server::handle_request_schedule_prompt):
function has no `.await`; dropped the `async` and its `.await` call
site.
- `needless_pass_by_value` (scheduled_prompts::submit): take
`&NewSchedule` instead of moving the struct in; updated two prod
callers and eight test sites to pass references.
- `type_complexity` (approvals::mark_cancelled): hoisted the
7-tuple SELECT row shape into a `type CancelLookupRow = (…);` alias.
Allow-with-reason for intentional patterns:
- `option_option` (6 sites across dashboard / scheduled_prompts /
manager_server): `Option<Option<T>>` carries three-state PATCH
semantics (missing key = leave alone, `Some(None)` = clear,
`Some(Some(v))` = set). Collapsing to `Option<T>` loses the
"clear" state.
- `dead_code` (rebuild_queue::QueueKind::Destroy /
QueueSource::CrashRecover; topology::parent_of / default_seed):
wire-shape variants + API surfaces kept for the upcoming features
(#361 follow-ups, future `Destroy` queue routing, crash-recovery
path). Allowed at the variant / function level with the rationale
in `reason = "…"`.
- `too_many_lines` on three specific call-sites: a 117-line
exhaustive-variant test (dashboard_events::kind_tag_matches_…),
the meta-flake string template renderer
(meta::render_flake_with_lookup), and the notification poll loop
(forge_notify::poll_once) — splitting any of them would just hide
the contiguous shape they exist to keep visible.
`nix flake check` formatting target is still broken on main itself
(pre-existing nixfmt drift across ~28 files unrelated to this PR);
left alone here so the scope stays "crane port + lints the port
exposed" and the operator's review doesn't have to triage drive-by
nixfmt churn.
Framework swap, no public API change.
- naersk input → crane (`github:ipetkov/crane`); crane is stateless, no
nixpkgs.follows needed.
- `forAllSystems` exposes `craneLib = crane.mkLib pkgs`,
`cargoArtifacts = craneLib.buildDepsOnly` (built once, reused), and
a shared `nativeBuildInputs = [ pkgs.librsvg pkgs.git ]` consumed by
buildDepsOnly + buildPackage + cargoClippy so the three derivations
see the same toolchain shape.
- `packages.default = craneLib.buildPackage` (was naersk-lib.buildPackage)
with explicit `pname = "hyperhive-workspace"; version = "0.1.0";` —
the virtual workspace Cargo.toml has no [package].name so crane
needs the hint.
- `checks.clippy = craneLib.cargoClippy` (was naersk + overrideAttrs
hack). Crane parses `cargoClippyExtraArgs = "--workspace --all-targets
-- -D warnings"` correctly; naersk's `mode = "clippy"` used to mangle
the `--` separator which is why the old wiring went through
overrideAttrs. The whole hack — including `doCheck = false`,
`copyTarget = false`, and the swapped buildPhase/installPhase — is
now gone.
- librsvg native dep (#424) preserved on all three derivations. Added
pkgs.git too — naersk auto-included it; crane is more minimal, so
hive-c0re's `lifecycle::tests::setup_proposed_*` (which shell out to
`git init`+commit) need it explicit to pass under `cargo test` in
the sandbox.
- build.rs + hive-c0re/Cargo.toml comments updated from "naersk
derivation" to "crane derivation".
- 3 doc-list-indentation lints in hive-sh4re/src/lib.rs cleaned up
(replaced `+`-at-line-start with `and`/`/` so doc continuations
don't trigger `clippy::doc_lazy_continuation`).
Validated locally: `nix build .#default --fallback` succeeds, all
117 tests pass, all four bins in `result/bin/`.