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;
# Clippy as a check via crane's first-class `cargoClippy`
# builder. Reuses the shared `cargoArtifacts` (deps already
# built) and runs `cargo clippy --workspace --all-targets
# -- -D warnings` directly — no `overrideAttrs` hack needed,
# because crane parses `cargoClippyExtraArgs` correctly
# (naersk's `mode = "clippy"` used to mangle the `--`
# separator, which is why the old wiring went through
# overrideAttrs).
# built) and runs `cargo clippy --workspace --all-targets`
# directly — no `overrideAttrs` hack needed, because crane
# parses `cargoClippyExtraArgs` correctly (naersk's
# `mode = "clippy"` used to mangle the `--` separator, which
# is why the old wiring went through 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 {
src = cleanSrc;
inherit cargoArtifacts nativeBuildInputs;
pname = "hyperhive-workspace";
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
# `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
// reach another agent's matrix daemon from here).
if is_self_query
&& let Some(unread_rooms) = matrix_unread_summary().await
{
if is_self_query && let Some(unread_rooms) = matrix_unread_summary().await {
let total = u32::try_from(unread_rooms.len()).unwrap_or(u32::MAX);
if total > 0 {
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 {
Ok(DaemonResponse::Ok { payload }) => {
let finished = payload["finished"].as_bool().unwrap_or(false);
if finished {
if let Some(task) = payload.get("task") {
return format_task(id, task);
}
if finished && let Some(task) = payload.get("task") {
return format_task(id, task);
}
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,
// return the full status instead of just the task ID.
let wait = wait_seconds.unwrap_or(0);
if wait > 0 {
if let Some(task) = runner::wait_for_task(&id, wait).await {
if matches!(
task.status,
TaskStatus::Done | TaskStatus::TimedOut | TaskStatus::Interrupted
) {
return DaemonResponse::ok(&serde_json::json!({
"id": id,
"finished": true,
"task": task,
}));
}
}
if wait > 0
&& let Some(task) = runner::wait_for_task(&id, wait).await
&& matches!(
task.status,
TaskStatus::Done | TaskStatus::TimedOut | TaskStatus::Interrupted
)
{
return DaemonResponse::ok(&serde_json::json!({
"id": id,
"finished": true,
"task": task,
}));
}
DaemonResponse::ok(&serde_json::json!({ "id": id, "finished": false }))
}

View file

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

View file

@ -90,10 +90,10 @@ pub async fn rebuild_agent(
&paths,
&|step| coord.set_queue_step(queue_entry_id, step),
&|log_id| {
if let Some(qid) = queue_entry_id {
if coord.rebuild_queue.set_build_log_id(qid, log_id) {
coord.emit_rebuild_queue_snapshot();
}
if let Some(qid) = queue_entry_id
&& coord.rebuild_queue.set_build_log_id(qid, log_id)
{
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,
/// alphabetically within their tier. Stable within each depth tier.
pub fn topology_sort(
names: &mut Vec<String>,
names: &mut [String],
topo: &std::collections::BTreeMap<String, Option<String>>,
) {
use std::collections::{HashMap, VecDeque};
// Build depth map using owned clones so the borrow on `names` is released
// 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 queue: VecDeque<String> = VecDeque::new();
// Seed roots: entries with no parent, or names not present in topo at all.
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);
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) {
for ext in ["json", "out", "err"] {
let path = dir.join(format!("{stem}.{ext}"));
if path.exists() {
if let Err(e) = std::fs::remove_file(&path) {
tracing::warn!(path = %path.display(), error = ?e, "bash-tasks vacuum: remove failed");
}
if path.exists()
&& let Err(e) = std::fs::remove_file(&path)
{
tracing::warn!(path = %path.display(), error = ?e, "bash-tasks vacuum: remove failed");
}
}
}

View file

@ -8,7 +8,7 @@
use std::collections::HashMap;
use std::path::Path;
use serde::{Deserialize, Serialize};
use serde::Serialize;
use crate::coordinator::Coordinator;
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.
fn read_harness_flags(name: &str) -> (bool, bool) {
let dir = Coordinator::agent_notes_dir(name);
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 rl = v
.get("rate_limited")
.and_then(|x| x.as_bool())
.unwrap_or(false);
let nl = v
.get("needs_login")
.and_then(|x| x.as_bool())
.unwrap_or(false);
return (rl, nl);
}
if let Ok(raw) = std::fs::read_to_string(dir.join("hyperhive-harness.json"))
&& let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw)
{
let rl = v
.get("rate_limited")
.and_then(|x| x.as_bool())
.unwrap_or(false);
let nl = v
.get("needs_login")
.and_then(|x| x.as_bool())
.unwrap_or(false);
return (rl, nl);
}
// Legacy fallback: presence of individual sentinel files.
let rate_limited = dir.join("hyperhive-rate-limited").exists();

View file

@ -263,6 +263,12 @@ impl TransientKind {
}
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(
db_path: &Path,
hyperhive_flake: String,
@ -342,7 +348,7 @@ impl Coordinator {
/// creates the tmpfs entry on first call. All other paths are
/// derived statically from `name`.
///
/// ```no_run
/// ```ignore
/// let paths = Coordinator::agent_paths(name, coord.ensure_runtime(name)?);
/// ```
#[must_use]

View file

@ -26,7 +26,7 @@ use tokio_stream::{Stream, StreamExt};
use tower_http::services::ServeDir;
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::lifecycle::{self, MANAGER_NAME};

View file

@ -4,7 +4,7 @@
//! collaborator access to for operator-curated shared content.
//! 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 base64::Engine;

View file

@ -1549,7 +1549,7 @@ mod tests {
// Result must still be in .2-.254.
let ip = from_bridge.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

View file

@ -204,6 +204,12 @@ async fn main() -> Result<()> {
/// Start the coordinator daemon: open the broker, run migrations, spawn
/// background tasks (auto-update, vacuums, crash-watcher, reminder-scheduler,
/// 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(
hyperhive_flake: String,
nixpkgs_flake: String,
@ -268,10 +274,10 @@ async fn cmd_serve(
// No-op when the core token or forge are absent.
let webhook_port = dashboard_port;
tokio::spawn(async move {
if let Some(token) = forge::core_token() {
if let Err(e) = knowledge::ensure_webhook(&token, webhook_port).await {
tracing::warn!(error = ?e, "knowledge: ensure_webhook failed");
}
if let Some(token) = forge::core_token()
&& let Err(e) = knowledge::ensure_webhook(&token, webhook_port).await
{
tracing::warn!(error = ?e, "knowledge: ensure_webhook failed");
}
});
// 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.
#[allow(
clippy::too_many_lines,
clippy::too_many_arguments,
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>(
hyperhive_flake: &str,

View file

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

View file

@ -665,7 +665,9 @@ mod tests {
topo.insert("orphan".to_owned(), None);
let mut top = top_level_agents_in(&topo);
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]
@ -741,7 +743,7 @@ mod tests {
#[test]
fn reconcile_roles_in_does_not_reseed_after_explicit_revoke() {
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();
// Tombstone: manager was seen before but all roles were revoked.
roles.insert(mgr.to_owned(), vec![]);
@ -759,7 +761,7 @@ mod tests {
#[test]
fn reconcile_roles_in_seeds_root_when_absent() {
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 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;
let mut result = Vec::new();
for room in client.joined_rooms() {
let count = u32::try_from(room.unread_notification_counts().notification_count)
.unwrap_or(u32::MAX);
let count =
u32::try_from(room.unread_notification_counts().notification_count).unwrap_or(u32::MAX);
if count == 0 {
continue;
}

View file

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