diff --git a/flake.nix b/flake.nix index 1c7d8032..bafe4a9d 100644 --- a/flake.nix +++ b/flake.nix @@ -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` diff --git a/hive-ag3nt/src/mcp.rs b/hive-ag3nt/src/mcp.rs index 278452df..8290e6f1 100644 --- a/hive-ag3nt/src/mcp.rs +++ b/hive-ag3nt/src/mcp.rs @@ -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); diff --git a/hive-bash-mcp/src/bin/mcp.rs b/hive-bash-mcp/src/bin/mcp.rs index 26a75e1a..6c87ac62 100644 --- a/hive-bash-mcp/src/bin/mcp.rs +++ b/hive-bash-mcp/src/bin/mcp.rs @@ -108,10 +108,8 @@ fn render_bash_run(id: &str, resp: Result) -> 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}") } diff --git a/hive-bash-mcp/src/socket.rs b/hive-bash-mcp/src/socket.rs index d9e515be..5334c976 100644 --- a/hive-bash-mcp/src/socket.rs +++ b/hive-bash-mcp/src/socket.rs @@ -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 })) } diff --git a/hive-c0re/src/actions.rs b/hive-c0re/src/actions.rs index 0364d8ed..5235d226 100644 --- a/hive-c0re/src/actions.rs +++ b/hive-c0re/src/actions.rs @@ -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(); } }, ) diff --git a/hive-c0re/src/auto_update.rs b/hive-c0re/src/auto_update.rs index 3f21a210..355499ef 100644 --- a/hive-c0re/src/auto_update.rs +++ b/hive-c0re/src/auto_update.rs @@ -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) -> 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, + names: &mut [String], topo: &std::collections::BTreeMap>, ) { 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 = names.clone(); + let name_set: Vec = names.to_vec(); let mut depth: HashMap = HashMap::new(); let mut queue: VecDeque = 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()); } diff --git a/hive-c0re/src/bash_tasks_vacuum.rs b/hive-c0re/src/bash_tasks_vacuum.rs index 8bdb3a00..ae0975af 100644 --- a/hive-c0re/src/bash_tasks_vacuum.rs +++ b/hive-c0re/src/bash_tasks_vacuum.rs @@ -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"); } } } diff --git a/hive-c0re/src/container_view.rs b/hive-c0re/src/container_view.rs index 2a507795..841f9f68 100644 --- a/hive-c0re/src/container_view.rs +++ b/hive-c0re/src/container_view.rs @@ -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::(&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::(&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(); diff --git a/hive-c0re/src/coordinator.rs b/hive-c0re/src/coordinator.rs index c8260098..8efd4d50 100644 --- a/hive-c0re/src/coordinator.rs +++ b/hive-c0re/src/coordinator.rs @@ -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] diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs index 9290ab9f..c03e4ef2 100644 --- a/hive-c0re/src/dashboard.rs +++ b/hive-c0re/src/dashboard.rs @@ -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}; diff --git a/hive-c0re/src/forge.rs b/hive-c0re/src/forge.rs index 19ec131b..83ae2f0c 100644 --- a/hive-c0re/src/forge.rs +++ b/hive-c0re/src/forge.rs @@ -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; diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index c11c3bc0..b8e00982 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -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 diff --git a/hive-c0re/src/main.rs b/hive-c0re/src/main.rs index 07b5aed2..3cfb814f 100644 --- a/hive-c0re/src/main.rs +++ b/hive-c0re/src/main.rs @@ -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 diff --git a/hive-c0re/src/meta.rs b/hive-c0re/src/meta.rs index f2e10772..8acb7142 100644 --- a/hive-c0re/src/meta.rs +++ b/hive-c0re/src/meta.rs @@ -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( hyperhive_flake: &str, diff --git a/hive-c0re/src/rebuild_queue.rs b/hive-c0re/src/rebuild_queue.rs index f415ac1d..3da40a2d 100644 --- a/hive-c0re/src/rebuild_queue.rs +++ b/hive-c0re/src/rebuild_queue.rs @@ -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), diff --git a/hive-c0re/src/topology.rs b/hive-c0re/src/topology.rs index 7f3e319f..a24946bd 100644 --- a/hive-c0re/src/topology.rs +++ b/hive-c0re/src/topology.rs @@ -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> = 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> = BTreeMap::new(); // empty let should_seed = agent_names.iter().any(|n| n == mgr) && !roles.contains_key(mgr); diff --git a/hive-matrix-mcp/src/handlers.rs b/hive-matrix-mcp/src/handlers.rs index 219dd097..ad3549dd 100644 --- a/hive-matrix-mcp/src/handlers.rs +++ b/hive-matrix-mcp/src/handlers.rs @@ -450,8 +450,8 @@ pub async fn collect_unread(client: &Client) -> Vec 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; } diff --git a/hive-priv/src/main.rs b/hive-priv/src/main.rs index 71c7e6e2..29c79532 100644 --- a/hive-priv/src/main.rs +++ b/hive-priv/src/main.rs @@ -66,21 +66,22 @@ fn socket_listener() -> Result { .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.