diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md
index 440ff87b..52497f16 100644
--- a/docs/web-ui/dashboard.md
+++ b/docs/web-ui/dashboard.md
@@ -480,20 +480,15 @@ re-renders the terminal row. Manager is addressed as `@root`.
## H0M3 page (`/`)
The H0M3 hub is the primary landing page (served at `/` by default). A
-responsive grid of link tiles — Dashboard, Flow, Logs, Matrix (when enabled),
-Forge (when enabled) — each pointing to their respective surfaces. The page
-is a pure portal with no tab-bar or SSE subscriptions. Typography + colours
-inherit from the shared theme (Catppuccin Mocha via `common.css` + `theme.css`).
-Optional tiles are hidden until `home.js` confirms their availability:
-Matrix is hidden until `home.js` confirms `matrix_gui_enabled` (same gating as
-the dashboard's M4TR1X tab); Forge is hidden until `home.js` confirms
-`state.forge_present` and fills the href from `state.forge_public_url` (the
-gateway-served public URL when `services.hyperhive.forge.behindGateway=true`)
-or falls back to the direct `:3000` port. Operators without matrix or forge
-enabled never see dead links. `home.js` also fills the swarm/hive identity
-line at the top. Dashboard is now served at `/dashboard.html` (route swap
-completed in #1464 step 2); the home page at `/` replaces the old dashboard
-root. All dashboard sub-pages include a `← Home` back-link for navigation.
+responsive grid of link tiles — Dashboard, Flow, Logs, Matrix (when enabled)
+— each pointing to their respective surfaces. The page is a pure portal with
+no tab-bar or SSE subscriptions. Typography + colours inherit from the shared
+theme (Catppuccin Mocha via `common.css` + `theme.css`). The Matrix tile is
+hidden until `home.js` confirms `matrix_gui_enabled` (same gating as the
+dashboard's M4TR1X tab); `home.js` also fills the swarm/hive identity line
+at the top. Dashboard is now served at `/dashboard.html` (route swap completed
+in #1464 step 2); the home page at `/` replaces the old dashboard root. All
+dashboard sub-pages include a `← Home` back-link for navigation.
## L0GS page (`/logs.html`)
diff --git a/frontend/packages/agent/src/app.js b/frontend/packages/agent/src/app.js
index 3798351a..53feb7e5 100644
--- a/frontend/packages/agent/src/app.js
+++ b/frontend/packages/agent/src/app.js
@@ -756,16 +756,6 @@ window.marked = marked;
const h = Math.floor(m / 60);
return h + 'h ' + (m % 60) + 'm';
}
- // Wall-clock HH:MM:SS (UTC, matching the inbox timestamps on this page)
- // from a unix-seconds value. Used to label turn-start / turn-end rows
- // when the event carries a `ts` (see the turn renderers below).
- function fmtClock(sec) {
- return new Date(sec * 1000).toISOString().slice(11, 19);
- }
- // Unix-seconds stamp of the most recent open turn-start, so the
- // matching turn-end can show a duration. Turns are sequential, so a
- // single slot is enough (history replays chronologically too).
- let pendingTurnStartTs = null;
const STATE_TOOLTIPS = {
loading: 'harness not yet contacted',
offline: 'harness unreachable or claude not logged in',
@@ -1707,16 +1697,6 @@ window.marked = marked;
if (api.fromHistory) openTurnsFromHistory += 1;
else { setBannerActive(true); setState('thinking'); }
const block = api.row('turn-start', '◆ TURN ← ' + ev.from);
- // Turn start time. Guarded on a numeric `ts` (unix seconds)
- // so the row degrades to its old text-only form until the
- // backend surfaces per-event timestamps.
- if (typeof ev.ts === 'number') {
- pendingTurnStartTs = ev.ts;
- const t = document.createElement('span');
- t.className = 'turn-time';
- t.textContent = '· ' + fmtClock(ev.ts);
- block.appendChild(t);
- }
if (ev.unread > 0) {
const badge = document.createElement('span');
badge.className = 'unread-badge';
@@ -1737,23 +1717,9 @@ window.marked = marked;
refreshLooseEnds();
}
const cls = ev.ok ? 'turn-end-ok' : 'turn-end-fail';
- const row = api.row(cls,
+ api.row(cls,
(ev.ok ? '✓' : '✗') + ' turn ' + (ev.ok ? 'ok' : 'fail')
+ (ev.note ? ' — ' + ev.note : ''));
- // Turn end time + duration since the paired turn-start.
- // Same `ts` guard as turn_start; duration only when we saw the
- // matching start's stamp.
- if (typeof ev.ts === 'number') {
- const t = document.createElement('span');
- t.className = 'turn-time';
- let label = '· ' + fmtClock(ev.ts);
- if (pendingTurnStartTs != null && ev.ts >= pendingTurnStartTs) {
- label += ' · ' + fmtAge((ev.ts - pendingTurnStartTs) * 1000);
- }
- t.textContent = label;
- row.appendChild(t);
- }
- pendingTurnStartTs = null;
},
note(ev, api) {
const t = String(ev.text || '');
diff --git a/frontend/packages/dashboard/src/home.js b/frontend/packages/dashboard/src/home.js
index 2e216ce0..95112e4b 100644
--- a/frontend/packages/dashboard/src/home.js
+++ b/frontend/packages/dashboard/src/home.js
@@ -27,18 +27,6 @@ async function init() {
if (tile) tile.hidden = false;
}
- // Forge tile: reveal + point at the live forge only when the
- // hive-forge container is up. Prefer the gateway-served public URL
- // (set when forge.behindGateway=true), fall back to the direct :3000
- // port — same precedence the dashboard uses for forge links.
- if (state.forge_present) {
- const tile = $('home-tile-forge');
- if (tile) {
- tile.href = state.forge_public_url || `http://${location.hostname}:3000`;
- tile.hidden = false;
- }
- }
-
const ident = $('hive-identity');
if (ident && (state.swarm_name || state.hive_name)) {
const parts = [state.swarm_name, state.hive_name].filter(Boolean);
diff --git a/frontend/packages/dashboard/src/index.html b/frontend/packages/dashboard/src/index.html
index 64021b66..669b40a1 100644
--- a/frontend/packages/dashboard/src/index.html
+++ b/frontend/packages/dashboard/src/index.html
@@ -68,18 +68,6 @@
operator-local prefs · browser notifications
-
-
-
- ⚒
- Forge
-
- issues · pull requests · agent-config repos
-
-
diff --git a/frontend/packages/shared/src/terminal.css b/frontend/packages/shared/src/terminal.css
index 37413827..2cfe7dee 100644
--- a/frontend/packages/shared/src/terminal.css
+++ b/frontend/packages/shared/src/terminal.css
@@ -100,10 +100,6 @@
.live .row .md, .live .row > details { text-indent: 0; }
.live .turn-end-ok { color: var(--green); border-left-color: var(--green); }
.live .turn-end-fail { color: var(--red); border-left-color: var(--red); }
-/* Wall-clock time (+ duration on turn-end) appended to the turn-start /
- turn-end rows. Dim + smaller so the boundary glyph stays the focus and
- the timestamp reads as metadata. */
-.live .turn-time { color: var(--muted); font-size: 0.85em; margin-left: 0.5em; }
.live .text { color: var(--fg); }
.live .thinking { color: var(--muted); font-style: italic; }
.live .tool-use { color: var(--cyan); }
diff --git a/hive-ag3nt/src/events.rs b/hive-ag3nt/src/events.rs
index a4e15a07..0acc3bd5 100644
--- a/hive-ag3nt/src/events.rs
+++ b/hive-ag3nt/src/events.rs
@@ -169,23 +169,6 @@ CREATE INDEX IF NOT EXISTS idx_events_ts ON events (ts);
#[derive(Debug, Clone, Serialize)]
pub struct BusEvent {
pub seq: u64,
- /// Unix seconds at emit time. Serialized as a sibling of the `kind`
- /// tag so the agent terminal can render turn start/end times (and
- /// turn duration) on the live stream; history rows carry the same
- /// `ts` field sourced from the persisted `events.ts` column, so the
- /// renderer reads `ts` identically for live + scrollback.
- pub ts: i64,
- #[serde(flatten)]
- pub event: LiveEvent,
-}
-
-/// A persisted event paired with its stored unix-seconds timestamp.
-/// Serializes with `ts` as a sibling of the `kind` tag — same wire shape
-/// as a live [`BusEvent`] minus `seq` — so the agent terminal reads `ts`
-/// identically whether an event arrives live or is replayed from history.
-#[derive(Debug, Clone, Serialize)]
-pub struct StoredEvent {
- pub ts: i64,
#[serde(flatten)]
pub event: LiveEvent,
}
@@ -298,7 +281,7 @@ impl EventStore {
Ok(())
}
- fn recent(&self, limit: usize) -> rusqlite::Result> {
+ fn recent(&self, limit: usize) -> rusqlite::Result> {
let (events, _, _) = self.page(None, limit)?;
Ok(events)
}
@@ -310,52 +293,43 @@ impl EventStore {
&self,
before_id: Option,
limit: usize,
- ) -> rusqlite::Result<(Vec, Option, bool)> {
+ ) -> rusqlite::Result<(Vec, Option, bool)> {
let limit_i = i64::try_from(limit).unwrap_or(i64::MAX);
let conn = self.conn.lock().unwrap();
// Fetch one extra row so we can tell whether more exist.
let fetch = limit_i.saturating_add(1);
- // `ts` is the persisted emit-time unix-seconds stamp; carried out
- // alongside each event so history replay shows the same turn
- // start/end times the live stream did.
- let rows: Vec<(i64, StoredEvent)> = if let Some(bid) = before_id {
+ let rows: Vec<(i64, LiveEvent)> = if let Some(bid) = before_id {
let mut stmt = conn.prepare(
- "SELECT id, ts, payload_json FROM events
+ "SELECT id, payload_json FROM events
WHERE id < ?1
ORDER BY id DESC
LIMIT ?2",
)?;
stmt.query_map(params![bid, fetch], |row| {
let id: i64 = row.get(0)?;
- let ts: i64 = row.get(1)?;
- let s: String = row.get(2)?;
- Ok(serde_json::from_str::(&s)
- .ok()
- .map(|event| (id, StoredEvent { ts, event })))
+ let s: String = row.get(1)?;
+ Ok(serde_json::from_str::(&s).ok().map(|e| (id, e)))
})?
.flatten()
.flatten()
.collect()
} else {
let mut stmt = conn.prepare(
- "SELECT id, ts, payload_json FROM events
+ "SELECT id, payload_json FROM events
ORDER BY id DESC
LIMIT ?1",
)?;
stmt.query_map(params![fetch], |row| {
let id: i64 = row.get(0)?;
- let ts: i64 = row.get(1)?;
- let s: String = row.get(2)?;
- Ok(serde_json::from_str::(&s)
- .ok()
- .map(|event| (id, StoredEvent { ts, event })))
+ let s: String = row.get(1)?;
+ Ok(serde_json::from_str::(&s).ok().map(|e| (id, e)))
})?
.flatten()
.flatten()
.collect()
};
let has_more = rows.len() > limit;
- let mut rows: Vec<(i64, StoredEvent)> = rows.into_iter().take(limit).collect();
+ let mut rows: Vec<(i64, LiveEvent)> = rows.into_iter().take(limit).collect();
rows.reverse(); // oldest first
let min_id = rows.first().map(|(id, _)| *id);
let events = rows.into_iter().map(|(_, e)| e).collect();
@@ -1029,7 +1003,6 @@ impl Bus {
}
let envelope = BusEvent {
seq: self.next_seq(),
- ts: now_unix(),
event,
};
// Lagged subscribers drop events — fine; the UI is a tail, not a log.
@@ -1045,7 +1018,7 @@ impl Bus {
/// Drives the terminal pre-fill when the operator opens the agent
/// page; without a store (db open failed) this is empty.
#[must_use]
- pub fn history(&self) -> Vec {
+ pub fn history(&self) -> Vec {
let Some(store) = &self.store else {
return Vec::new();
};
@@ -1062,7 +1035,7 @@ impl Bus {
&self,
before_id: Option,
limit: usize,
- ) -> (Vec, Option, bool) {
+ ) -> (Vec, Option, bool) {
let Some(store) = &self.store else {
return (Vec::new(), None, false);
};
@@ -1078,41 +1051,9 @@ impl Default for Bus {
#[cfg(test)]
mod tests {
- use super::{
- BusEvent, DEFAULT_EFFORT, EFFORT_LEVELS, LiveEvent, StoredEvent, TokenUsage,
- is_valid_effort,
- };
+ use super::{DEFAULT_EFFORT, EFFORT_LEVELS, TokenUsage, is_valid_effort};
use serde_json::json;
- #[test]
- fn stored_event_serializes_ts_beside_kind() {
- // History-row wire shape: `ts` is a flattened sibling of `kind`,
- // which is what the agent terminal reads to time turn boundaries.
- let v = serde_json::to_value(StoredEvent {
- ts: 1_700_000_000,
- event: LiveEvent::Note { text: "hi".into() },
- })
- .unwrap();
- assert_eq!(v["ts"], 1_700_000_000_i64);
- assert_eq!(v["kind"], "note");
- assert_eq!(v["text"], "hi");
- }
-
- #[test]
- fn bus_event_serializes_ts_and_seq_beside_kind() {
- // Live SSE frame: same `ts` sibling as history (plus `seq`), so the
- // renderer is path-agnostic between live + scrollback.
- let v = serde_json::to_value(BusEvent {
- seq: 7,
- ts: 1_700_000_000,
- event: LiveEvent::Note { text: "yo".into() },
- })
- .unwrap();
- assert_eq!(v["seq"], 7);
- assert_eq!(v["ts"], 1_700_000_000_i64);
- assert_eq!(v["kind"], "note");
- }
-
#[test]
fn effort_validation_accepts_only_known_levels() {
for level in EFFORT_LEVELS {
diff --git a/hive-ag3nt/src/forge_notify.rs b/hive-ag3nt/src/forge_notify.rs
index c5e0f4b4..607b79a3 100644
--- a/hive-ag3nt/src/forge_notify.rs
+++ b/hive-ag3nt/src/forge_notify.rs
@@ -46,7 +46,7 @@ pub async fn run(socket: PathBuf) {
let token_path = format!("{state_dir}/forge-token");
// Retry reading the token to handle races where hive-priv provisions the
// token after the harness starts, or where a parent-container chown briefly
- // makes the file unreadable. We wait up to
+ // makes the file unreadable (see #1304 / #1309). We wait up to
// TOKEN_RETRY_MAX * TOKEN_RETRY_SECS before giving up.
let token = {
let mut attempts = 0u32;
@@ -205,14 +205,14 @@ fn escape_md_headings(body: &str) -> String {
/// Strict `CommonMark` ATX-heading detector: 1-6 leading `#`s followed
/// by either a space, tab, or end-of-line. Anything tighter (`#tag`,
-/// `#9`) is a non-heading line that the renderer will not promote.
+/// `#123`) is a non-heading line that the renderer will not promote.
fn is_atx_heading(line: &str) -> bool {
let hashes = line.bytes().take_while(|&b| b == b'#').count();
if !(1..=6).contains(&hashes) {
return false;
}
// Bare `#` / `##` / ... on its own line, or proper ATX with a
- // space/tab after the run of `#`s; anything else (`#tag` / `#9`)
+ // space/tab after the run of `#`s; anything else (`#tag` / `#123`)
// is not a heading.
matches!(line.as_bytes().get(hashes), None | Some(b' ' | b'\t'))
}
@@ -843,11 +843,11 @@ mod tests {
#[test]
fn escape_md_headings_skips_non_atx_hash_lines() {
- // ATX requires a space after the `#`s. Lines like `#tag`, a
- // hash-then-digits run, or `#!/bin/bash` are NOT headings —
- // escaping them would just add cosmetic noise where the
- // renderer wouldn't promote the line in the first place.
- let body = "#tag\n#123\n#!/bin/bash\n####### too many hashes\nbody"; // lint:allow: hash-digit heading test input, not a tracker tag
+ // ATX requires a space after the `#`s. Lines like `#tag`,
+ // `#123`, `#!/bin/bash` are NOT headings — escaping them
+ // would just add cosmetic noise where the renderer
+ // wouldn't promote the line in the first place.
+ let body = "#tag\n#123\n#!/bin/bash\n####### too many hashes\nbody";
let escaped = escape_md_headings(body);
// All four leading `#` lines pass through untouched: too few
// (still need space), seven `#`s (over the cap), shebang
diff --git a/hive-c0re/src/agent_server.rs b/hive-c0re/src/agent_server.rs
index c8842d06..13ae10a1 100644
--- a/hive-c0re/src/agent_server.rs
+++ b/hive-c0re/src/agent_server.rs
@@ -841,7 +841,7 @@ pub(crate) fn handle_send(
let resolved = crate::topology::resolve_recipient(agent, to);
// Validate that the resolved recipient is a known local agent or the
// special "operator" recipient. Without this check a typo in `to`
- // silently queues a message nobody will ever read.
+ // silently queues a message nobody will ever read (issue #1165).
//
// Cross-hive messaging (`name@hive` qualified names) is not routed
// through the broker — use the Matrix MCP tools for that instead.
diff --git a/hive-c0re/src/broker.rs b/hive-c0re/src/broker.rs
index 52d67e2c..1ad74403 100644
--- a/hive-c0re/src/broker.rs
+++ b/hive-c0re/src/broker.rs
@@ -347,7 +347,7 @@ impl Broker {
}
/// Unacknowledged messages addressed to `recipient`, newest-first.
- /// Backs the dashboard's operator inbox: the operator never
+ /// Backs the dashboard's operator inbox (#1469): the operator never
/// `recv`s over an agent socket, so messages to `"operator"` sit in
/// the broker with `acked_at IS NULL` until the operator hits "mark
/// all read" (which calls [`Broker::mark_all_read`]). This read
@@ -1352,7 +1352,7 @@ mod tests {
assert_eq!(broker.ack_turn("b").unwrap(), 5);
}
- /// Transient-wake regression guard: a `ping` fired while no `recv` is parked
+ /// The #1462 fix: a transient `ping` fired while no `recv` is parked
/// must NOT be lost — it's buffered and drained by the next collect.
#[test]
fn transient_ping_buffered_when_no_receiver_parked() {
diff --git a/hive-c0re/src/coordinator.rs b/hive-c0re/src/coordinator.rs
index ff79f063..69487a03 100644
--- a/hive-c0re/src/coordinator.rs
+++ b/hive-c0re/src/coordinator.rs
@@ -114,14 +114,6 @@ pub struct Coordinator {
/// watcher consults both this and the active map before declaring
/// a stop deliberate.
recent_transient: Mutex>,
- /// Timestamps of recent unexpected container crashes, keyed by agent.
- /// Fed by `crash_watch` each time it classifies a stop as a crash (so
- /// a crash-looping container — which `Restart=on-failure` flips back
- /// to running between polls — accumulates one entry per down-transition,
- /// not just whatever its point-in-time state happens to be). Read by
- /// the dashboard's `agents_crashing` banner warning via
- /// `recent_crash_counts`, which prunes entries older than its window.
- recent_crashes: Mutex>>,
/// Unified wire-facing event channel feeding the dashboard SSE
/// stream. Carries broker messages (mirrored from `broker.subscribe`
/// by the forwarder task in `main.rs`) and dashboard-only mutation
@@ -438,7 +430,6 @@ impl Coordinator {
agents: Mutex::new(HashMap::new()),
transient: Mutex::new(HashMap::new()),
recent_transient: Mutex::new(HashMap::new()),
- recent_crashes: Mutex::new(HashMap::new()),
dashboard_events,
event_seq: AtomicU64::new(0),
meta_updates_active: AtomicU64::new(0),
@@ -1084,33 +1075,6 @@ impl Coordinator {
.collect()
}
- /// Record an unexpected crash for `agent`. Called by the crash
- /// watcher whenever it classifies a container stop as a crash (not an
- /// operator action). Append-only here; pruning happens lazily on read
- /// in `recent_crash_counts`.
- pub fn record_crash(&self, agent: &str) {
- self.recent_crashes
- .lock()
- .unwrap()
- .entry(agent.to_owned())
- .or_default()
- .push(std::time::Instant::now());
- }
-
- /// Per-agent count of crashes within the last `window`. Lazily reaps
- /// older timestamps and drops agents with none left, so the map stays
- /// bounded and only lists agents actively crashing. Powers the
- /// dashboard's `agents_crashing` banner warning.
- pub fn recent_crash_counts(&self, window: std::time::Duration) -> HashMap {
- let now = std::time::Instant::now();
- let mut map = self.recent_crashes.lock().unwrap();
- map.retain(|_, times| {
- times.retain(|ts| now.duration_since(*ts) <= window);
- !times.is_empty()
- });
- map.iter().map(|(k, v)| (k.clone(), v.len())).collect()
- }
-
/// Set a transient state and return a guard that clears it on drop.
/// Use this from any path where the surrounding future could be
/// cancelled or panic between set and clear (HTTP handlers, spawned
diff --git a/hive-c0re/src/crash_watch.rs b/hive-c0re/src/crash_watch.rs
index c7cca394..6a7d89dc 100644
--- a/hive-c0re/src/crash_watch.rs
+++ b/hive-c0re/src/crash_watch.rs
@@ -95,7 +95,6 @@ fn emit_crash_transitions(coord: &Coordinator, prev: &HashSet, current:
continue;
}
tracing::warn!(agent = %stopped, "container crash detected");
- coord.record_crash(stopped);
coord.notify_manager(&hive_sh4re::HelperEvent::ContainerCrash {
agent: stopped.clone(),
note: Some("container stopped without an operator action".into()),
diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs
index 05e8b43f..d5e20fc3 100644
--- a/hive-c0re/src/dashboard.rs
+++ b/hive-c0re/src/dashboard.rs
@@ -463,13 +463,6 @@ where
}
}
-/// Window over which container crashes count toward the `agents_crashing`
-/// banner warning. Wide enough that a crash-looping container (restarted
-/// by `Restart=on-failure` every few seconds) keeps the warning lit
-/// between flaps, short enough that a single recovered crash clears within
-/// minutes.
-const CRASH_WARNING_WINDOW: std::time::Duration = std::time::Duration::from_mins(10);
-
async fn api_state(headers: HeaderMap, State(state): State) -> axum::Json {
let host = headers
.get("host")
@@ -530,18 +523,6 @@ async fn api_state(headers: HeaderMap, State(state): State) -> axum::J
.map(QuestionView::from_question)
.collect();
- // Banner warnings: host probes (disk) + agent-state (pending logins,
- // crashing agents). Built before the response struct because the
- // agent-state producer borrows `containers`, which moves in below.
- let server_warnings = {
- let mut w = crate::host_stats::server_warnings();
- w.extend(crate::host_stats::agent_state_warnings(
- &containers,
- &state.coord.recent_crash_counts(CRASH_WARNING_WINDOW),
- ));
- w
- };
-
axum::Json(StateSnapshot {
seq,
hostname,
@@ -583,7 +564,7 @@ async fn api_state(headers: HeaderMap, State(state): State) -> axum::J
.ok()
.filter(|s| !s.is_empty()),
peer_hives: parse_peer_hives(),
- server_warnings,
+ server_warnings: crate::host_stats::server_warnings(),
})
}
@@ -1240,8 +1221,8 @@ pub(crate) fn emit_meta_inputs_snapshot(coord: &Coordinator) {
});
}
-/// Unread operator-directed messages for the dashboard's Y3R C4LL inbox.
-/// Returns messages addressed to `"operator"` that haven't been
+/// Unread operator-directed messages for the dashboard's Y3R C4LL inbox
+/// (#1469). Returns messages addressed to `"operator"` that haven't been
/// acked yet (the operator clears them via the existing
/// `POST /api/agent/operator/mark-all-read`). Newest-first; path-shaped
/// tokens are validated so the client renders file links like the
diff --git a/hive-c0re/src/host_stats.rs b/hive-c0re/src/host_stats.rs
index 712e5568..61c95d9a 100644
--- a/hive-c0re/src/host_stats.rs
+++ b/hive-c0re/src/host_stats.rs
@@ -13,12 +13,8 @@
//! pressure, a failed unit, …) is a backend-only change — no frontend
//! edit. Keep producers cheap; this runs on every `/api/state` assembly.
-use std::collections::HashMap;
-
use serde::Serialize;
-use crate::container_view::ContainerView;
-
/// One server-level warning for the dashboard's top-of-page banner.
#[derive(Debug, Clone, Serialize)]
pub struct ServerWarning {
@@ -69,69 +65,6 @@ pub fn server_warnings() -> Vec {
out
}
-/// Agent-state warnings derived from the live container snapshot the
-/// dashboard already holds: agents that need a claude login, and agents
-/// that are crashing. Kept separate from [`server_warnings`] (host
-/// probes) because the caller owns the container list + crash counts;
-/// the dashboard concatenates both into one banner list.
-///
-/// `crash_counts` is `Coordinator::recent_crash_counts(window)` — agent →
-/// number of crashes inside that window — so a crash-looping agent shows
-/// its repeat count rather than a single point-in-time flap.
-#[must_use]
-pub fn agent_state_warnings(
- containers: &[ContainerView],
- crash_counts: &HashMap,
-) -> Vec {
- let mut out = Vec::new();
-
- // `needs_login` is already running-gated in `container_view::build_all`,
- // so a stopped container never lights this.
- let mut pending: Vec<&str> = containers
- .iter()
- .filter(|c| c.needs_login)
- .map(|c| c.name.as_str())
- .collect();
- if !pending.is_empty() {
- pending.sort_unstable();
- out.push(ServerWarning {
- kind: "pending_logins",
- level: "warn",
- message: format!(
- "{n} agent{plural} {verb} claude login: {list} \
- — run `hivectl login ` to authenticate",
- n = pending.len(),
- plural = if pending.len() == 1 { "" } else { "s" },
- verb = if pending.len() == 1 { "needs" } else { "need" },
- list = pending.join(", "),
- ),
- });
- }
-
- if !crash_counts.is_empty() {
- let mut crashing: Vec<(&String, usize)> =
- crash_counts.iter().map(|(a, n)| (a, *n)).collect();
- crashing.sort_unstable_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(b.0)));
- let list = crashing
- .iter()
- .map(|(a, n)| format!("{a} (×{n})"))
- .collect::>()
- .join(", ");
- out.push(ServerWarning {
- kind: "agents_crashing",
- level: "crit",
- message: format!(
- "{n} agent{plural} crashing: {list} \
- — check the container journal (`hivectl logs `)",
- n = crashing.len(),
- plural = if crashing.len() == 1 { "" } else { "s" },
- ),
- });
- }
-
- out
-}
-
/// Disk usage for the filesystem backing the host nix store — internal to
/// the disk-pressure producer above.
struct DiskUsage {
@@ -189,71 +122,3 @@ fn disk_usage(path: &str) -> Option {
used_pct,
})
}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- fn cv(name: &str, needs_login: bool) -> ContainerView {
- ContainerView {
- name: name.to_owned(),
- container: format!("h-{name}"),
- port: 0,
- running: true,
- needs_update: false,
- needs_login,
- deployed_sha: None,
- pending_reminders: 0,
- parent: None,
- }
- }
-
- #[test]
- fn no_agent_warnings_when_all_healthy() {
- let containers = [cv("alice", false), cv("bob", false)];
- assert!(agent_state_warnings(&containers, &HashMap::new()).is_empty());
- }
-
- #[test]
- fn pending_logins_lists_sorted_agents() {
- let containers = [cv("zoe", true), cv("amy", false), cv("bob", true)];
- let w = agent_state_warnings(&containers, &HashMap::new());
- assert_eq!(w.len(), 1);
- assert_eq!(w[0].kind, "pending_logins");
- assert_eq!(w[0].level, "warn");
- // sorted, login-needing only, count reflected
- assert!(
- w[0].message
- .starts_with("2 agents need claude login: bob, zoe")
- );
- }
-
- #[test]
- fn singular_grammar_for_one_agent() {
- let containers = [cv("solo", true)];
- let w = agent_state_warnings(&containers, &HashMap::new());
- assert!(w[0].message.starts_with("1 agent needs claude login: solo"));
- }
-
- #[test]
- fn crashing_warning_is_crit_and_count_ordered() {
- let crashes = HashMap::from([("flap".to_owned(), 5), ("blip".to_owned(), 1)]);
- let w = agent_state_warnings(&[], &crashes);
- assert_eq!(w.len(), 1);
- assert_eq!(w[0].kind, "agents_crashing");
- assert_eq!(w[0].level, "crit");
- // higher crash count first
- assert!(w[0].message.contains("flap (×5), blip (×1)"));
- }
-
- #[test]
- fn both_warnings_coexist() {
- let containers = [cv("a", true)];
- let crashes = HashMap::from([("b".to_owned(), 2)]);
- let kinds: Vec<&str> = agent_state_warnings(&containers, &crashes)
- .iter()
- .map(|w| w.kind)
- .collect();
- assert_eq!(kinds, ["pending_logins", "agents_crashing"]);
- }
-}
diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs
index 011e363b..51d7af3a 100644
--- a/hive-c0re/src/lifecycle.rs
+++ b/hive-c0re/src/lifecycle.rs
@@ -1145,7 +1145,7 @@ async fn set_nspawn_flags(
// Make /shared writable by every agent. Containers share host uids (no
// PrivateUsers), but each agent is a distinct unix user, so a root-owned
// 0755 dir leaves them unable to write — the documented "read/write for
- // all agents" contract was broken. A setgid group would need a
+ // all agents" contract was broken (#1374). A setgid group would need a
// pinned GID declared in every container plus all agent users joined to
// it (cross-container coordination + a rebuild cascade); instead we use
// the /tmp model — sticky world-writable (1777). The sticky bit lets any
diff --git a/hive-c0re/src/rebuild_queue.rs b/hive-c0re/src/rebuild_queue.rs
index 394d515f..6d8d6735 100644
--- a/hive-c0re/src/rebuild_queue.rs
+++ b/hive-c0re/src/rebuild_queue.rs
@@ -1224,7 +1224,7 @@ mod tests {
kind: QueueKind::Rebuild,
agent: "agent-a".to_owned(),
source: QueueSource::Approval,
- reason: "approval 42 apply commit".to_owned(),
+ reason: "approval #42 apply commit".to_owned(),
parent_id: None,
inputs: Vec::new(),
approval_id: Some(42),
diff --git a/hive-forge/src/body.rs b/hive-forge/src/body.rs
index 05e39c88..0025c572 100644
--- a/hive-forge/src/body.rs
+++ b/hive-forge/src/body.rs
@@ -1,5 +1,5 @@
//! Body-input resolution shared by every verb that posts a body.
-//! Matches the bash `resolve_body` helper: exactly one source
+//! Matches the bash `resolve_body` helper (#382): exactly one source
//! between `--body`, `--body-file`, and piped stdin. Passing both
//! `--body` and `--body-file` is a clear error.
diff --git a/hive-forge/src/client.rs b/hive-forge/src/client.rs
index ace4a3b3..3308fc9e 100644
--- a/hive-forge/src/client.rs
+++ b/hive-forge/src/client.rs
@@ -32,7 +32,7 @@ pub struct Client {
pub default_repo: String,
/// Global `--json` flag — verbs that have a human-readable
/// default path branch on `client.json_mode()` to pick the
- /// JSON output shape instead.
+ /// JSON output shape instead. Closes #421.
json_mode: bool,
}
@@ -131,7 +131,7 @@ impl Client {
/// params (`?limit=N&state=open&...`) are preserved. Pages drain
/// while the response carries a `Link: rel="next"` header, up to
/// `max_pages` (the runaway-loop safety cap). Returns the merged
- /// array. Used by `lint` for repo-wide queries.
+ /// array. Used by `lint` for repo-wide queries (closes #505).
pub fn get_json_all(&self, path: &str, max_pages: u32) -> Result> {
let sep = if path.contains('?') { '&' } else { '?' };
let mut merged = Vec::new();
@@ -271,8 +271,8 @@ fn read_token() -> Result {
}
/// Surface non-2xx HTTP responses as anyhow errors with the response
-/// body included (matches `curl --fail-with-body`) — turns
-/// silent failures into errors with a clear message.
+/// body included (matches `curl --fail-with-body`). Closes #353's
+/// "silent failures with no clue what went wrong" case.
fn check_status(resp: Response, op: &str) -> Result {
let status = resp.status();
if status.is_success() {
diff --git a/hive-forge/src/main.rs b/hive-forge/src/main.rs
index 4f99bfb1..d086a556 100644
--- a/hive-forge/src/main.rs
+++ b/hive-forge/src/main.rs
@@ -8,7 +8,7 @@
//! Single binary with verb subcommands. Replaces the prior bash
//! script (`hive-forge-tools.nix`) so that agents and operators get
//! the same error handling, exit codes, and JSON shapes regardless
-//! of how the bash mood was that day.
+//! of how the bash mood was that day (closes #280).
#![warn(missing_docs)]
// Clap-derived `Args` structs are intentionally consumed by their
@@ -26,7 +26,7 @@ use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(
name = "hive-forge",
- about = "Forgejo CLI wrapper for hyperhive",
+ about = "Forgejo CLI wrapper for hyperhive (closes #280)",
disable_help_subcommand = true
)]
struct Cli {
@@ -36,7 +36,7 @@ struct Cli {
#[arg(short = 'r', long, global = true)]
repo: Option,
/// Emit JSON output instead of the verb's default human-readable
- /// shape, for verbs that support both. Verbs whose
+ /// shape, for verbs that support both (closes #421). Verbs whose
/// only output is already JSON (`issue`, `pr`, etc.) ignore this
/// flag — they always print JSON regardless.
#[arg(long, global = true)]
@@ -100,7 +100,7 @@ enum Verb {
Subscription(verbs::subscription::Args),
/// List timeline events on an issue or PR (closes, label adds,
/// assignments, commit refs, pushes, etc.) — the audit trail
- /// `view` + `comments` don't surface.
+ /// `view` + `comments` don't surface (closes #783).
Timeline(verbs::timeline::Args),
/// Upload a file as an attachment to an issue.
AttachIssue(verbs::attach::IssueArgs),
diff --git a/hive-forge/src/verbs/assign.rs b/hive-forge/src/verbs/assign.rs
index d257e239..5c9ed713 100644
--- a/hive-forge/src/verbs/assign.rs
+++ b/hive-forge/src/verbs/assign.rs
@@ -1,7 +1,7 @@
//! `assign [--remove]` — add or remove a user from an
//! issue/PR's assignee list. Forgejo has no dedicated POST endpoint —
-//! we read the current list, mutate, and PATCH the issue back (Forgejo
-//! has no such endpoint; matches the bash helper's logic).
+//! we read the current list, mutate, and PATCH the issue back (closes
+//! #353's "no such endpoint" trap; matches the bash helper's logic).
use anyhow::Result;
use clap::Args as ClapArgs;
diff --git a/hive-forge/src/verbs/comments.rs b/hive-forge/src/verbs/comments.rs
index 6c5bcb6a..026bbc1a 100644
--- a/hive-forge/src/verbs/comments.rs
+++ b/hive-forge/src/verbs/comments.rs
@@ -1,6 +1,6 @@
//! `comments [--limit N | --tail N]` — list comments on an
-//! issue or PR. Replaces the curl fallback; `--tail`
-//! handles the paging-for-long-threads
+//! issue or PR. Closes the curl-fallback gap (#418); `--tail`
+//! closes the third of the four #694 gaps (paging-for-long-threads
//! awkwardness).
//!
//! - `--limit N` (default 50, Forgejo's cap) returns the first N
@@ -14,7 +14,7 @@
//! on this long thread?" without scrolling through the whole
//! history.
//!
-//! Use the global `--json` flag for JSON output.
+//! Use the global `--json` flag for JSON output (#421).
use anyhow::Result;
use clap::Args as ClapArgs;
diff --git a/hive-forge/src/verbs/diff.rs b/hive-forge/src/verbs/diff.rs
index 57f27bbf..a0eb6e82 100644
--- a/hive-forge/src/verbs/diff.rs
+++ b/hive-forge/src/verbs/diff.rs
@@ -5,7 +5,7 @@
//! `package-lock.json`, …) is collapsed to a single
//! `[: contents changed (+N -M, --full for content)]`
//! line so a `flake.lock` rev bump doesn't drown the human-
-//! authored changes in 5 000 lines of lock churn. The
+//! authored changes in 5 000 lines of lock churn (#222). The
//! per-file git headers (`diff --git`, `index`, `---`, `+++`,
//! and any rename / mode metadata) are suppressed alongside the
//! hunks since the placeholder already carries the file path and
diff --git a/hive-forge/src/verbs/lint.rs b/hive-forge/src/verbs/lint.rs
index ff4dd516..af24ffac 100644
--- a/hive-forge/src/verbs/lint.rs
+++ b/hive-forge/src/verbs/lint.rs
@@ -1,5 +1,5 @@
//! `lint ` — issue/PR/branch lint queries for triage
-//! workflows. Replaces ad-hoc curl + jq filtering with
+//! workflows (closes #505). Replaces ad-hoc curl + jq filtering with
//! typed commands that always emit JSON via the global `--json`
//! (default is a compact one-line-per-item human shape).
//!
@@ -97,7 +97,7 @@ struct NoReviewerArgs {
/// Reviewer login to look for (matches `@` in PR body or
/// any comment). Required — defaulting to a specific name would
/// bake one deployment's reviewer convention into the binary
- /// (flagged in review).
+ /// (mara's nit on #507).
#[arg(long)]
reviewer: String,
}
@@ -177,7 +177,7 @@ fn run_no_reviewer(client: &Client, args: NoReviewerArgs) -> Result<()> {
continue;
}
// Paginate so PRs with >50 comments don't yield false positives
- // (flagged in review). Same 1000-comment ceiling as elsewhere.
+ // (argus nit on #507). Same 1000-comment ceiling as elsewhere.
let comments = client.get_json_all(
&format!("/repos/{repo}/issues/{number}/comments?limit={PAGE_LIMIT}"),
MAX_PAGES,
diff --git a/hive-forge/src/verbs/list.rs b/hive-forge/src/verbs/list.rs
index c8764a10..49a78bc9 100644
--- a/hive-forge/src/verbs/list.rs
+++ b/hive-forge/src/verbs/list.rs
@@ -5,7 +5,7 @@
//!
//! Mirrors Forgejo's `GET /repos/{owner}/{repo}/issues` query-string
//! filters one-for-one so the mental model carries over. Closes the
-//! read-side curl-fallback gap (no boundary concerns —
+//! second of the four #694 gaps (read-side; no boundary concerns —
//! every agent + the operator queries the issue tracker constantly).
use std::fmt::Write as _;
diff --git a/hive-forge/src/verbs/pr_create.rs b/hive-forge/src/verbs/pr_create.rs
index c6cd21eb..97a2b23d 100644
--- a/hive-forge/src/verbs/pr_create.rs
+++ b/hive-forge/src/verbs/pr_create.rs
@@ -8,7 +8,7 @@
//! hint block is filtered out of git's stderr (we print the canonical
//! URL ourselves once the API returns). Other git stderr passes
//! through. Default behaviour is unchanged: no push unless asked.
-//! Adds the auto-push path per operator decision (opt-in
+//! Closes the auto-push half of #222 per operator decision (opt-in
//! flag).
//!
//! With `--agit` the PR is opened via Forgejo's `AGit` flow instead of
diff --git a/hive-forge/src/verbs/timeline.rs b/hive-forge/src/verbs/timeline.rs
index bd662e72..8677cb8d 100644
--- a/hive-forge/src/verbs/timeline.rs
+++ b/hive-forge/src/verbs/timeline.rs
@@ -1,7 +1,7 @@
//! `timeline [--limit N]` — list timeline events on an
-//! issue or PR. Fills the gap where agents kept
+//! issue or PR. Closes #783 (last piece of the #694 epic: agents kept
//! falling back to curl for "who closed this?" / "when was this
-//! labelled?" archaeology. Composes naturally with `view ` /
+//! labelled?" archaeology). Composes naturally with `view ` /
//! `comments ` — separate verb keeps the existing shapes stable.
//!
//! Forgejo's `/issues/{n}/timeline` endpoint returns BOTH the actual
@@ -12,7 +12,7 @@
//!
//! `--tail N` is a follow-up (the timeline endpoint doesn't expose a
//! total-count field so we can't use the count-then-page trick that
-//! `comments --tail` uses; future shape probably mirrors
+//! `comments --tail` lands in #770; future shape probably mirrors
//! `comments --tail` once Forgejo grows a `count` query or we accept
//! the trailing-slice cost).
@@ -211,7 +211,7 @@ mod tests {
//! Tests call `format_event` directly so any new event-type arm
//! added in `print_event`'s dispatch is automatically covered by
//! the rendering path (no parallel test-side dispatch to keep in
- //! sync). A review flagged: "extract a `format_event(ev) ->
+ //! sync). Argus on PR #798 🟡: "extract a `format_event(ev) ->
//! String` helper and test that function directly instead of
//! duplicating the logic" — addressed.
use super::*;
diff --git a/hive-matrix-mcp/src/handlers.rs b/hive-matrix-mcp/src/handlers.rs
index 23125db7..1be0d4a2 100644
--- a/hive-matrix-mcp/src/handlers.rs
+++ b/hive-matrix-mcp/src/handlers.rs
@@ -4,7 +4,7 @@
//! the payload to claude; `Error { message }` becomes the tool-call
//! error message claude sees).
//!
-//! Tool surface mirrors damocles-daemon's v0 set per the operator's call:
+//! Tool surface mirrors damocles-daemon's v0 set per mara on #548:
//! `send_message`, `send_dm`, `send_reaction`, `send_reply`, `mark_read`,
//! `list_rooms`, `list_room_members`, `read_room`. Plus a `ping` for the
//! MCP bridge's liveness probe.
diff --git a/hive-matrix-mcp/src/lib.rs b/hive-matrix-mcp/src/lib.rs
index 25433b0d..8db22f9a 100644
--- a/hive-matrix-mcp/src/lib.rs
+++ b/hive-matrix-mcp/src/lib.rs
@@ -5,8 +5,8 @@
//! The wire protocol between the two binaries lives in [`protocol`].
//! Path helpers (token file, daemon socket) live in [`paths`].
//!
-//! Architecture rationale and tool surface mirror the existing
-//! `damocles-daemon` v0 set.
+//! Phase 3 of #548. Architecture rationale + tool surface mirror the
+//! existing `damocles-daemon` (see issue thread for details).
pub mod client;
pub mod paths;
diff --git a/hive-matrix-mcp/src/paths.rs b/hive-matrix-mcp/src/paths.rs
index 92918dd9..9838aff5 100644
--- a/hive-matrix-mcp/src/paths.rs
+++ b/hive-matrix-mcp/src/paths.rs
@@ -18,7 +18,7 @@ pub const DEFAULT_HOMESERVER: &str = "http://localhost:8008";
/// Lives under systemd's `RuntimeDirectory=hive-matrix` (a tmpfs path
/// that disappears on container restart — fine, because the daemon
/// recreates the socket on its own boot) so the agent unix user
-/// can bind a socket inside it without root in `/run`.
+/// (post-#658) can bind a socket inside it without root in `/run`.
pub const DEFAULT_DAEMON_SOCKET: &str = "/run/hive-matrix/socket";
/// Resolve the matrix access-token file path. Override via
diff --git a/hive-matrix-mcp/src/timeline.rs b/hive-matrix-mcp/src/timeline.rs
index 4a81669f..fcb7e425 100644
--- a/hive-matrix-mcp/src/timeline.rs
+++ b/hive-matrix-mcp/src/timeline.rs
@@ -1,7 +1,7 @@
//! Matrix event handlers: incoming room messages fire a hyperhive
//! wake signal so the agent's harness drives a new claude turn.
//!
-//! Per the operator's call: wake body is a SHORT TEASER, not the full message
+//! Per mara on #548: wake body is a SHORT TEASER, not the full message
//! (msg stays unread server-side; agent fetches via `read_room`). The
//! `wake::format_wake_body` truncates to ~100 chars.
//!
diff --git a/hive-matrix-mcp/src/wake.rs b/hive-matrix-mcp/src/wake.rs
index 48d4ed53..2fef492e 100644
--- a/hive-matrix-mcp/src/wake.rs
+++ b/hive-matrix-mcp/src/wake.rs
@@ -7,7 +7,7 @@
//! The agent harness's `agent_server` parses it and treats it as a
//! `Wake` from the matrix subsystem.
//!
-//! Per the operator's call (phase 3): the body is a SHORT TEASER, not
+//! Per mara's call on #548 phase 3: the body is a SHORT TEASER, not
//! the full message — the agent then reads the unmarked event via
//! the `read_room` MCP tool. Truncation to ~100 chars keeps the wake
//! prompt focused (`forge_notify` embeds longer excerpts because the
diff --git a/hive-sh4re/src/assets.rs b/hive-sh4re/src/assets.rs
index ca6e239f..b5fcc0e1 100644
--- a/hive-sh4re/src/assets.rs
+++ b/hive-sh4re/src/assets.rs
@@ -77,7 +77,7 @@ pub fn core_avatar_png() -> PathBuf {
/// `$HIVE_ASSETS_DIR/branding/agent-configs.png` — secondary org
/// mark for the `agent-configs/` mirror org. Rendered from
/// `agent-configs.svg` at asset-build time (was rendered in
-/// `hive-c0re/build.rs`).
+/// `hive-c0re/build.rs` before #555).
#[must_use]
pub fn config_org_avatar_png() -> PathBuf {
dir().join("branding/agent-configs.png")
diff --git a/scripts/check-issue-refs.sh b/scripts/check-issue-refs.sh
index 8053bed2..5cec3d3e 100755
--- a/scripts/check-issue-refs.sh
+++ b/scripts/check-issue-refs.sh
@@ -19,24 +19,16 @@
# overruns) and digit-runs followed by a letter — e.g. hash-route
# fragments like #24h. Residual: a pure-numeric short hex (e.g. three
# identical digits) trips it — write the six-digit form to dodge.
-#
-# Escape hatch: a line containing the marker `lint:allow` is exempt.
-# Reserve it for genuine `#` that aren't tracker tags — e.g. a
-# `#123` markdown-heading example or hash-prefixed test-input data —
-# and keep a short reason next to the marker. Don't use it to keep a
-# real tracker tag; rewrite those to prose.
set -eu
pattern='#[0-9]{2,5}([^0-9a-zA-Z]|$)'
# `/dev/null` forces grep to always print a filename prefix, even when
# xargs hands it a single file. `-r`/`-0` keep it robust to odd paths
-# and an empty file list. Lines carrying the `lint:allow` marker are
-# dropped (legitimate non-tracker `#`; see the header).
+# and an empty file list.
hits="$(
git ls-files -z '*.rs' '*.nix' '*.js' '*.ts' '*.css' '*.html' \
- | xargs -0 -r grep -nE "$pattern" /dev/null 2>/dev/null \
- | grep -v 'lint:allow' || true
+ | xargs -0 -r grep -nE "$pattern" /dev/null 2>/dev/null || true
)"
if [ -n "$hits" ]; then