diff --git a/hive-c0re/src/actions.rs b/hive-c0re/src/actions.rs index 9206a518..54a73f31 100644 --- a/hive-c0re/src/actions.rs +++ b/hive-c0re/src/actions.rs @@ -128,6 +128,28 @@ pub async fn approve(coord: Arc, id: i64) -> Result<()> { } result } + ApprovalKind::MergeConfigPr => { + // Like ApplyCommit, the work ends in a container rebuild, so + // route it through the rebuild queue. The queue worker + // dispatches MergeConfigPr approvals to `run_merge_config_pr` + // (verify the reviewed PR head, ff the forge config repo's + // main to it, mark merged, then the shared deploy tail). + coord + .rebuild_queue + .enqueue_full(crate::rebuild_queue::FullEnqueue { + kind: crate::rebuild_queue::QueueKind::Rebuild, + agent: approval.agent.clone(), + source: crate::rebuild_queue::QueueSource::Approval, + reason: format!("approval #{id} merge config pr"), + parent_id: None, + inputs: Vec::new(), + approval_id: Some(id), + perm_payload: None, + depends_on: Vec::new(), + }); + coord.emit_rebuild_queue_snapshot(); + Ok(()) + } } } @@ -381,6 +403,7 @@ fn finish_approval( ApprovalKind::InitConfig => "init_config", ApprovalKind::UpdateMetaInputs => "update_meta_inputs", ApprovalKind::SchedulePrompt => "schedule_prompt", + ApprovalKind::MergeConfigPr => "merge_config_pr", }; let sha_short = approval .fetched_sha @@ -430,6 +453,15 @@ fn finish_approval( sha: approval.fetched_sha.clone(), tag: terminal_tag, }), + // MergeConfigPr ends in a container rebuild like ApplyCommit, so + // surface the same Rebuilt lifecycle event. + ApprovalKind::MergeConfigPr => coord.notify_manager(&HelperEvent::Rebuilt { + agent: approval.agent.clone(), + ok, + note, + sha: approval.fetched_sha.clone(), + tag: terminal_tag, + }), // UpdateMetaInputs / SchedulePrompt: ApprovalResolved already // carries the result. No separate lifecycle event needed. ApprovalKind::UpdateMetaInputs | ApprovalKind::SchedulePrompt => {} @@ -809,6 +841,7 @@ pub async fn deny(coord: &Coordinator, id: i64, note: Option<&str>) -> Result<() ApprovalKind::InitConfig => "init_config", ApprovalKind::UpdateMetaInputs => "update_meta_inputs", ApprovalKind::SchedulePrompt => "schedule_prompt", + ApprovalKind::MergeConfigPr => "merge_config_pr", }; let sha_short = sha.as_deref().map(|s| s[..s.len().min(12)].to_owned()); let description = a.description.clone(); diff --git a/hive-c0re/src/approvals.rs b/hive-c0re/src/approvals.rs index 21a4b617..2abf82bc 100644 --- a/hive-c0re/src/approvals.rs +++ b/hive-c0re/src/approvals.rs @@ -335,6 +335,7 @@ fn row_to_approval(row: &rusqlite::Row<'_>) -> rusqlite::Result { "init_config" => ApprovalKind::InitConfig, "update_meta_inputs" => ApprovalKind::UpdateMetaInputs, "schedule_prompt" => ApprovalKind::SchedulePrompt, + "merge_config_pr" => ApprovalKind::MergeConfigPr, other => { return Err(rusqlite::Error::FromSqlConversionFailure( 2, @@ -383,6 +384,7 @@ pub(crate) fn kind_to_str(kind: ApprovalKind) -> &'static str { ApprovalKind::InitConfig => "init_config", ApprovalKind::UpdateMetaInputs => "update_meta_inputs", ApprovalKind::SchedulePrompt => "schedule_prompt", + ApprovalKind::MergeConfigPr => "merge_config_pr", } } @@ -393,6 +395,7 @@ fn kind_from_str(s: &str) -> Result { "init_config" => ApprovalKind::InitConfig, "update_meta_inputs" => ApprovalKind::UpdateMetaInputs, "schedule_prompt" => ApprovalKind::SchedulePrompt, + "merge_config_pr" => ApprovalKind::MergeConfigPr, other => bail!("unknown approval kind '{other}'"), }) } diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs index 21e3f7ce..5ca29f1f 100644 --- a/hive-c0re/src/dashboard.rs +++ b/hive-c0re/src/dashboard.rs @@ -870,6 +870,7 @@ fn history_view(a: Approval) -> ApprovalHistoryView { hive_sh4re::ApprovalKind::InitConfig => "init_config", hive_sh4re::ApprovalKind::UpdateMetaInputs => "update_meta_inputs", hive_sh4re::ApprovalKind::SchedulePrompt => "schedule_prompt", + hive_sh4re::ApprovalKind::MergeConfigPr => "merge_config_pr", }; ApprovalHistoryView { id: a.id, @@ -939,6 +940,24 @@ async fn build_approval_views(approvals: Vec) -> Vec { description: a.description, requested_at: a.requested_at, }, + hive_sh4re::ApprovalKind::MergeConfigPr => { + // commit_ref = PR number; fetched_sha = the reviewed PR + // head. Show the head sha; the forge PR diff surface is + // a later phase (#1838 P4) — None for now. + let sha = a + .fetched_sha + .as_deref() + .map(|s| s[..s.len().min(12)].to_owned()); + ApprovalView { + id: a.id, + agent: a.agent, + kind: "merge_config_pr", + sha_short: sha, + diff: None, + description: a.description, + requested_at: a.requested_at, + } + } }); } out diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index ed121f15..7079ec94 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -154,10 +154,12 @@ pub struct Approval { /// Kind-specific payload (git sha / inputs array / schedule /// payload / empty). See the Approval struct doc. pub commit_ref: String, - /// `ApplyCommit` only: the canonical hive-c0re-vouched sha after - /// the proposal fetch, tagged `proposal/`. Stable for the + /// The canonical hive-c0re-vouched sha. For `ApplyCommit`: the sha + /// after the proposal fetch, tagged `proposal/` (stable for the /// approval's lifetime — manager amends in proposed don't change - /// what gets built. + /// what gets built). For `MergeConfigPr`: the reviewed PR head + /// pinned at submit; if the PR head drifts off it before merge, + /// hive-c0re refreshes this + re-renders the card for re-review. #[serde(default, skip_serializing_if = "Option::is_none")] pub fetched_sha: Option, pub requested_at: i64, @@ -192,6 +194,12 @@ pub enum ApprovalKind { 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 same deploy tail as + /// `ApplyCommit`. `commit_ref` = PR number; `fetched_sha` = the + /// reviewed PR head pinned at submit. See `docs/approvals.md`. + MergeConfigPr, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]