diff --git a/docs/knowledge.md b/docs/knowledge.md index 7b1e8d39..73aa1e8f 100644 --- a/docs/knowledge.md +++ b/docs/knowledge.md @@ -50,24 +50,6 @@ hive-c0re maintains the local clone at restarted between pushes). The pull is best-effort — a failure logs a warning and does not affect the rest of the daemon. -Both paths share the same `knowledge::pull()` function, which also -handles the change notice below — neither path can forget to wire it -in since the broadcast logic lives once, in `pull()` itself, not at -each call site. - -### Change notice - -When a pull actually moves the local clone's `HEAD` (a real change, -not a no-op — e.g. the periodic pull finding nothing new), hive-c0re -broadcasts a short notice to every currently-registered agent's inbox: -sender `system`, body `[system] /knowledge updated:` followed by a -`git diff --stat ..` summary of what changed (or a generic -"see the repo" fallback if computing the diff itself fails). This is -the same broadcast mechanism used for other hive-wide notices — inbox -message only, no forced wake, and it carries the standard "this was a -broadcast" hint. A diff or per-agent send failure is logged but never -blocks the pull itself. - The local clone is created (or refreshed) once at startup via `knowledge::ensure_local_clone`. If the repo is brand new and empty, `ensure_local_clone` seeds it with the default README diff --git a/hive-c0re/src/dashboard/webhook.rs b/hive-c0re/src/dashboard/webhook.rs index 1abc8cd8..ef92beb6 100644 --- a/hive-c0re/src/dashboard/webhook.rs +++ b/hive-c0re/src/dashboard/webhook.rs @@ -129,9 +129,8 @@ pub(super) async fn post_webhook_knowledge( return (StatusCode::OK, "ignored").into_response(); } tracing::info!("webhook/knowledge: pull triggered by push to {expected_repo}"); - let coord = state.coord.clone(); - tokio::spawn(async move { - if let Err(e) = crate::knowledge::pull(&coord).await { + tokio::spawn(async { + if let Err(e) = crate::knowledge::pull().await { tracing::warn!(error = ?e, "webhook/knowledge: pull failed"); } }); diff --git a/hive-c0re/src/main.rs b/hive-c0re/src/main.rs index c43d5385..51a7633b 100644 --- a/hive-c0re/src/main.rs +++ b/hive-c0re/src/main.rs @@ -376,12 +376,11 @@ async fn cmd_serve( // missed (e.g. hive-c0re was down during a push). First fires at // startup (immediate pull after the clone is already present). let mut knowledge_shutdown = coord.shutdown_rx(); - let knowledge_coord = coord.clone(); tokio::spawn(async move { // Initial pull — reconcile any commits that landed while c0re // was offline. Not fed to the health tracker: a startup miss is // expected (the clone may not exist yet) and is logged at debug. - if let Err(e) = knowledge::pull(&knowledge_coord).await { + if let Err(e) = knowledge::pull().await { tracing::debug!(error = ?e, "knowledge: startup pull skipped (no clone yet?)"); } // Persistent-failure → banner. An hourly sweep that keeps failing for @@ -394,7 +393,7 @@ async fn cmd_serve( loop { tokio::select! { () = tokio::time::sleep(interval) => { - match knowledge::pull(&knowledge_coord).await { + match knowledge::pull().await { Ok(()) => health.record_ok(), Err(e) => { tracing::warn!(error = ?e, "knowledge: periodic pull failed"); diff --git a/hive-c0re/src/workers/knowledge.rs b/hive-c0re/src/workers/knowledge.rs index 553b3268..4cd348e4 100644 --- a/hive-c0re/src/workers/knowledge.rs +++ b/hive-c0re/src/workers/knowledge.rs @@ -16,7 +16,6 @@ use std::collections::BTreeMap; use anyhow::{Context, Result}; use forgejo_api::structs::{CreateHookOption, CreateHookOptionConfig, CreateHookOptionType}; -use crate::coordinator::Coordinator; use crate::forge::forge_git_url; pub const ORG: &str = "internal"; @@ -242,92 +241,28 @@ pub async fn ensure_webhook( Ok(()) } -/// Current `HEAD` sha of the local clone, best-effort. `None` on any -/// failure (used only to detect whether a pull actually moved `HEAD` — -/// worth skipping the broadcast over, not worth failing the pull for). -async fn head_sha() -> Option { - let out = tokio::process::Command::new("git") - .args(["-C", LOCAL_DIR, "rev-parse", "HEAD"]) - .output() - .await - .ok()?; - out.status - .success() - .then(|| String::from_utf8_lossy(&out.stdout).trim().to_owned()) -} - /// Pull the latest changes in the local clone. Called from the webhook /// handler on every push to `internal/knowledge` main, and periodically /// from `main.rs` as a fallback. Uses `--ff-only` so a force-push to /// the knowledge repo never wedges the local copy silently. -/// -/// When the pull actually moves `HEAD` (a real change, not a no-op), -/// broadcasts a short `git diff --stat` summary to every live agent's -/// inbox via `coord` — inbox-only, no forced wake, and shared by both -/// call sites since the broadcast lives in here rather than in each -/// caller. -pub async fn pull(coord: &Coordinator) -> Result<()> { +pub async fn pull() -> Result<()> { // Sanity: if the clone is missing (e.g. storage was wiped), refuse // to pull and let the caller decide whether to re-clone. let git_dir = std::path::Path::new(LOCAL_DIR).join(".git"); if !git_dir.exists() { anyhow::bail!("knowledge: {LOCAL_DIR}/.git not found — clone first"); } - let before = head_sha().await; let out = tokio::process::Command::new("git") .args(["-C", LOCAL_DIR, "pull", "--ff-only"]) .output() .await .context("git pull knowledge")?; - if !out.status.success() { + if out.status.success() { + let stdout = String::from_utf8_lossy(&out.stdout).trim().to_owned(); + tracing::info!(result = %stdout, "knowledge: pull succeeded"); + Ok(()) + } else { let stderr = String::from_utf8_lossy(&out.stderr).trim().to_owned(); anyhow::bail!("git pull {ORG}/{REPO} failed: {stderr}") } - let stdout = String::from_utf8_lossy(&out.stdout).trim().to_owned(); - tracing::info!(result = %stdout, "knowledge: pull succeeded"); - - let after = head_sha().await; - if let (Some(before), Some(after)) = (before, after) - && before != after - { - broadcast_change(coord, &before, &after).await; - } - Ok(()) -} - -/// Diff `before..after` in the local clone and broadcast a one-line-per-file -/// summary to every live agent. Best-effort — a diff or send failure is -/// logged, never propagated (the pull itself already succeeded). -async fn broadcast_change(coord: &Coordinator, before: &str, after: &str) { - let diff = tokio::process::Command::new("git") - .args([ - "-C", - LOCAL_DIR, - "diff", - "--stat", - &format!("{before}..{after}"), - ]) - .output() - .await; - let stat = match diff { - Ok(out) if out.status.success() => String::from_utf8_lossy(&out.stdout).trim().to_owned(), - Ok(out) => { - let stderr = String::from_utf8_lossy(&out.stderr).trim().to_owned(); - tracing::warn!(%stderr, "knowledge: diff --stat failed; broadcasting without it"); - String::new() - } - Err(e) => { - tracing::warn!(error = ?e, "knowledge: diff --stat failed; broadcasting without it"); - String::new() - } - }; - let body = if stat.is_empty() { - "[system] /knowledge updated — see the repo for what changed.".to_owned() - } else { - format!("[system] /knowledge updated:\n{stat}") - }; - let errors = coord.broadcast_send(hive_sh4re::SYSTEM_SENDER, &body); - if !errors.is_empty() { - tracing::warn!(?errors, "knowledge: broadcast had per-agent failures"); - } }