Commit graph

826 commits

Author SHA1 Message Date
müde
c53640040a fix agent crash on start 2026-05-29 16:19:46 +02:00
damocles
7e041e6079 harness-base: matrix-avatar-sync oneshot mirroring forge avatar (#548 phase 2.5) 2026-05-29 13:18:23 +02:00
damocles
666c32ae65 broker: add mark_all_read + POST /api/agent/{name}/mark-all-read (#559 backend half) 2026-05-29 13:14:31 +02:00
iris
fe2f9c2396 agent: "mark all read" button on the inbox flyout (#559 frontend half)
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.
2026-05-29 13:14:06 +02:00
damocles
11db710ddf hive-matrix: refresh stale bindMounts comment after activation-script fix (argus) 2026-05-29 13:13:13 +02:00
damocles
dfec976461 matrix: activationScript pre-creates token + share reqwest client across sweep (argus #565 nits) 2026-05-29 13:13:13 +02:00
damocles
50ceb929d7 matrix: provision per-agent accounts on startup via UIAA registration (#548 phase 2) 2026-05-29 13:13:13 +02:00
iris
b3f675e367 flake: lift cargo test out of buildPackage so prompt edits don't bust the binary cache (#555)
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.)
2026-05-29 12:59:48 +02:00
iris
0058ed1e59 flake: narrow crane src to cleanCargoSource + prompts (closes #555)
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.
2026-05-29 12:59:48 +02:00
iris
73684fb00a rust+nix: load static assets at runtime, drop build.rs (#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"`.
2026-05-29 12:59:48 +02:00
iris
246dd19390 nix: split static runtime assets into their own derivation (#555)
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).
2026-05-29 12:59:48 +02:00
damocles
dfb2595a50 hive-matrix: fix tuwunel option types (address/port lists, max_request_size int) 2026-05-29 02:48:50 +02:00
lexis
c580b7ce6d docs: fix tuwunel upstream URL + clarify domain/serverName requirement 2026-05-29 02:24:13 +02:00
lexis
249635430d docs: add hyperhive.domain + matrix module to README host config (follow-up to #552) 2026-05-29 02:22:16 +02:00
damocles
e3c9715220 cargo: regenerate Cargo.lock to add transitive deps surfaced by resolver=3 (closes #556) 2026-05-29 02:03:38 +02:00
lexis
b0e35687ae nix: fix stale useSubdomain reference in hyperhive.domain description 2026-05-29 01:55:16 +02:00
iris
9ed58ab96d clippy: fix lints that crane's cargoClippy properly enforces (#538)
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.
2026-05-29 01:45:48 +02:00
iris
4b6c733afb flake: replace naersk with crane (#538)
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/`.
2026-05-29 01:45:48 +02:00
damocles
b652e6a6b0 hive-matrix: always use matrix.<domain> subdomain (mara on #552) 2026-05-29 01:31:40 +02:00
damocles
5a4eb3e053 hive-matrix: stateVersion 26.05 + drop premature federationPort firewall (argus nits #552) 2026-05-29 01:28:46 +02:00
damocles
2b1c1b54ac nix: add hive-matrix module + hyperhive.domain option (#548 part 1) 2026-05-29 01:25:29 +02:00
damocles
b43438d1a6 hive-forge: collapse autogenerated diffs to a single one-line summary (#222) 2026-05-29 01:16:51 +02:00
lexis
c9f537c2e3 docs: forge_notify.rs file map + wait_for_login mtime resumption (follow-up to #544 #545) 2026-05-29 00:34:11 +02:00
lexis
4a2b2fca4f docs(web-ui): logs flyout fills side-panel height (follow-up to #543) 2026-05-29 00:22:19 +02:00
damocles
994166b20c turn: snapshot file_count alongside mtime to survive unreadable meta.modified() 2026-05-28 20:08:27 +02:00
damocles
6e833b22d6 turn: wait_for_login resumes only on credentials mtime bump (closes #542) 2026-05-28 20:08:27 +02:00
damocles
b5be139fb7 forge_notify: diff mentions against raw excerpt to avoid heading-mention duplication 2026-05-28 20:08:05 +02:00
damocles
4b30f20dcf forge_notify: embed issue/pr body excerpt + surface truncated mention lines (closes #539) 2026-05-28 20:08:05 +02:00
iris
8bbb3e4905 tabs: logs flyout fills the side-panel height (closes #541)
The journald viewer opens in the side panel, but the <pre> only takes
the height of its content — so a short log fetch leaves the bottom 80%
of the panel empty, and a long one pushes the controls past the
viewport. Wrap the body in a column-flex container that fills the
side-panel-body, keep controls fixed at the top, and let the <pre>
flex-grow into the rest of the panel with its own overflow:

  .journal-body { display: flex; flex-direction: column; height: 100%; }
  .journal-controls { flex: 0 0 auto; }
  .journal-output { flex: 1 1 0; min-height: 0; overflow: auto; }

`min-height: 0` is the canonical "let me actually flex-shrink for
overflow" escape hatch on flex children. Also moved the auto-scroll
target from side-panel-body onto the <pre> itself — the panel-body no
longer overflows now that journal-body fills it; the <pre> is the new
scroll surface.

Pure CSS + one JS line; no DOM shape change.
2026-05-28 18:54:02 +02:00
damocles
2d9109d1bd docs(web-ui): describe new single-table schedules layout (#124#535) 2026-05-28 15:25:32 +02:00
iris
42a2503834 tabs: render schedules as a single table with tilted agent columns (closes #535)
Replaces the per-schedule card layout with one table:

  | # | src | next | every | owner | body | …agents… | actions |
  |---|-----|------|-------|-------|------|----------|---------|
  | 5 | op  | 5m   | 10m   | mara  | "…"  | ✓ ✓ . ✓ |  ↯ ✎ ✕  |

Each schedule is one <tr> in the tbody. Agent columns are dynamic —
operator + manager + live containers + any "extra" name that appears
as a target on some schedule but isn't a current container (same
membership rule buildTargetChips uses, so the table + new/edit forms
agree on what's addressable). Per-agent cells:

  - active target  → <button>✓</button> that cancels just that target
    on click (replaces the per-row ✕ from the old targets sub-table)
  - cancelled target → muted ✕ glyph (no button; re-add flows through
    the edit form's targets multi-select)
  - not a target → empty cell

Agent column headers tilt -45° via CSS so each column reads as 28px
of horizontal real estate instead of the full word width. Standard
rotated-header pattern: 95px-tall <th> with position:relative, inner
<div> positioned absolute at bottom-left, transform rotates about
left-bottom.

Body cell truncates with ellipsis + full-text title. Description used
to be a separate visible block on the card layout; the table folds
it into the body cell's title to keep row height tight. If mara wants
description visible in-table it's a small follow-up — easier to
iterate on a rejection.

Edit form expands inline into a colspan'd row underneath the schedule
row it edits (instead of inside the card). Wrapper drops the form's
background so it reads as a row extension.

No backend changes; everything renders from existing schedulesState
+ containersState.
2026-05-28 14:22:39 +02:00
iris
78a503d8b9 nix/frontend: refresh stale dist-layout comment
The output-layout block was written before:
- the dashboard's app.js → tabs.js rename (#495)
- the flow.html / flow.js page (#406 / #485)
- the SharedWorker stream-worker.js (#448)
- the build.mjs split-into-static/ subdir convention

Brings the comment in line with what `find dist -type f` actually
prints for both packages. Pure documentation refresh; install phase
and build hashes untouched.
2026-05-28 14:17:52 +02:00
iris
bc1c910044 tabs: extract scheduleField() helper — collapse 8 label/caption sites
Every field on the new- and edit-schedule forms (plus buildIntervalComposer
and buildTargetChips) builds the same wrapper shape:

    el('label', { class: 'schedule-field' },
      el('span', { class: 'schedule-field-label' }, '…'))

then appends an input. Eight identical-modulo-text sites. Extracting one
small helper:

    function scheduleField(labelText, ...children) {
      return el('label', { class: 'schedule-field' },
        el('span', { class: 'schedule-field-label' }, labelText),
        ...children);
    }

drops the per-site cost to a single `form_.append(scheduleField('caption',
input));` line. -10 LOC net; no behaviour change.
2026-05-28 14:13:35 +02:00
damocles
1d7feeb029 hive-forge: pr-create --push opts into branch push + hint suppression (#222) 2026-05-28 01:08:43 +02:00
damocles
def06fb388 docs(web-ui): SharedWorker heartbeat + watchdog (#124 follow-up to #515) 2026-05-28 00:57:15 +02:00
damocles
800d9925a5 prompt: matched-close check + drop dead-code guard (argus #527 nits) 2026-05-28 00:33:43 +02:00
damocles
1c8e72c81f docs(CLAUDE.md): file map reflects unified prompts/system.md + src/prompt.rs 2026-05-28 00:33:43 +02:00
damocles
01af5003d1 harness: unify agent + manager prompts into single template (closes #519) 2026-05-28 00:33:43 +02:00
iris
88476976b7 stream-worker: honest comment on heartbeat catch (argus nit on #529)
argus pointed out the inline catch comment claimed dead ports get cleaned up on
next subscribe, but nothing actually prunes the allPorts Set on subscribe — the
honest answer is the one already at the bottom of onconnect: dead entries are
left in the Set, the bound cost is acceptable, and postMessage's throw is the
ambient signal we use. Point at that comment instead of repeating a wrong
description.
2026-05-27 23:40:38 +02:00
iris
b9df538940 frontend/dashboard: heartbeat + watchdog so a dead SharedWorker self-heals (#515)
Firefox kills "idle" SharedWorkers under memory pressure with no native
signal to the client. The page's port silently becomes a no-op and
events stop flowing — observable symptom: mara's "dashboard never
refreshes; F5 fixes it" (because F5 creates a fresh page that creates
a fresh worker).

The worker now pings every connected port every 30s. The client tracks
last-activity-from-worker on every message arrival (incl. pings, since
those carry no URL — bumped before the URL filter in the route handler).
A visibility-gated watchdog polls every 15s; if the page is visible AND
has active subs AND hasn't heard from the worker in >90s, it presumes
the worker dead, logs a console warning, and re-subscribes on a fresh
port. Three pings missed before we act, so a normal tab-throttle blip
doesn't false-positive.

The fresh-port re-subscribe re-uses the bfcache-restore code path
(same shape: drop stale listeners, getSharedPort → new SharedWorker,
re-attach each route + repost subscribe). Recovery is per-tab — when
one tab's watchdog fires and brings up a new worker, other tabs that
share the named worker pick it up on their own watchdog cycle.

Falls back gracefully on environments without SharedWorker (the
existing direct-EventSource path is untouched) and is invisible on the
healthy path — pings are 30s apart, no UI surface.
2026-05-27 23:37:21 +02:00
damocles
c786b9ec37 docs(persistence): broker.sqlite is six tables now (#124 follow-up) 2026-05-27 23:07:15 +02:00
lexis
29d8c264ca docs: update README flake example to nixos-26.05 2026-05-27 22:12:32 +02:00
damocles
308b2b4773 docs: point upstream URL at public mirror at forge.darkest.space (closes #523) 2026-05-27 22:09:42 +02:00
damocles
7d4811504a forge_notify: strict ATX detection + preserve trailing newline (argus nits on #518) 2026-05-27 19:39:39 +02:00
iris
e8c86bef3b tabs: dedup target-chips + interval-parse between new/edit schedule forms
mara: \"look through the code for dedups, structural improvements
and so on\". Two near-identical blocks across the new-schedule and
edit-schedule forms folded into shared helpers.

## `buildTargetChips({ idPrefix, fieldName, checked, extraNames })`

Was inlined twice in 18-line blocks that built the same
`<label class="schedule-field">` + `<div class="schedule-targets">`
+ candidate-list logic (containers + operator + manager). Now one
function, two callers; `extraNames` lets the edit form keep
showing already-active targets that have vanished from the live
container list so the operator can still uncheck them
intentionally.

## `intervalSecondsFromFormData(fd, namePrefix)`

Both submit handlers had the same ~13-line d/h/m/s → total-seconds
parser (with `NaN` propagation on bad input). Pulled into one
helper next to `buildIntervalComposer`; call sites become 3 lines:

  const intervalTotal = intervalSecondsFromFormData(fd, 'interval_');
  if (Number.isNaN(intervalTotal)) { alert(...); return; }
  const interval_seconds = intervalTotal > 0 ? intervalTotal : null;

Net -13 lines, but the bigger win is shape — when (not if) a new
schedule field surfaces, there's one chip-render path + one
interval-parser to thread it through instead of two.

Zero behaviour change. Built clean.
2026-05-27 19:22:59 +02:00
damocles
12f2c8311e forge_notify: escape ATX headings in embedded body so they don't blow into wrapper h1/h2 (closes #455) 2026-05-27 19:22:39 +02:00
damocles
0460bbe9ec docs: ContainerCrash mentions recent-transient grace (#124 followup to #425) 2026-05-27 19:19:08 +02:00
damocles
dca62fda40 harness: remove deprecated manager-side request_spawn surface (closes #442) 2026-05-27 18:58:04 +02:00
damocles
03d39788d9 prompts: cancel_loose_end approval kind + lint verb in hive-forge list (#124) 2026-05-27 17:06:40 +02:00
damocles
dfc944d5fa flake: expose agent-base + manager toplevels as packages, opt-in pre-build via hive-c0re module (closes #97) 2026-05-27 14:37:50 +02:00