diff --git a/hive-c0re/src/forge/mod.rs b/hive-c0re/src/forge/mod.rs index f9be3d02..7fdfc35f 100644 --- a/hive-c0re/src/forge/mod.rs +++ b/hive-c0re/src/forge/mod.rs @@ -169,9 +169,15 @@ pub(crate) fn api(token: &str) -> Result { /// /// Called by both `ensure_all()` (startup sweep) and `rebuild_agent` /// (per-rebuild) so the two paths stay equivalent. -pub async fn sync_agent(name: &str, core_token: Option<&str>) { +/// Returns `true` if all steps succeeded, `false` if any step failed. The +/// caller can use the return value to aggregate per-agent failures into a +/// dashboard warning (see [`ensure_all`]); the rebuild path ignores it and +/// relies on the journal `warn!` lines alone (a rebuild is its own retry). +pub async fn sync_agent(name: &str, core_token: Option<&str>) -> bool { + let mut ok = true; if let Err(e) = ensure_user_for(name).await { tracing::warn!(%name, error = ?e, "forge: ensure_user failed"); + ok = false; } // Align email to match the git user.email set by meta::render_flake // so commits link to the agent's Forgejo profile. Best-effort; @@ -188,9 +194,11 @@ pub async fn sync_agent(name: &str, core_token: Option<&str>) { // was down. if let Err(e) = ensure_config_repo(name).await { tracing::warn!(%name, error = ?e, "forge: ensure_config_repo failed"); + ok = false; } if let Err(e) = push_config(name).await { tracing::warn!(%name, error = ?e, "forge: push_config failed"); + ok = false; } // Grant read-only access to core/meta and wire the `meta` remote // into the proposed repo so agents can fetch their deployment context. @@ -198,9 +206,11 @@ pub async fn sync_agent(name: &str, core_token: Option<&str>) { && let Err(e) = meta_read_access(name, token).await { tracing::warn!(%name, error = ?e, "forge: ensure_meta_read_access failed"); + ok = false; } if let Err(e) = ensure_meta_remote(name).await { tracing::warn!(%name, error = ?e, "forge: ensure_meta_remote failed"); + ok = false; } // Grant read-only access to internal/docs so the agent can clone // the operator-curated shared skills/runbook repo. Best-effort. @@ -208,8 +218,10 @@ pub async fn sync_agent(name: &str, core_token: Option<&str>) { && let Err(e) = shared_docs_access(name, token).await { tracing::warn!(%name, error = ?e, "forge: shared_docs_access failed"); + ok = false; } // internal/knowledge is public — no per-agent collaborator grant needed. + ok } /// The `core_token.is_some()` half of [`ensure_all`]: orgs, teams, the meta @@ -346,11 +358,27 @@ pub async fn ensure_all() { ); return; }; + let mut sync_failed: Vec = Vec::new(); for c in containers { let Some(name) = c.strip_prefix(crate::lifecycle::AGENT_PREFIX) else { continue; }; - sync_agent(name, core_token.as_deref()).await; + if !sync_agent(name, core_token.as_deref()).await { + sync_failed.push(name.to_owned()); + } + } + if !sync_failed.is_empty() { + // Use static_kind to mint a `&'static str` key from the failed-agent + // list (bounded leak: one per hive-c0re boot, not per request). + let key = static_kind(format!("forge_sync_agent_{}", sync_failed.join("_"))); + crate::warnings::set_boot_warning( + key, + "warn", + format!( + "forge: per-agent sync failed for: {} (see journal for per-step detail)", + sync_failed.join(", ") + ), + ); } }