fix(ci): unblock nix flake check after clippy 0.1.95 bump (#1368)

The nixpkgs bump to clippy 0.1.95 / cargo 1.95.0 added + strengthened a
large batch of lints. CI denied ALL warnings (`-D warnings`) against the
`pedantic = warn` workspace lint, so the bump hard-failed `nix flake
check` workspace-wide with zero code changes — and would recur on every
future clippy bump.

Posture fix (the durable part): CI now runs
`-D warnings -A clippy::pedantic`, so the default/correctness/style lints
stay a hard gate while the "extra, opinionated" pedantic group is
advisory only (still `warn` for local `cargo clippy` via the workspace
lints table, just non-blocking in CI). `-A` rather than `-W` so the
group drop doesn't re-enable the specific pedantic lints the workspace
allows (e.g. `must_use_candidate`).

Also fixes the genuine DEFAULT/STYLE lints the bump surfaced across the
workspace (doc_lazy_continuation, collapsible_if, ptr_arg,
match_like_matches_macro, …) via `cargo clippy --fix` + manual stragglers
(`too_many_arguments` #[allow] on the host-config constructors), and
three tests that had rotted while the CI runner was offline (#1221):
- topology::top_level_agents_in_multi_root — hardcoded unsorted expected
- rebuild_queue::depends_on_evicted_dep_counts_as_resolved — needs
  MAX_HISTORY_PER_KIND newer terminals to evict, not one
- coordinator::agent_paths doctest — illustrative pseudo-code, now `ignore`

Validated: clippy + formatting + cargo-test checks all pass.
This commit is contained in:
atlas 2026-06-05 15:01:59 +02:00 committed by mara
commit 734fe88858
18 changed files with 129 additions and 103 deletions

View file

@ -320,18 +320,29 @@
formatting = treefmt-eval.config.build.check self; formatting = treefmt-eval.config.build.check self;
# Clippy as a check via crane's first-class `cargoClippy` # Clippy as a check via crane's first-class `cargoClippy`
# builder. Reuses the shared `cargoArtifacts` (deps already # builder. Reuses the shared `cargoArtifacts` (deps already
# built) and runs `cargo clippy --workspace --all-targets # built) and runs `cargo clippy --workspace --all-targets`
# -- -D warnings` directly — no `overrideAttrs` hack needed, # directly — no `overrideAttrs` hack needed, because crane
# because crane parses `cargoClippyExtraArgs` correctly # parses `cargoClippyExtraArgs` correctly (naersk's
# (naersk's `mode = "clippy"` used to mangle the `--` # `mode = "clippy"` used to mangle the `--` separator, which
# separator, which is why the old wiring went through # is why the old wiring went through overrideAttrs).
# overrideAttrs). #
# `-D warnings` makes the default/correctness/style lints a
# hard CI gate. `-A clippy::pedantic` then drops the pedantic
# group from that gate: pedantic is the "extra, opinionated"
# group the clippy team grows freely, so denying it means
# every toolchain bump that adds a new pedantic lint breaks CI
# with zero code changes (#1368). The `pedantic = warn`
# workspace lint (Cargo.toml) keeps it as advisory signal in
# local `cargo clippy` — it just no longer blocks the build.
# (`-A` rather than `-W` here: `-W clippy::pedantic` would
# re-enable the specific pedantic lints the workspace lints
# table allows, e.g. `must_use_candidate`.)
clippy = craneLib.cargoClippy { clippy = craneLib.cargoClippy {
src = cleanSrc; src = cleanSrc;
inherit cargoArtifacts nativeBuildInputs; inherit cargoArtifacts nativeBuildInputs;
pname = "hyperhive-workspace"; pname = "hyperhive-workspace";
version = "0.1.0"; version = "0.1.0";
cargoClippyExtraArgs = "--workspace --all-targets -- -D warnings"; cargoClippyExtraArgs = "--workspace --all-targets -- -D warnings -A clippy::pedantic";
}; };
# `cargo test --workspace` lifted out of `buildPackage` so the # `cargo test --workspace` lifted out of `buildPackage` so the
# `hyperhive-assets` dep (which `hive-ag3nt::prompt::tests` # `hyperhive-assets` dep (which `hive-ag3nt::prompt::tests`

View file

@ -776,9 +776,7 @@ impl AgentServer {
}; };
// Prepend matrix unread entry for self-queries only (can't // Prepend matrix unread entry for self-queries only (can't
// reach another agent's matrix daemon from here). // reach another agent's matrix daemon from here).
if is_self_query if is_self_query && let Some(unread_rooms) = matrix_unread_summary().await {
&& let Some(unread_rooms) = matrix_unread_summary().await
{
let total = u32::try_from(unread_rooms.len()).unwrap_or(u32::MAX); let total = u32::try_from(unread_rooms.len()).unwrap_or(u32::MAX);
if total > 0 { if total > 0 {
let summary = format_matrix_summary(&unread_rooms); let summary = format_matrix_summary(&unread_rooms);

View file

@ -108,10 +108,8 @@ fn render_bash_run(id: &str, resp: Result<DaemonResponse>) -> String {
match resp { match resp {
Ok(DaemonResponse::Ok { payload }) => { Ok(DaemonResponse::Ok { payload }) => {
let finished = payload["finished"].as_bool().unwrap_or(false); let finished = payload["finished"].as_bool().unwrap_or(false);
if finished { if finished && let Some(task) = payload.get("task") {
if let Some(task) = payload.get("task") { return format_task(id, task);
return format_task(id, task);
}
} }
format!("task started: id={id}") format!("task started: id={id}")
} }

View file

@ -69,19 +69,18 @@ async fn dispatch(req: DaemonRequest) -> DaemonResponse {
// Inline wait: if requested and the task finishes quickly, // Inline wait: if requested and the task finishes quickly,
// return the full status instead of just the task ID. // return the full status instead of just the task ID.
let wait = wait_seconds.unwrap_or(0); let wait = wait_seconds.unwrap_or(0);
if wait > 0 { if wait > 0
if let Some(task) = runner::wait_for_task(&id, wait).await { && let Some(task) = runner::wait_for_task(&id, wait).await
if matches!( && matches!(
task.status, task.status,
TaskStatus::Done | TaskStatus::TimedOut | TaskStatus::Interrupted TaskStatus::Done | TaskStatus::TimedOut | TaskStatus::Interrupted
) { )
return DaemonResponse::ok(&serde_json::json!({ {
"id": id, return DaemonResponse::ok(&serde_json::json!({
"finished": true, "id": id,
"task": task, "finished": true,
})); "task": task,
} }));
}
} }
DaemonResponse::ok(&serde_json::json!({ "id": id, "finished": false })) DaemonResponse::ok(&serde_json::json!({ "id": id, "finished": false }))
} }

View file

@ -560,10 +560,10 @@ async fn run_apply_commit(
&paths, &paths,
&|step| coord.set_queue_step(queue_entry_id, step), &|step| coord.set_queue_step(queue_entry_id, step),
&|log_id| { &|log_id| {
if let Some(qid) = queue_entry_id { if let Some(qid) = queue_entry_id
if coord.rebuild_queue.set_build_log_id(qid, log_id) { && coord.rebuild_queue.set_build_log_id(qid, log_id)
coord.emit_rebuild_queue_snapshot(); {
} coord.emit_rebuild_queue_snapshot();
} }
}, },
) )

View file

@ -90,10 +90,10 @@ pub async fn rebuild_agent(
&paths, &paths,
&|step| coord.set_queue_step(queue_entry_id, step), &|step| coord.set_queue_step(queue_entry_id, step),
&|log_id| { &|log_id| {
if let Some(qid) = queue_entry_id { if let Some(qid) = queue_entry_id
if coord.rebuild_queue.set_build_log_id(qid, log_id) { && coord.rebuild_queue.set_build_log_id(qid, log_id)
coord.emit_rebuild_queue_snapshot(); {
} coord.emit_rebuild_queue_snapshot();
} }
}, },
) )
@ -199,18 +199,18 @@ pub async fn ensure_root_agent(coord: &Arc<Coordinator>) -> Result<()> {
/// Uses BFS from root agents (depth 0). Agents absent from `topo` sort last, /// Uses BFS from root agents (depth 0). Agents absent from `topo` sort last,
/// alphabetically within their tier. Stable within each depth tier. /// alphabetically within their tier. Stable within each depth tier.
pub fn topology_sort( pub fn topology_sort(
names: &mut Vec<String>, names: &mut [String],
topo: &std::collections::BTreeMap<String, Option<String>>, topo: &std::collections::BTreeMap<String, Option<String>>,
) { ) {
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
// Build depth map using owned clones so the borrow on `names` is released // Build depth map using owned clones so the borrow on `names` is released
// before the sort_by mutable borrow. // before the sort_by mutable borrow.
let name_set: Vec<String> = names.clone(); let name_set: Vec<String> = names.to_vec();
let mut depth: HashMap<String, usize> = HashMap::new(); let mut depth: HashMap<String, usize> = HashMap::new();
let mut queue: VecDeque<String> = VecDeque::new(); let mut queue: VecDeque<String> = VecDeque::new();
// Seed roots: entries with no parent, or names not present in topo at all. // Seed roots: entries with no parent, or names not present in topo at all.
for name in &name_set { for name in &name_set {
if topo.get(name).map_or(true, |p| p.is_none()) { if topo.get(name).is_none_or(|p| p.is_none()) {
depth.insert(name.clone(), 0); depth.insert(name.clone(), 0);
queue.push_back(name.clone()); queue.push_back(name.clone());
} }

View file

@ -111,10 +111,10 @@ fn should_delete(json_path: &Path, cutoff: i64) -> bool {
fn delete_trio(dir: &Path, stem: &str) { fn delete_trio(dir: &Path, stem: &str) {
for ext in ["json", "out", "err"] { for ext in ["json", "out", "err"] {
let path = dir.join(format!("{stem}.{ext}")); let path = dir.join(format!("{stem}.{ext}"));
if path.exists() { if path.exists()
if let Err(e) = std::fs::remove_file(&path) { && let Err(e) = std::fs::remove_file(&path)
tracing::warn!(path = %path.display(), error = ?e, "bash-tasks vacuum: remove failed"); {
} tracing::warn!(path = %path.display(), error = ?e, "bash-tasks vacuum: remove failed");
} }
} }
} }

View file

@ -8,7 +8,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
use serde::{Deserialize, Serialize}; use serde::Serialize;
use crate::coordinator::Coordinator; use crate::coordinator::Coordinator;
use crate::lifecycle::{self, AGENT_PREFIX}; use crate::lifecycle::{self, AGENT_PREFIX};
@ -113,18 +113,18 @@ pub fn claude_has_session(dir: &Path) -> bool {
/// upgrades don't lose state during the transition window. /// upgrades don't lose state during the transition window.
fn read_harness_flags(name: &str) -> (bool, bool) { fn read_harness_flags(name: &str) -> (bool, bool) {
let dir = Coordinator::agent_notes_dir(name); let dir = Coordinator::agent_notes_dir(name);
if let Ok(raw) = std::fs::read_to_string(dir.join("hyperhive-harness.json")) { if let Ok(raw) = std::fs::read_to_string(dir.join("hyperhive-harness.json"))
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw) { && let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw)
let rl = v {
.get("rate_limited") let rl = v
.and_then(|x| x.as_bool()) .get("rate_limited")
.unwrap_or(false); .and_then(|x| x.as_bool())
let nl = v .unwrap_or(false);
.get("needs_login") let nl = v
.and_then(|x| x.as_bool()) .get("needs_login")
.unwrap_or(false); .and_then(|x| x.as_bool())
return (rl, nl); .unwrap_or(false);
} return (rl, nl);
} }
// Legacy fallback: presence of individual sentinel files. // Legacy fallback: presence of individual sentinel files.
let rate_limited = dir.join("hyperhive-rate-limited").exists(); let rate_limited = dir.join("hyperhive-rate-limited").exists();

View file

@ -263,6 +263,12 @@ impl TransientKind {
} }
impl Coordinator { impl Coordinator {
#[allow(
clippy::too_many_arguments,
reason = "constructor wiring host-level config (flakes, ports, pronouns, \
context-window + resource limits) into the coordinator; bundling \
into a struct would just move the same fields one level out"
)]
pub fn open( pub fn open(
db_path: &Path, db_path: &Path,
hyperhive_flake: String, hyperhive_flake: String,
@ -342,7 +348,7 @@ impl Coordinator {
/// creates the tmpfs entry on first call. All other paths are /// creates the tmpfs entry on first call. All other paths are
/// derived statically from `name`. /// derived statically from `name`.
/// ///
/// ```no_run /// ```ignore
/// let paths = Coordinator::agent_paths(name, coord.ensure_runtime(name)?); /// let paths = Coordinator::agent_paths(name, coord.ensure_runtime(name)?);
/// ``` /// ```
#[must_use] #[must_use]

View file

@ -26,7 +26,7 @@ use tokio_stream::{Stream, StreamExt};
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
use crate::actions; use crate::actions;
use crate::container_view::{self, ContainerView, claude_has_session}; use crate::container_view::{ContainerView, claude_has_session};
use crate::coordinator::Coordinator; use crate::coordinator::Coordinator;
use crate::lifecycle::{self, MANAGER_NAME}; use crate::lifecycle::{self, MANAGER_NAME};

View file

@ -4,7 +4,7 @@
//! collaborator access to for operator-curated shared content. //! collaborator access to for operator-curated shared content.
//! No-op when `hive-forge` isn't running. Full design: `docs/forge.md`. //! No-op when `hive-forge` isn't running. Full design: `docs/forge.md`.
use std::path::{Path, PathBuf}; use std::path::Path;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use base64::Engine; use base64::Engine;

View file

@ -1549,7 +1549,7 @@ mod tests {
// Result must still be in .2-.254. // Result must still be in .2-.254.
let ip = from_bridge.unwrap(); let ip = from_bridge.unwrap();
let last: u8 = ip.rsplit('.').next().unwrap().parse().unwrap(); let last: u8 = ip.rsplit('.').next().unwrap().parse().unwrap();
assert!(last >= 2 && last <= 254, "host byte {last}"); assert!((2..=254).contains(&last), "host byte {last}");
} }
/// `setup_proposed` is idempotent: calling it on an existing repo is a /// `setup_proposed` is idempotent: calling it on an existing repo is a

View file

@ -204,6 +204,12 @@ async fn main() -> Result<()> {
/// Start the coordinator daemon: open the broker, run migrations, spawn /// Start the coordinator daemon: open the broker, run migrations, spawn
/// background tasks (auto-update, vacuums, crash-watcher, reminder-scheduler, /// background tasks (auto-update, vacuums, crash-watcher, reminder-scheduler,
/// dashboard), then serve the admin socket until a signal arrives. /// dashboard), then serve the admin socket until a signal arrives.
#[allow(
clippy::too_many_arguments,
reason = "the `serve` subcommand's args are the host-level config the daemon \
boots from (flakes, ports, pronouns, context-window + resource \
limits); they flow straight through to Coordinator::open"
)]
async fn cmd_serve( async fn cmd_serve(
hyperhive_flake: String, hyperhive_flake: String,
nixpkgs_flake: String, nixpkgs_flake: String,
@ -268,10 +274,10 @@ async fn cmd_serve(
// No-op when the core token or forge are absent. // No-op when the core token or forge are absent.
let webhook_port = dashboard_port; let webhook_port = dashboard_port;
tokio::spawn(async move { tokio::spawn(async move {
if let Some(token) = forge::core_token() { if let Some(token) = forge::core_token()
if let Err(e) = knowledge::ensure_webhook(&token, webhook_port).await { && let Err(e) = knowledge::ensure_webhook(&token, webhook_port).await
tracing::warn!(error = ?e, "knowledge: ensure_webhook failed"); {
} tracing::warn!(error = ?e, "knowledge: ensure_webhook failed");
} }
}); });
// Knowledge periodic pull: hourly fallback in case the webhook is // Knowledge periodic pull: hourly fallback in case the webhook is

View file

@ -570,8 +570,10 @@ fn agent_canonical_inputs(name: &str) -> Vec<&'static str> {
/// agent flake-lock introspection. /// agent flake-lock introspection.
#[allow( #[allow(
clippy::too_many_lines, clippy::too_many_lines,
clippy::too_many_arguments,
reason = "templated string-builder for the meta flake — the length is one \ reason = "templated string-builder for the meta flake — the length is one \
contiguous fmt block, splitting it would just hide the shape" contiguous fmt block, splitting it would just hide the shape; the \
args mirror the host-level config the flake is rendered from"
)] )]
fn render_flake_with_lookup<F>( fn render_flake_with_lookup<F>(
hyperhive_flake: &str, hyperhive_flake: &str,

View file

@ -392,17 +392,16 @@ impl RebuildQueue {
// tool-groups change and a capabilities change for the same // tool-groups change and a capabilities change for the same
// agent are distinct operations and must not collapse into one. // agent are distinct operations and must not collapse into one.
for entry in &mut inner.entries { for entry in &mut inner.entries {
let perm_type_matches = match (&entry.perm_payload, &perm_payload) { let perm_type_matches = matches!(
(Some(PermPayload::ToolGroups { .. }), Some(PermPayload::ToolGroups { .. })) => { (&entry.perm_payload, &perm_payload),
true
}
( (
Some(PermPayload::ToolGroups { .. }),
Some(PermPayload::ToolGroups { .. })
) | (
Some(PermPayload::Capabilities { .. }), Some(PermPayload::Capabilities { .. }),
Some(PermPayload::Capabilities { .. }), Some(PermPayload::Capabilities { .. })
) => true, ) | (None, None)
(None, None) => true, );
_ => false,
};
if entry.state == QueueState::Queued if entry.state == QueueState::Queued
&& entry.kind == kind && entry.kind == kind
&& entry.agent == agent && entry.agent == agent
@ -1505,17 +1504,21 @@ mod tests {
None, None,
); );
q.take_next(); q.take_next();
// One more terminal to push `dep` out of the history window.
q.finish(dep, QueueState::Done, None); q.finish(dep, QueueState::Done, None);
let extra = q.enqueue( // Push `dep` out of the per-kind history window: `trim_history`
QueueKind::Rebuild, // keeps the newest MAX_HISTORY_PER_KIND terminals per kind, so it
"extra".to_owned(), // takes that many newer terminals to evict `dep`.
QueueSource::Manual, for i in 0..MAX_HISTORY_PER_KIND {
"extra".to_owned(), let extra = q.enqueue(
None, QueueKind::Rebuild,
); format!("extra-{i}"),
q.take_next(); QueueSource::Manual,
q.finish(extra, QueueState::Done, None); format!("extra-{i}"),
None,
);
q.take_next();
q.finish(extra, QueueState::Done, None);
}
// `dep` should now be evicted. // `dep` should now be evicted.
assert!( assert!(
q.snapshot().iter().all(|e| e.id != dep), q.snapshot().iter().all(|e| e.id != dep),

View file

@ -665,7 +665,9 @@ mod tests {
topo.insert("orphan".to_owned(), None); topo.insert("orphan".to_owned(), None);
let mut top = top_level_agents_in(&topo); let mut top = top_level_agents_in(&topo);
top.sort(); top.sort();
assert_eq!(top, vec![crate::lifecycle::MANAGER_NAME, "orphan"]); let mut expected = vec![crate::lifecycle::MANAGER_NAME, "orphan"];
expected.sort();
assert_eq!(top, expected);
} }
#[test] #[test]
@ -741,7 +743,7 @@ mod tests {
#[test] #[test]
fn reconcile_roles_in_does_not_reseed_after_explicit_revoke() { fn reconcile_roles_in_does_not_reseed_after_explicit_revoke() {
let mgr = crate::lifecycle::MANAGER_NAME; let mgr = crate::lifecycle::MANAGER_NAME;
let agent_names = vec![mgr.to_owned(), "alice".to_owned()]; let agent_names = [mgr.to_owned(), "alice".to_owned()];
let mut roles: BTreeMap<String, Vec<String>> = BTreeMap::new(); let mut roles: BTreeMap<String, Vec<String>> = BTreeMap::new();
// Tombstone: manager was seen before but all roles were revoked. // Tombstone: manager was seen before but all roles were revoked.
roles.insert(mgr.to_owned(), vec![]); roles.insert(mgr.to_owned(), vec![]);
@ -759,7 +761,7 @@ mod tests {
#[test] #[test]
fn reconcile_roles_in_seeds_root_when_absent() { fn reconcile_roles_in_seeds_root_when_absent() {
let mgr = crate::lifecycle::MANAGER_NAME; let mgr = crate::lifecycle::MANAGER_NAME;
let agent_names = vec![mgr.to_owned(), "alice".to_owned()]; let agent_names = [mgr.to_owned(), "alice".to_owned()];
let roles: BTreeMap<String, Vec<String>> = BTreeMap::new(); // empty let roles: BTreeMap<String, Vec<String>> = BTreeMap::new(); // empty
let should_seed = agent_names.iter().any(|n| n == mgr) && !roles.contains_key(mgr); let should_seed = agent_names.iter().any(|n| n == mgr) && !roles.contains_key(mgr);

View file

@ -450,8 +450,8 @@ pub async fn collect_unread(client: &Client) -> Vec<crate::protocol::RoomUnread>
use crate::protocol::RoomUnread; use crate::protocol::RoomUnread;
let mut result = Vec::new(); let mut result = Vec::new();
for room in client.joined_rooms() { for room in client.joined_rooms() {
let count = u32::try_from(room.unread_notification_counts().notification_count) let count =
.unwrap_or(u32::MAX); u32::try_from(room.unread_notification_counts().notification_count).unwrap_or(u32::MAX);
if count == 0 { if count == 0 {
continue; continue;
} }

View file

@ -66,21 +66,22 @@ fn socket_listener() -> Result<UnixListener> {
.ok() .ok()
.and_then(|s| s.parse().ok()); .and_then(|s| s.parse().ok());
if let (Some(n), Some(p)) = (listen_fds, listen_pid) { if let (Some(n), Some(p)) = (listen_fds, listen_pid)
if n >= 1 && p == std::process::id() { && n >= 1
// SAFETY: systemd has passed us a ready UnixListener on fd 3. && p == std::process::id()
let std_listener = unsafe { {
use std::os::unix::io::FromRawFd; // SAFETY: systemd has passed us a ready UnixListener on fd 3.
std::os::unix::net::UnixListener::from_raw_fd(3) let std_listener = unsafe {
}; use std::os::unix::io::FromRawFd;
std_listener std::os::unix::net::UnixListener::from_raw_fd(3)
.set_nonblocking(true) };
.context("set socket non-blocking")?; std_listener
let listener = .set_nonblocking(true)
tokio::net::UnixListener::from_std(std_listener).context("wrap systemd socket")?; .context("set socket non-blocking")?;
tracing::info!("using systemd-activated socket"); let listener =
return Ok(listener); tokio::net::UnixListener::from_std(std_listener).context("wrap systemd socket")?;
} tracing::info!("using systemd-activated socket");
return Ok(listener);
} }
// Fallback: bind the socket ourselves. // Fallback: bind the socket ourselves.