From 484cea62c7f49e4b94e67bfcf31d760e20d916b4 Mon Sep 17 00:00:00 2001 From: damocles Date: Fri, 22 May 2026 19:02:11 +0200 Subject: [PATCH] fix #265: resolve all remaining clippy warnings (cast, too_many_lines, doc nits) --- hive-ag3nt/src/bin/hive-ag3nt.rs | 18 ++--- hive-ag3nt/src/bin/hive-m1nd.rs | 1 + hive-ag3nt/src/forge_notify.rs | 14 ++-- hive-ag3nt/src/turn.rs | 2 + hive-ag3nt/src/web_ui.rs | 4 +- hive-c0re/src/actions.rs | 2 + hive-c0re/src/broker.rs | 1 + hive-c0re/src/container_view.rs | 4 +- hive-c0re/src/events_vacuum.rs | 2 +- hive-c0re/src/lifecycle.rs | 126 +++++++++++++++---------------- hive-c0re/src/main.rs | 5 +- hive-c0re/src/stats_vacuum.rs | 2 +- 12 files changed, 95 insertions(+), 86 deletions(-) diff --git a/hive-ag3nt/src/bin/hive-ag3nt.rs b/hive-ag3nt/src/bin/hive-ag3nt.rs index 4b06199..4510da1 100644 --- a/hive-ag3nt/src/bin/hive-ag3nt.rs +++ b/hive-ag3nt/src/bin/hive-ag3nt.rs @@ -149,7 +149,7 @@ async fn main() -> Result<()> { } } -#[allow(clippy::too_many_arguments, clippy::similar_names)] +#[allow(clippy::too_many_arguments, clippy::similar_names, clippy::too_many_lines)] async fn serve( socket: &Path, interval: Duration, @@ -307,14 +307,14 @@ async fn serve( } } -/// Per-turn user prompt. The role/tools/etc. is in the system prompt -/// (`prompts/agent.md` → `claude --system-prompt-file`); this is just the -/// wake signal claude reacts to. `unread` is the count of *other* -/// messages in the inbox right after this one was popped. -/// `redelivered` flags messages that were popped in a prior harness -/// session, never acked, and resurfaced after a restart — a banner -/// at the top of the wake prompt warns that any side-effects of -/// previous handling may already have happened. +// Per-turn user prompt: the role/tools/etc. is in the system prompt +// (`prompts/agent.md` → `claude --system-prompt-file`); this is just the +// wake signal claude reacts to. `unread` is the count of *other* +// messages in the inbox right after this one was popped. +// `redelivered` flags messages that were popped in a prior harness +// session, never acked, and resurfaced after a restart — a banner +// at the top of the wake prompt warns that any side-effects of +// previous handling may already have happened. /// Best-effort: tell the broker every message we popped during the /// turn is now fully handled (turn-end-OK). Swallows transport diff --git a/hive-ag3nt/src/bin/hive-m1nd.rs b/hive-ag3nt/src/bin/hive-m1nd.rs index 816ce64..00054f7 100644 --- a/hive-ag3nt/src/bin/hive-m1nd.rs +++ b/hive-ag3nt/src/bin/hive-m1nd.rs @@ -113,6 +113,7 @@ async fn main() -> Result<()> { } } +#[allow(clippy::too_many_lines)] // linear startup sequence; splitting adds indirection without clarity async fn serve( socket: &Path, interval: Duration, diff --git a/hive-ag3nt/src/forge_notify.rs b/hive-ag3nt/src/forge_notify.rs index b043424..e0682d6 100644 --- a/hive-ag3nt/src/forge_notify.rs +++ b/hive-ag3nt/src/forge_notify.rs @@ -22,6 +22,7 @@ //! are silently marked read — the agent already knows it opened them. //! - Comment notifications where the comment author matches this agent's own //! forge login are silently marked read. +//! //! Own login is fetched once at startup via `GET /user` and cached for the //! lifetime of the polling loop. //! @@ -32,6 +33,7 @@ //! generic `[comment on PR #N repo]` so agents can action it immediately. use std::collections::HashSet; +use std::fmt::Write as _; use std::path::{Path, PathBuf}; use std::time::Duration; @@ -209,6 +211,7 @@ fn review_state_label(state: &str) -> Option<&str> { /// /// Number is extracted from `html_url` last path segment before any `#`. /// Repo slug (`owner/name`) is always included — agents may watch multiple repos. +#[allow(clippy::too_many_lines)] // multiple notification types handled in one place by design async fn format_notification( client: &reqwest::Client, token: &str, @@ -329,9 +332,9 @@ async fn format_notification( let kind = format!("PR {review_label}{num}{repo}"); let mut out = format!("[{kind}] {title}\nurl: {url}"); if body_text.is_empty() { - out.push_str(&format!("\n\nreviewer: {author}")); + write!(out, "\n\nreviewer: {author}").ok(); } else { - out.push_str(&format!("\n\n{author}: {}", truncate(body_text, BODY_TRUNCATE))); + write!(out, "\n\n{author}: {}", truncate(body_text, BODY_TRUNCATE)).ok(); } out.push_str(&meta_suffix); Some(out) @@ -449,15 +452,12 @@ async fn poll_once( debug!(count = notifications.len(), "forge_notify: delivering notifications"); for notif in ¬ifications { - let id = match notif["id"].as_u64() { - Some(n) => n, - None => continue, - }; + let Some(id) = notif["id"].as_u64() else { continue }; let body_opt = format_notification(client, token, notif, own_login).await; // None means self-echo — mark read silently, no delivery. - let body = if let Some(b) = body_opt { b } else { + let Some(body) = body_opt else { mark_read(client, forge_url, token, id).await; continue; }; diff --git a/hive-ag3nt/src/turn.rs b/hive-ag3nt/src/turn.rs index db5a69c..7aefc63 100644 --- a/hive-ag3nt/src/turn.rs +++ b/hive-ag3nt/src/turn.rs @@ -198,6 +198,7 @@ pub fn rate_limit_sleep_secs() -> u64 { /// 1. `HIVE_AUTO_RESET_WATERMARK_TOKENS` env var (explicit override). /// 2. 50% of the model's context window (derived from `bus.model()` + /// `events::context_window_tokens`). +/// /// `0` disables auto-reset entirely. fn auto_reset_watermark_tokens(bus: &Bus) -> u64 { if let Some(v) = std::env::var("HIVE_AUTO_RESET_WATERMARK_TOKENS") @@ -223,6 +224,7 @@ fn cache_ttl_secs() -> u64 { /// 1. `HIVE_COMPACT_WATERMARK_TOKENS` env var (explicit override). /// 2. 75% of the model's context window (derived from `bus.model()` + /// `events::context_window_tokens`). +/// /// `0` disables proactive compaction (reactive path still applies). fn compact_watermark_tokens(bus: &Bus) -> u64 { if let Some(v) = std::env::var("HIVE_COMPACT_WATERMARK_TOKENS") diff --git a/hive-ag3nt/src/web_ui.rs b/hive-ag3nt/src/web_ui.rs index a0d2b0b..79d0c5b 100644 --- a/hive-ag3nt/src/web_ui.rs +++ b/hive-ag3nt/src/web_ui.rs @@ -335,7 +335,9 @@ async fn api_stats( // Pass the window span to the reminder-stats RPC so the broker // filters its counts to the same time range as the chart data. let window_secs = window.span_secs(); - snapshot.reminder_stats = fetch_reminder_stats(&state.socket, state.flavor(), window_secs as u64).await; + #[allow(clippy::cast_sign_loss)] // window span is always a positive duration + let window_secs_u = window_secs.max(0) as u64; + snapshot.reminder_stats = fetch_reminder_stats(&state.socket, state.flavor(), window_secs_u).await; axum::Json(snapshot) } diff --git a/hive-c0re/src/actions.rs b/hive-c0re/src/actions.rs index 977b9f2..02d12dd 100644 --- a/hive-c0re/src/actions.rs +++ b/hive-c0re/src/actions.rs @@ -23,6 +23,7 @@ use crate::lifecycle::{self, MANAGER_NAME}; /// /// In all cases an `ApprovalResolved` helper event lands in the manager's /// inbox when the work resolves. +#[allow(clippy::too_many_lines)] // approval dispatch covers several independent approval kinds pub async fn approve(coord: Arc, id: i64) -> Result<()> { let approval = coord.approvals.mark_approved(id)?; tracing::info!( @@ -268,6 +269,7 @@ fn finish_approval( /// and reset the working tree back to the last known-good main. main /// never advances on a failed build, so a crash-and-recover doesn't /// leave the agent pointing at a tree it can't evaluate. +#[allow(clippy::too_many_lines)] // sequential build/tag/notify pipeline; splitting would obscure the flow async fn run_apply_commit( coord: &Arc, approval: &hive_sh4re::Approval, diff --git a/hive-c0re/src/broker.rs b/hive-c0re/src/broker.rs index f2e1698..ea88934 100644 --- a/hive-c0re/src/broker.rs +++ b/hive-c0re/src/broker.rs @@ -609,6 +609,7 @@ impl Broker { /// in the last `since_secs` seconds (0 = all reminders). pub fn reminder_rollup_for(&self, agent: &str, since_secs: u64) -> Result { let conn = self.conn.lock().unwrap(); + #[allow(clippy::cast_possible_wrap)] // unix epoch secs fit in i64 until year 292B let cutoff_time = if since_secs > 0 { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) diff --git a/hive-c0re/src/container_view.rs b/hive-c0re/src/container_view.rs index ffbf5b9..0f4c51b 100644 --- a/hive-c0re/src/container_view.rs +++ b/hive-c0re/src/container_view.rs @@ -164,8 +164,8 @@ fn is_rate_limited(name: &str) -> bool { /// silently yields `None` so a missing/corrupt file never blocks /// `build_all`. /// -/// Context tokens = `last_input_tokens + last_cache_read_input_tokens -/// + last_cache_creation_input_tokens`, mirroring +/// Context tokens are the sum of `last_input_tokens`, `last_cache_read_input_tokens`, +/// and `last_cache_creation_input_tokens`, mirroring /// `hive_ag3nt::events::TokenUsage::context_tokens`. fn read_last_ctx_tokens(name: &str) -> Option { let path = Coordinator::agent_notes_dir(name).join("hyperhive-turn-stats.sqlite"); diff --git a/hive-c0re/src/events_vacuum.rs b/hive-c0re/src/events_vacuum.rs index b545f22..8a487cc 100644 --- a/hive-c0re/src/events_vacuum.rs +++ b/hive-c0re/src/events_vacuum.rs @@ -23,7 +23,7 @@ const KEEP_SECS: i64 = 7 * 24 * 3600; /// Background loop: sweep every existing agent state dir hourly, run /// the vacuum SQL against its events.sqlite if present. Errors are /// logged but don't tear the loop down. -pub fn spawn(coord: Arc) { +pub fn spawn(coord: &Arc) { let mut shutdown = coord.shutdown_rx(); tokio::spawn(async move { loop { diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index 5e34a02..2f67992 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -767,69 +767,6 @@ async fn systemd_daemon_reload() -> Result<()> { Ok(()) } -#[cfg(test)] -mod tests { - use super::*; - - /// Regression test: setup_proposed must seed both agent.nix and flake.nix - /// in the initial commit. Before commit 5b5a93e flake.nix was missing from - /// the scaffold, requiring manual creation (seen with the damocles agent). - #[tokio::test] - async fn setup_proposed_seeds_flake_nix() { - let dir = tempfile::tempdir().expect("tempdir"); - let proposed = dir.path().join("proposed"); - setup_proposed(&proposed, "test-agent") - .await - .expect("setup_proposed"); - - // Both files must exist on disk. - assert!(proposed.join("agent.nix").exists(), "agent.nix missing"); - assert!(proposed.join("flake.nix").exists(), "flake.nix missing"); - - // flake.nix must export nixosModules.default (the meta-flake contract). - let flake = std::fs::read_to_string(proposed.join("flake.nix")).unwrap(); - assert!( - flake.contains("nixosModules.default"), - "flake.nix does not export nixosModules.default" - ); - - // Both files must be tracked in the initial git commit. - let out = git_command() - .current_dir(&proposed) - .args(["show", "--name-only", "--format=", "HEAD"]) - .output() - .await - .expect("git show"); - let tracked = String::from_utf8_lossy(&out.stdout); - assert!(tracked.contains("agent.nix"), "agent.nix not committed"); - assert!(tracked.contains("flake.nix"), "flake.nix not committed"); - } - - /// setup_proposed is idempotent: calling it on an existing repo is a - /// no-op (the fresh guard skips all writes). - #[tokio::test] - async fn setup_proposed_idempotent() { - let dir = tempfile::tempdir().expect("tempdir"); - let proposed = dir.path().join("proposed"); - setup_proposed(&proposed, "test-agent") - .await - .expect("first call"); - // Second call must not error even though .git already exists. - setup_proposed(&proposed, "test-agent") - .await - .expect("second call"); - // Still one commit. - let out = git_command() - .current_dir(&proposed) - .args(["rev-list", "--count", "HEAD"]) - .output() - .await - .expect("git rev-list"); - let count = String::from_utf8_lossy(&out.stdout).trim().to_owned(); - assert_eq!(count, "1", "expected exactly one commit after idempotent call"); - } -} - /// Idempotently rewrite the lines in `/etc/nixos-containers/.conf` /// that hive-c0re owns: `PRIVATE_NETWORK` (forced 0 so the agent's web UI port /// is reachable on the host) and `EXTRA_NSPAWN_FLAGS` (the runtime-dir bind). @@ -1097,3 +1034,66 @@ async fn container_journal_tail(args: &[&str]) -> String { _ => String::new(), } } + +#[cfg(test)] +mod tests { + use super::*; + + /// Regression test: `setup_proposed` must seed both agent.nix and flake.nix + /// in the initial commit. Before commit 5b5a93e flake.nix was missing from + /// the scaffold, requiring manual creation (seen with the damocles agent). + #[tokio::test] + async fn setup_proposed_seeds_flake_nix() { + let dir = tempfile::tempdir().expect("tempdir"); + let proposed = dir.path().join("proposed"); + setup_proposed(&proposed, "test-agent") + .await + .expect("setup_proposed"); + + // Both files must exist on disk. + assert!(proposed.join("agent.nix").exists(), "agent.nix missing"); + assert!(proposed.join("flake.nix").exists(), "flake.nix missing"); + + // flake.nix must export nixosModules.default (the meta-flake contract). + let flake = std::fs::read_to_string(proposed.join("flake.nix")).unwrap(); + assert!( + flake.contains("nixosModules.default"), + "flake.nix does not export nixosModules.default" + ); + + // Both files must be tracked in the initial git commit. + let out = git_command() + .current_dir(&proposed) + .args(["show", "--name-only", "--format=", "HEAD"]) + .output() + .await + .expect("git show"); + let tracked = String::from_utf8_lossy(&out.stdout); + assert!(tracked.contains("agent.nix"), "agent.nix not committed"); + assert!(tracked.contains("flake.nix"), "flake.nix not committed"); + } + + /// `setup_proposed` is idempotent: calling it on an existing repo is a + /// no-op (the fresh guard skips all writes). + #[tokio::test] + async fn setup_proposed_idempotent() { + let dir = tempfile::tempdir().expect("tempdir"); + let proposed = dir.path().join("proposed"); + setup_proposed(&proposed, "test-agent") + .await + .expect("first call"); + // Second call must not error even though .git already exists. + setup_proposed(&proposed, "test-agent") + .await + .expect("second call"); + // Still one commit. + let out = git_command() + .current_dir(&proposed) + .args(["rev-list", "--count", "HEAD"]) + .output() + .await + .expect("git rev-list"); + let count = String::from_utf8_lossy(&out.stdout).trim().to_owned(); + assert_eq!(count, "1", "expected exactly one commit after idempotent call"); + } +} diff --git a/hive-c0re/src/main.rs b/hive-c0re/src/main.rs index 70f9409..74a8e18 100644 --- a/hive-c0re/src/main.rs +++ b/hive-c0re/src/main.rs @@ -100,6 +100,7 @@ enum Cmd { } #[tokio::main] +#[allow(clippy::too_many_lines)] // top-level dispatch; hard to split without losing readability async fn main() -> Result<()> { tracing_subscriber::fmt() .with_env_filter( @@ -186,10 +187,10 @@ async fn main() -> Result<()> { }); // Per-agent events.sqlite vacuum: host-side so the harness // doesn't need any retention wiring of its own. - events_vacuum::spawn(coord.clone()); + events_vacuum::spawn(&coord); // Per-agent turn-stats.sqlite vacuum: same pattern, 90-day // retention so trend analysis has enough history. - stats_vacuum::spawn(coord.clone()); + stats_vacuum::spawn(&coord); // Container crash watcher: emits HelperEvent::ContainerCrash // when a previously-running container goes away without an // operator-initiated transient state. diff --git a/hive-c0re/src/stats_vacuum.rs b/hive-c0re/src/stats_vacuum.rs index 4f92160..c08be60 100644 --- a/hive-c0re/src/stats_vacuum.rs +++ b/hive-c0re/src/stats_vacuum.rs @@ -21,7 +21,7 @@ const KEEP_SECS: i64 = 90 * 24 * 3600; /// Background loop: sweep every existing agent state dir hourly, run /// the vacuum SQL against its turn-stats.sqlite if present. Errors /// are logged but don't tear the loop down. -pub fn spawn(coord: Arc) { +pub fn spawn(coord: &Arc) { let mut shutdown = coord.shutdown_rx(); tokio::spawn(async move { loop {