diff --git a/hive-agent-sock/src/lib.rs b/hive-agent-sock/src/lib.rs index 83d92b34..0045c9d7 100644 --- a/hive-agent-sock/src/lib.rs +++ b/hive-agent-sock/src/lib.rs @@ -147,7 +147,9 @@ pub enum Response { /// `CountPendingReminders` result. PendingRemindersCount { count: u64 }, /// `ReminderRollup` result. - ReminderRollup { stats: hive_sh4re::ReminderStats }, + ReminderRollup { + stats: hive_sh4re::approvals::ReminderStats, + }, /// Op failed; `message` is operator-facing. Err { message: String }, } diff --git a/hive-agent/src/reminders.rs b/hive-agent/src/reminders.rs index f5837d91..df3da591 100644 --- a/hive-agent/src/reminders.rs +++ b/hive-agent/src/reminders.rs @@ -18,7 +18,7 @@ use std::sync::Mutex; use anyhow::{Context, Result}; use chrono::{DateTime, Utc}; -use hive_sh4re::ReminderStats; +use hive_sh4re::approvals::ReminderStats; use hive_sh4re::wire_time; use rusqlite::{Connection, params}; diff --git a/hive-agent/src/stats.rs b/hive-agent/src/stats.rs index ff018018..20dbc370 100644 --- a/hive-agent/src/stats.rs +++ b/hive-agent/src/stats.rs @@ -15,7 +15,7 @@ use chrono::Utc; use rusqlite::{Connection, OpenFlags}; use serde::Serialize; -use hive_sh4re::ReminderStats; +use hive_sh4re::approvals::ReminderStats; /// Window param accepted by `/api/stats?window=`. Each maps to a /// total span + the bucket width used to roll up trend series. diff --git a/hive-agent/src/web_ui/stats.rs b/hive-agent/src/web_ui/stats.rs index 3cbc3ae3..f2f83614 100644 --- a/hive-agent/src/web_ui/stats.rs +++ b/hive-agent/src/web_ui/stats.rs @@ -29,7 +29,7 @@ pub(super) async fn api_stats( /// `HIVE_AGENT_SOCKET` — was a broker RPC before reminders moved /// in-container. Returns `None` on any transport / decode failure or /// when the socket is unset — the stats are decorative, not authoritative. -async fn fetch_reminder_stats(window_secs: u64) -> Option { +async fn fetch_reminder_stats(window_secs: u64) -> Option { match crate::todo_server::dial(&hive_agent_sock::Request::ReminderRollup { since_secs: window_secs, }) diff --git a/hive-c0re/src/actions.rs b/hive-c0re/src/actions.rs index 11c94c2f..55849150 100644 --- a/hive-c0re/src/actions.rs +++ b/hive-c0re/src/actions.rs @@ -6,7 +6,8 @@ use std::sync::Arc; use anyhow::{Context as _, Result, bail}; -use hive_sh4re::{ApprovalKind, ApprovalStatus, HelperEvent}; +use hive_sh4re::HelperEvent; +use hive_sh4re::approvals::{ApprovalKind, ApprovalStatus}; use crate::coordinator::Coordinator; use crate::lifecycle; @@ -142,7 +143,7 @@ fn rollback_ref(approval_id: i64) -> String { /// `pr` and `reviewed` are fields of the approval row the operator signed off /// on — so re-reading is both cheap and the authoritative source of truth. struct DeployCtx { - approval: hive_sh4re::Approval, + approval: hive_sh4re::approvals::Approval, /// PR number, parsed from `approval.commit_ref`. pr: u64, /// The PR head sha the operator reviewed (`approval.fetched_sha`). @@ -410,7 +411,7 @@ const PR_FAIL_LOG_TAIL_BYTES: usize = 4000; /// only the error text. async fn post_merge_failure_to_pr( coord: &Arc, - approval: &hive_sh4re::Approval, + approval: &hive_sh4re::approvals::Approval, err: &anyhow::Error, ) { let Ok(pr) = approval.commit_ref.parse::() else { @@ -472,7 +473,7 @@ fn tail_bytes(s: &str, max_bytes: usize) -> String { /// The worker takes over from here — fan-out at fire time. async fn run_approval_schedule_prompt( coord: &Coordinator, - approval: hive_sh4re::Approval, + approval: hive_sh4re::approvals::Approval, ) -> Result<()> { let result: Result<()> = async { let payload: hive_sh4re::SchedulePromptPayload = serde_json::from_str(&approval.commit_ref) @@ -598,7 +599,7 @@ fn fetch_approval_for_worker( coord: &Coordinator, approval_id: i64, expected_kind: ApprovalKind, -) -> Result { +) -> Result { let approval = coord .approvals .get(approval_id) @@ -641,7 +642,7 @@ async fn forge_after_first_spawn(coord: &Arc, agent: &str) { /// work that doesn't justify a queue card. async fn run_approval_init_config( coord: &Coordinator, - approval: hive_sh4re::Approval, + approval: hive_sh4re::approvals::Approval, proposed_dir: std::path::PathBuf, claude_dir: std::path::PathBuf, notes_dir: std::path::PathBuf, @@ -681,7 +682,7 @@ async fn run_approval_init_config( async fn finish_approval( coord: &Coordinator, - approval: &hive_sh4re::Approval, + approval: &hive_sh4re::approvals::Approval, result: Result<()>, terminal_tag: Option, ) -> Result<()> { diff --git a/hive-c0re/src/dashboard/approvals.rs b/hive-c0re/src/dashboard/approvals.rs index 5deef931..6a29f033 100644 --- a/hive-c0re/src/dashboard/approvals.rs +++ b/hive-c0re/src/dashboard/approvals.rs @@ -8,7 +8,7 @@ use axum::{ http::StatusCode, response::{IntoResponse, Response}, }; -use hive_sh4re::Approval; +use hive_sh4re::approvals::Approval; use serde::Deserialize; use utoipa::ToSchema; @@ -87,7 +87,8 @@ pub(super) fn gc_orphans(coord: &Coordinator, approvals: Vec) -> Vec ApprovalHistoryView { Some(displayed[..displayed.len().min(12)].to_owned()) }; let status = match a.status { - hive_sh4re::ApprovalStatus::Approved => "approved", - hive_sh4re::ApprovalStatus::Denied => "denied", - hive_sh4re::ApprovalStatus::Failed => "failed", - hive_sh4re::ApprovalStatus::Cancelled => "cancelled", + hive_sh4re::approvals::ApprovalStatus::Approved => "approved", + hive_sh4re::approvals::ApprovalStatus::Denied => "denied", + hive_sh4re::approvals::ApprovalStatus::Failed => "failed", + hive_sh4re::approvals::ApprovalStatus::Cancelled => "cancelled", // Pending shouldn't appear in recent_resolved, but be defensive. - hive_sh4re::ApprovalStatus::Pending => "pending", + hive_sh4re::approvals::ApprovalStatus::Pending => "pending", }; let kind = a.kind.as_str(); ApprovalHistoryView { @@ -584,7 +584,7 @@ fn build_approval_views(approvals: Vec) -> Vec { let mut out = Vec::with_capacity(approvals.len()); for a in approvals { out.push(match a.kind { - hive_sh4re::ApprovalKind::Spawn => ApprovalView { + hive_sh4re::approvals::ApprovalKind::Spawn => ApprovalView { id: a.id, agent: a.agent.to_string(), kind: "spawn", @@ -594,7 +594,7 @@ fn build_approval_views(approvals: Vec) -> Vec { commit_ref: None, requested_at: a.requested_at, }, - hive_sh4re::ApprovalKind::InitConfig => ApprovalView { + hive_sh4re::approvals::ApprovalKind::InitConfig => ApprovalView { id: a.id, agent: a.agent.to_string(), kind: "init_config", @@ -604,7 +604,7 @@ fn build_approval_views(approvals: Vec) -> Vec { commit_ref: None, requested_at: a.requested_at, }, - hive_sh4re::ApprovalKind::UpdateMetaInputs => ApprovalView { + hive_sh4re::approvals::ApprovalKind::UpdateMetaInputs => ApprovalView { id: a.id, agent: a.agent.to_string(), kind: "update_meta_inputs", @@ -614,7 +614,7 @@ fn build_approval_views(approvals: Vec) -> Vec { commit_ref: Some(a.commit_ref), requested_at: a.requested_at, }, - hive_sh4re::ApprovalKind::SchedulePrompt => ApprovalView { + hive_sh4re::approvals::ApprovalKind::SchedulePrompt => ApprovalView { id: a.id, agent: a.agent.to_string(), kind: "schedule_prompt", @@ -624,7 +624,7 @@ fn build_approval_views(approvals: Vec) -> Vec { commit_ref: Some(a.commit_ref), requested_at: a.requested_at, }, - hive_sh4re::ApprovalKind::MergeConfigPr => { + hive_sh4re::approvals::ApprovalKind::MergeConfigPr => { // commit_ref = PR number; fetched_sha = the reviewed PR // head. Show the head sha; the config diff surface lives // on the forge PR itself. diff --git a/hive-c0re/src/dashboard/webhook.rs b/hive-c0re/src/dashboard/webhook.rs index 4b220994..248c6c03 100644 --- a/hive-c0re/src/dashboard/webhook.rs +++ b/hive-c0re/src/dashboard/webhook.rs @@ -3,7 +3,7 @@ //! - **`/webhook/knowledge`** — push events on `internal/knowledge` trigger a //! `git pull` on the local clone so agents see up-to-date docs. //! - **`/webhook/config-pr`** — `pull_request` events on any `agent-configs/*` -//! repo queue a [`hive_sh4re::ApprovalKind::MergeConfigPr`] approval row +//! repo queue a [`hive_sh4re::approvals::ApprovalKind::MergeConfigPr`] approval row //! so the operator can review + approve the merge from the dashboard. //! //! Both endpoints are reached via the gateway (HTTPS, public domain URL) so diff --git a/hive-c0re/src/forge/config_pr_poll.rs b/hive-c0re/src/forge/config_pr_poll.rs index fe8ef2b2..5cb76fce 100644 --- a/hive-c0re/src/forge/config_pr_poll.rs +++ b/hive-c0re/src/forge/config_pr_poll.rs @@ -145,7 +145,7 @@ fn reconcile_stale_config_pr_approvals( } }; for a in pending { - if a.kind != hive_sh4re::ApprovalKind::MergeConfigPr + if a.kind != hive_sh4re::approvals::ApprovalKind::MergeConfigPr || !scanned_agents.contains(a.agent.as_str()) { continue; diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index 5e87597c..e72e62dc 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -88,7 +88,7 @@ async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse { tracing::info!(%name, "request_spawn"); let id = coord.approvals.submit_kind( name.as_str(), - hive_sh4re::ApprovalKind::Spawn, + hive_sh4re::approvals::ApprovalKind::Spawn, "", None, "operator", diff --git a/hive-c0re/src/socket_server/config_approvals.rs b/hive-c0re/src/socket_server/config_approvals.rs index 6cf7617b..83a2856b 100644 --- a/hive-c0re/src/socket_server/config_approvals.rs +++ b/hive-c0re/src/socket_server/config_approvals.rs @@ -57,7 +57,7 @@ pub(super) fn handle_request_update_meta_inputs( .approvals .submit_kind( requester, - hive_sh4re::ApprovalKind::UpdateMetaInputs, + hive_sh4re::approvals::ApprovalKind::UpdateMetaInputs, &commit_ref, description, requester, @@ -158,7 +158,7 @@ pub(crate) async fn submit_merge_config_pr( .approvals .submit_kind( agent, - hive_sh4re::ApprovalKind::MergeConfigPr, + hive_sh4re::approvals::ApprovalKind::MergeConfigPr, &pr_number.to_string(), description, submitter, @@ -210,7 +210,7 @@ pub(crate) fn submit_init_config( .approvals .submit_kind( name, - hive_sh4re::ApprovalKind::InitConfig, + hive_sh4re::approvals::ApprovalKind::InitConfig, parent.unwrap_or(""), description.as_deref(), // `parent` is the requesting agent (becomes the new child's diff --git a/hive-c0re/src/socket_server/schedules.rs b/hive-c0re/src/socket_server/schedules.rs index aa43e99c..e31d91db 100644 --- a/hive-c0re/src/socket_server/schedules.rs +++ b/hive-c0re/src/socket_server/schedules.rs @@ -58,7 +58,7 @@ pub(super) fn handle_request_schedule_prompt( }; let id = match coord.approvals.submit_kind( requester, - hive_sh4re::ApprovalKind::SchedulePrompt, + hive_sh4re::approvals::ApprovalKind::SchedulePrompt, &commit_ref, payload.description.as_deref(), requester, diff --git a/hive-c0re/src/stores/approvals.rs b/hive-c0re/src/stores/approvals.rs index 651af265..11b16dd9 100644 --- a/hive-c0re/src/stores/approvals.rs +++ b/hive-c0re/src/stores/approvals.rs @@ -8,7 +8,7 @@ use std::sync::Mutex; use anyhow::{Context, Result, bail}; use chrono::Utc; -use hive_sh4re::{Approval, ApprovalKind, ApprovalStatus}; +use hive_sh4re::approvals::{Approval, ApprovalKind, ApprovalStatus}; use rusqlite::{Connection, OptionalExtension, params}; use crate::db::Migration; @@ -445,7 +445,7 @@ fn kind_from_str(s: &str) -> Result { #[cfg(test)] mod tests { use super::*; - use hive_sh4re::ApprovalKind; + use hive_sh4re::approvals::ApprovalKind; fn open_temp() -> (tempfile::TempDir, std::path::PathBuf, Approvals) { let dir = tempfile::tempdir().expect("tempdir"); diff --git a/hive-host-sock/src/lib.rs b/hive-host-sock/src/lib.rs index 5073c72f..cfb2c748 100644 --- a/hive-host-sock/src/lib.rs +++ b/hive-host-sock/src/lib.rs @@ -12,7 +12,8 @@ use std::path::PathBuf; -use hive_sh4re::{AgentStatusRow, Approval}; +use hive_sh4re::AgentStatusRow; +use hive_sh4re::approvals::Approval; use hive_types::Ident; use serde::{Deserialize, Serialize}; diff --git a/hive-sh4re/src/approvals.rs b/hive-sh4re/src/approvals.rs new file mode 100644 index 00000000..a9df3c50 --- /dev/null +++ b/hive-sh4re/src/approvals.rs @@ -0,0 +1,111 @@ +//! The approval queue wire shape: one row (`Approval`) per pending/resolved +//! operator decision, its `kind` discriminator, and the terminal-state enum. +//! `ReminderStats` lives here too — small enough not to earn its own file, +//! and unrelated to any other topic module. + +use chrono::{DateTime, Utc}; +use hive_types::Ident; +use serde::{Deserialize, Serialize}; + +/// One row in the approval queue. `commit_ref` is overloaded per +/// `kind` — see `docs/approvals.md::Approval kinds (wire shapes)` +/// for the encoding table and lifecycle. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Approval { + pub id: i64, + pub agent: Ident, + #[serde(default)] + pub kind: ApprovalKind, + /// Kind-specific payload (git sha / inputs array / schedule + /// payload / empty). See the Approval struct doc. + pub commit_ref: String, + /// The canonical hive-c0re-vouched sha. For `MergeConfigPr`: the + /// reviewed PR head pinned at submit; if the PR head drifts off it + /// before merge, hive-c0re cancels the stale approval and re-queues a + /// fresh one for re-review. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub fetched_sha: Option, + pub requested_at: DateTime, + pub status: ApprovalStatus, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub resolved_at: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub note: Option, + /// Free-text description the manager attached at submission time; + /// shown on the dashboard approval card. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, +} + +/// What action the approval, when granted, will trigger. +/// Variant-specific payload encoding + flow lives in +/// `docs/approvals.md::Approval kinds (wire shapes)`. +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ApprovalKind { + /// Create + start a new sub-agent container with the given name + /// (under the default `agent.nix` template). + Spawn, + /// Create an agent's config repo and seed it from the default + /// template (step 1 of the two-step spawn flow). Creating it is the + /// whole of this step — tailoring what the template seeded is not a + /// separate mechanism, it's a `MergeConfigPr` like every later + /// change. + InitConfig, + /// Run `nix flake update [inputs...]` on the meta flake and commit + /// the resulting lock changes. + UpdateMetaInputs, + /// Add a scheduled prompt to the broker queue. + SchedulePrompt, + /// Merge an operator-reviewed config PR: hive-c0re verifies the + /// reviewed PR head, fast-forwards the forge config repo's `main` + /// to it, marks the PR merged, then runs the deploy tail. This is the + /// sole config-change flow — a manager opens a PR on its + /// `agent-configs/` repo and the operator reviews + approves it. + /// `commit_ref` = PR number; `fetched_sha` = the reviewed PR head + /// pinned at submit. See `docs/approvals.md`. + #[default] + MergeConfigPr, +} + +impl ApprovalKind { + /// Wire/UI string — the same value serde's `snake_case` rename + /// produces. The single source of truth for every place that needs + /// the kind as a `&'static str` (sqlite storage, dashboard events), + /// so adding a variant can't silently miss a hand-rolled match. + #[must_use] + pub fn as_str(self) -> &'static str { + match self { + ApprovalKind::Spawn => "spawn", + ApprovalKind::InitConfig => "init_config", + ApprovalKind::UpdateMetaInputs => "update_meta_inputs", + ApprovalKind::SchedulePrompt => "schedule_prompt", + ApprovalKind::MergeConfigPr => "merge_config_pr", + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ApprovalStatus { + Pending, + Approved, + Denied, + Failed, + /// Manager withdrew the request before the operator acted on it. + /// Distinct from `Denied` (operator decision) and `Failed` + /// (post-approval lifecycle error). See + /// `docs/approvals.md::Withdrawing a pending approval`. + Cancelled, +} + +/// Reminder activity statistics for an agent over a time window. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReminderStats { + /// Total reminders scheduled in the window (`created_at` >= cutoff). + pub scheduled: u64, + /// Reminders that have been delivered in the window (`sent_at` IS NOT NULL). + pub delivered: u64, + /// Reminders still pending in the window (`sent_at` IS NULL). + pub pending: u64, +} diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index eb1da503..78c78380 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -4,6 +4,7 @@ use chrono::{DateTime, Utc}; use hive_types::Ident; use serde::{Deserialize, Serialize}; +pub mod approvals; pub mod assets; pub mod bash_task; pub mod paths; @@ -56,109 +57,6 @@ pub fn pending_hint(remaining: u64) -> String { ) } -/// One row in the approval queue. `commit_ref` is overloaded per -/// `kind` — see `docs/approvals.md::Approval kinds (wire shapes)` -/// for the encoding table and lifecycle. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Approval { - pub id: i64, - pub agent: Ident, - #[serde(default)] - pub kind: ApprovalKind, - /// Kind-specific payload (git sha / inputs array / schedule - /// payload / empty). See the Approval struct doc. - pub commit_ref: String, - /// The canonical hive-c0re-vouched sha. For `MergeConfigPr`: the - /// reviewed PR head pinned at submit; if the PR head drifts off it - /// before merge, hive-c0re cancels the stale approval and re-queues a - /// fresh one for re-review. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub fetched_sha: Option, - pub requested_at: DateTime, - pub status: ApprovalStatus, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub resolved_at: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub note: Option, - /// Free-text description the manager attached at submission time; - /// shown on the dashboard approval card. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub description: Option, -} - -/// What action the approval, when granted, will trigger. -/// Variant-specific payload encoding + flow lives in -/// `docs/approvals.md::Approval kinds (wire shapes)`. -#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum ApprovalKind { - /// Create + start a new sub-agent container with the given name - /// (under the default `agent.nix` template). - Spawn, - /// Create an agent's config repo and seed it from the default - /// template (step 1 of the two-step spawn flow). Creating it is the - /// whole of this step — tailoring what the template seeded is not a - /// separate mechanism, it's a `MergeConfigPr` like every later - /// change. - InitConfig, - /// Run `nix flake update [inputs...]` on the meta flake and commit - /// the resulting lock changes. - UpdateMetaInputs, - /// Add a scheduled prompt to the broker queue. - SchedulePrompt, - /// Merge an operator-reviewed config PR: hive-c0re verifies the - /// reviewed PR head, fast-forwards the forge config repo's `main` - /// to it, marks the PR merged, then runs the deploy tail. This is the - /// sole config-change flow — a manager opens a PR on its - /// `agent-configs/` repo and the operator reviews + approves it. - /// `commit_ref` = PR number; `fetched_sha` = the reviewed PR head - /// pinned at submit. See `docs/approvals.md`. - #[default] - MergeConfigPr, -} - -impl ApprovalKind { - /// Wire/UI string — the same value serde's `snake_case` rename - /// produces. The single source of truth for every place that needs - /// the kind as a `&'static str` (sqlite storage, dashboard events), - /// so adding a variant can't silently miss a hand-rolled match. - #[must_use] - pub fn as_str(self) -> &'static str { - match self { - ApprovalKind::Spawn => "spawn", - ApprovalKind::InitConfig => "init_config", - ApprovalKind::UpdateMetaInputs => "update_meta_inputs", - ApprovalKind::SchedulePrompt => "schedule_prompt", - ApprovalKind::MergeConfigPr => "merge_config_pr", - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum ApprovalStatus { - Pending, - Approved, - Denied, - Failed, - /// Manager withdrew the request before the operator acted on it. - /// Distinct from `Denied` (operator decision) and `Failed` - /// (post-approval lifecycle error). See - /// `docs/approvals.md::Withdrawing a pending approval`. - Cancelled, -} - -/// Reminder activity statistics for an agent over a time window. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ReminderStats { - /// Total reminders scheduled in the window (`created_at` >= cutoff). - pub scheduled: u64, - /// Reminders that have been delivered in the window (`sent_at` IS NOT NULL). - pub delivered: u64, - /// Reminders still pending in the window (`sent_at` IS NULL). - pub pending: u64, -} - // ----------------------------------------------------------------------------- // Per-agent socket — /run/hyperhive/agents//mcp.sock on the host, // bind-mounted into the container at /run/hive/mcp.sock. @@ -456,7 +354,7 @@ pub enum HelperEvent { id: i64, agent: String, commit_ref: String, - status: ApprovalStatus, + status: approvals::ApprovalStatus, #[serde(default, skip_serializing_if = "Option::is_none")] note: Option, #[serde(default, skip_serializing_if = "Option::is_none")]