diff --git a/docs/approvals.md b/docs/approvals.md index 2656eb0e..4d27167e 100644 --- a/docs/approvals.md +++ b/docs/approvals.md @@ -90,7 +90,7 @@ initial configuration before any container is created. ### Approval kinds (wire shapes) -`ApprovalKind` carries six variants; each maps to a different +`ApprovalKind` carries five variants; each maps to a different `commit_ref` encoding because that field is overloaded as the kind-specific payload carrier. @@ -99,16 +99,6 @@ kind-specific payload carrier. proposal fetch lives in `fetched_sha` on the same `Approval` row (only `ApplyCommit` populates it). See the End-to-end flow above. -- `MergeConfigPr` — the PR-based config flow's counterpart to - `ApplyCommit`. `commit_ref` stores the **PR number** (decimal), - and `fetched_sha` is the PR **head sha the operator reviewed**. - On approve, `run_merge_config_pr` re-reads the live PR head and - aborts if it drifted from `fetched_sha` (re-review), then fetches - that head into the applied repo, eval-verifies it, fast-forwards - the forge config repo's `main` to it (the merge), marks the PR - merged (best-effort — `main` is already there), and runs the same - shared deploy tail as `ApplyCommit` (`deploy_applied_target`). - Never a first spawn. - `Spawn` — direct container creation under the default `agent.nix` template. `commit_ref` is empty. Submitted via `HostRequest::RequestSpawn` (operator-gated, the @@ -333,7 +323,6 @@ the approval handler enqueues a `QueueEntry` into the global | `ApprovalKind` | `QueueKind` queued | `QueueSource` | |---|---|---| | `ApplyCommit` | `Rebuild` | `Approval` | -| `MergeConfigPr` | `Rebuild` | `Approval` | | `UpdateMetaInputs` | `MetaUpdate` | `Approval` | | `Spawn` | `Spawn` | `Approval` | | `InitConfig` | — runs inline (sub-second git seed) | — | @@ -342,8 +331,7 @@ the approval handler enqueues a `QueueEntry` into the global Each queue entry carries the originating `approval_id` so the worker can re-fetch the approval row when it dispatches, run the kind-specific pipeline (`run_approval_apply_commit` / -`run_approval_merge_config_pr` / `run_approval_update_meta_inputs` / -`run_approval_spawn`), and +`run_approval_update_meta_inputs` / `run_approval_spawn`), and fire the matching `HelperEvent::*` on completion via `finish_approval`. diff --git a/hive-c0re/src/actions.rs b/hive-c0re/src/actions.rs index dfa733ea..9206a518 100644 --- a/hive-c0re/src/actions.rs +++ b/hive-c0re/src/actions.rs @@ -46,12 +46,20 @@ pub async fn approve(coord: Arc, id: i64) -> Result<()> { run_approval_init_config(&coord, approval, proposed_dir, claude_dir, notes_dir).await } ApprovalKind::ApplyCommit => { - enqueue_approval_rebuild( - &coord, - &approval.agent, - id, - format!("approval #{id} apply commit"), - ); + 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} apply commit"), + parent_id: None, + inputs: Vec::new(), + approval_id: Some(id), + perm_payload: None, + depends_on: Vec::new(), + }); + coord.emit_rebuild_queue_snapshot(); Ok(()) } ApprovalKind::UpdateMetaInputs => { @@ -120,49 +128,9 @@ 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). - enqueue_approval_rebuild( - &coord, - &approval.agent, - id, - format!("approval #{id} merge config pr"), - ); - Ok(()) - } } } -/// Enqueue a `Rebuild` queue entry tied to an approval id. Shared by the -/// `ApplyCommit` and `MergeConfigPr` dispatch arms — both end in a container -/// rebuild routed through the queue, differing only in the queue `reason`. -/// The queue worker branches on the approval's kind to pick the right handler. -fn enqueue_approval_rebuild( - coord: &Arc, - agent: &str, - approval_id: i64, - reason: String, -) { - coord - .rebuild_queue - .enqueue_full(crate::rebuild_queue::FullEnqueue { - kind: crate::rebuild_queue::QueueKind::Rebuild, - agent: agent.to_owned(), - source: crate::rebuild_queue::QueueSource::Approval, - reason, - parent_id: None, - inputs: Vec::new(), - approval_id: Some(approval_id), - perm_payload: None, - depends_on: Vec::new(), - }); - coord.emit_rebuild_queue_snapshot(); -} - /// Worker entry point for `ApprovalKind::ApplyCommit` queue entries. /// Re-fetches the approval row, runs the commit pipeline, and fires /// `ApprovalResolved` + the lifecycle event (`Rebuilt` / `Spawned` @@ -192,159 +160,6 @@ pub async fn run_approval_apply_commit( finish_approval(coord, &approval, result, terminal_tag, is_first_spawn) } -/// Worker entry point for `ApprovalKind::MergeConfigPr` queue entries — -/// the PR-based config flow's counterpart to `run_approval_apply_commit`. -/// Re-fetches the approval row, runs the merge pipeline, and fires -/// `ApprovalResolved` + the `Rebuilt` lifecycle event via `finish_approval`. -/// Unlike the apply-commit path it does NOT call `push_config` afterwards: -/// `run_merge_config_pr` already fast-forwarded the forge repo's `main` to -/// the reviewed head (that IS the merge), so a mirror push would be a no-op. -/// A `MergeConfigPr` is never a first spawn (the agent already exists). -pub async fn run_approval_merge_config_pr( - coord: &Arc, - queue_entry_id: Option, - approval_id: i64, -) -> Result<()> { - let approval = fetch_approval_for_worker(coord, approval_id, ApprovalKind::MergeConfigPr)?; - let agent_dir = coord.ensure_runtime(&approval.agent)?; - let applied_dir = Coordinator::agent_applied_dir(&approval.agent); - coord.set_queue_step(queue_entry_id, "merge config pr"); - let (result, terminal_tag) = - run_merge_config_pr(coord, &approval, &agent_dir, &applied_dir, queue_entry_id).await; - finish_approval(coord, &approval, result, terminal_tag, false) -} - -/// PR-merge config pipeline. `approval.commit_ref` is the PR number; -/// `approval.fetched_sha` is the PR head sha the operator reviewed. Steps: -/// 1. drift gate — re-read the live PR head; if it moved since review, abort -/// WITHOUT mutating anything (the operator must re-review the new head); -/// 2. fetch the reviewed head into the applied repo so later git ops resolve -/// it locally; -/// 3. eval-verify the reviewed commit against the meta flake BEFORE the -/// irreversible push (same gate `run_apply_commit` uses); -/// 4. fast-forward the forge repo's `main` to the reviewed head — THE merge; -/// 5. mark the PR merged (best-effort: `main` is already at the head, so a -/// failure here is logged, not fatal); -/// 6. run the shared deploy tail (`deploy_applied_target`): ff applied/main, -/// meta deploy, container rebuild, finalize/rollback. -/// -/// Returns `(build result, terminal tag)` like `deploy_applied_target`. Any -/// pre-merge abort returns `Err` with no mutation; the operator re-reviews. -async fn run_merge_config_pr( - coord: &Arc, - approval: &hive_sh4re::Approval, - agent_dir: &std::path::Path, - applied_dir: &std::path::Path, - queue_entry_id: Option, -) -> (Result<()>, Option) { - let id = approval.id; - let pr: u64 = match approval.commit_ref.parse() { - Ok(n) => n, - Err(e) => { - return ( - Err(anyhow::anyhow!( - "parse PR number from commit_ref {:?}: {e}", - approval.commit_ref - )), - None, - ); - } - }; - let reviewed = match approval.fetched_sha.as_deref() { - Some(s) => s.to_owned(), - None => { - return ( - Err(anyhow::anyhow!( - "merge config pr approval {id} has no reviewed head sha" - )), - None, - ); - } - }; - let repo = crate::forge::config_repo(&approval.agent); - - // 1. Drift gate: the live PR head must still equal what was reviewed. - coord.set_queue_step(queue_entry_id, "verify PR head"); - let head = match crate::forge::pr_head_sha(&repo, pr).await { - Ok(h) => h, - Err(e) => return (Err(anyhow::anyhow!("read PR #{pr} head: {e}")), None), - }; - if head != reviewed { - return ( - Err(anyhow::anyhow!( - "PR #{pr} head drifted since review (reviewed {reviewed}, now {head}); re-review before merging" - )), - None, - ); - } - - // 2. Fetch the reviewed head into applied so ff/verify/deploy resolve it. - coord.set_queue_step(queue_entry_id, "fetch PR head"); - if let Err(e) = crate::forge::fetch_pr_head_into_applied(&repo, pr).await { - return ( - Err(anyhow::anyhow!("fetch PR #{pr} head into applied: {e}")), - None, - ); - } - - // 3. Eval-verify BEFORE the irreversible push (bad nix fails fast here). - coord.set_queue_step(queue_entry_id, "verify proposal (eval)"); - if let Err(e) = crate::meta::verify_commit(&approval.agent, applied_dir, &reviewed).await { - return ( - Err(anyhow::anyhow!("verify merge head {reviewed}: {e:#}")), - None, - ); - } - - // Capture the currently-deployed sha for the deploy tail's rollback. - let prev_main_sha = match lifecycle::git_rev_parse(applied_dir, "refs/heads/main").await { - Ok(s) => s, - Err(e) => return (Err(anyhow::anyhow!("read applied/main: {e:#}")), None), - }; - - // 4. THE merge: fast-forward the forge repo's main to the reviewed head. - coord.set_queue_step(queue_entry_id, "fast-forward forge main"); - match crate::forge::ff_push_to_main(&repo, &reviewed).await { - Ok(()) => {} - Err(crate::forge::ForgeMergeError::NotFastForward { .. }) => { - return ( - Err(anyhow::anyhow!( - "PR #{pr}: forge main raced ahead of reviewed {reviewed}; re-review before merging" - )), - None, - ); - } - Err(e) => return (Err(anyhow::anyhow!("ff-push PR #{pr} to main: {e}")), None), - } - - // 5. Mark the PR merged. Best-effort: main is already at the reviewed - // head, so the deploy is correct regardless — a failure here (incl. - // HeadDrift vs the forge PR record) is logged, not fatal. - coord.set_queue_step(queue_entry_id, "mark PR merged"); - if let Err(e) = crate::forge::mark_pr_merged(&repo, pr, &reviewed).await { - tracing::warn!( - agent = %approval.agent, %id, %pr, error = ?e, - "mark PR merged failed; main already at reviewed head, continuing to deploy" - ); - } - - // 6. Shared deploy tail. target == finalize == the reviewed head; - // never a first spawn (the agent already exists). - deploy_applied_target( - coord, - &approval.agent, - agent_dir, - applied_dir, - &reviewed, - &reviewed, - id, - &prev_main_sha, - false, - queue_entry_id, - ) - .await -} - /// Inline (non-queued) handler for `ApprovalKind::SchedulePrompt`. /// On approve, decode the `SchedulePromptPayload` JSON from the /// approval's `commit_ref`, insert a row into `scheduled_prompts` @@ -566,7 +381,6 @@ 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 @@ -609,19 +423,13 @@ fn finish_approval( sha: approval.fetched_sha.clone(), }); } - // MergeConfigPr ends in a container rebuild just like a - // non-first-spawn ApplyCommit, so both surface the same Rebuilt - // lifecycle event. (MergeConfigPr is never a first spawn — the - // agent already exists — so it never hits the Spawned arm above.) - ApprovalKind::ApplyCommit | ApprovalKind::MergeConfigPr => { - coord.notify_manager(&HelperEvent::Rebuilt { - agent: approval.agent.clone(), - ok, - note, - sha: approval.fetched_sha.clone(), - tag: terminal_tag, - }); - } + ApprovalKind::ApplyCommit => 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 => {} @@ -638,8 +446,12 @@ fn finish_approval( /// (`deployed/`) or annotate `failed/` with the build error /// 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. The shared -/// ff/deploy/rebuild/finalize tail lives in `deploy_applied_target`. +/// leave the agent pointing at a tree it can't evaluate. +#[allow( + clippy::too_many_lines, + reason = "one sequential build/tag/notify pipeline; splitting the steps \ + across helpers would obscure the linear flow without shrinking it" +)] async fn run_apply_commit( coord: &Arc, approval: &hive_sh4re::Approval, @@ -730,76 +542,28 @@ async fn run_apply_commit( ); } - // Fast-forward applied/main to the proposal, run the meta deploy + - // container rebuild, and finalize/roll-back — the tail shared with the - // PR-merge flow. ApplyCommit's target == finalize sha source is - // `fetched_sha` (or the proposal ref when unset), matching the prior - // inline behavior exactly. - let (result, tag) = deploy_applied_target( - coord, - &approval.agent, - agent_dir, - applied_dir, - &proposal_ref, - approval.fetched_sha.as_deref().unwrap_or(&proposal_ref), - id, - &prev_main_sha, - is_first_spawn, - queue_entry_id, - ) - .await; - (result, tag, is_first_spawn) -} - -/// Shared deploy tail for config-applying approvals (`ApplyCommit` + the -/// PR-merge flow). Fast-forwards `applied/main` to `target_ref`, syncs the -/// working tree, runs the meta two-phase deploy + container rebuild, and -/// plants the `deployed/` / `failed/` bookkeeping tags. -/// On build failure it rolls `applied/main` back to `prev_main_sha` and aborts -/// the staged meta lock so the agent stays on its last-good tree. Returns the -/// build result + the terminal tag name. -/// -/// Caller-specific bits stay OUT of here: the source fetch (proposal tag vs -/// forge fetch), the `approved/building` tags, `verify_commit`, and any forge -/// ff-push / mark-merged. `is_first_spawn` gates the one-time meta -/// `sync_agents` step (only `ApplyCommit`'s first spawn passes `true`; -/// the PR-merge flow always passes `false` — the agent already exists). -/// `finalize_sha` is the sha recorded by `meta::finalize_deploy`; `target_ref` -/// is what `applied/main` fast-forwards to (a proposal ref or a commit sha). -#[allow( - clippy::too_many_arguments, - clippy::too_many_lines, - reason = "one sequential ff/deploy/rebuild/finalize pipeline shared by both \ - config-apply callers; splitting it would obscure the linear flow" -)] -async fn deploy_applied_target( - coord: &Arc, - agent: &str, - agent_dir: &std::path::Path, - applied_dir: &std::path::Path, - target_ref: &str, - finalize_sha: &str, - tag_base: i64, - prev_main_sha: &str, - is_first_spawn: bool, - queue_entry_id: Option, -) -> (Result<()>, Option) { - let id = tag_base; - coord.set_queue_step(queue_entry_id, "fast-forward applied/main"); - // Fast-forward applied/main to target_ref + sync the working tree. - // Meta input pins `?ref=main`, so this is what makes nix re-lock to - // the target commit on the prepare_deploy step below. On build - // failure we roll main back to prev_main_sha so a crash leaves the - // agent on its last-good tree. - if let Err(e) = lifecycle::git_update_ref(applied_dir, "refs/heads/main", target_ref).await { - return (Err(anyhow::anyhow!("ff main to {target_ref}: {e:#}")), None); + // Fast-forward applied/main to proposal/ + sync the working + // tree. Meta input pins `?ref=main`, so this is what makes nix + // re-lock to the proposal commit on the prepare_deploy step + // below. On build failure we roll main back to prev_main_sha so + // a crash leaves the agent on its last-good tree. + if let Err(e) = lifecycle::git_update_ref(applied_dir, "refs/heads/main", &proposal_ref).await { + return ( + Err(anyhow::anyhow!("ff main to {proposal_ref}: {e:#}")), + None, + is_first_spawn, + ); } if let Err(e) = lifecycle::git_read_tree_reset(applied_dir, "refs/heads/main").await { // main is ahead; working tree didn't sync. Roll main back to // keep the two consistent before bailing. - let _ = lifecycle::git_update_ref(applied_dir, "refs/heads/main", prev_main_sha).await; - return (Err(anyhow::anyhow!("read-tree to main: {e:#}")), None); + let _ = lifecycle::git_update_ref(applied_dir, "refs/heads/main", &prev_main_sha).await; + return ( + Err(anyhow::anyhow!("read-tree to main: {e:#}")), + None, + is_first_spawn, + ); } // First spawn: sync_agents must add this agent to the meta flake @@ -807,34 +571,40 @@ async fn deploy_applied_target( // exist yet if this is the agent's first deploy). if is_first_spawn { coord.set_queue_step(queue_entry_id, "meta sync_agents (first spawn)"); - let agents = match lifecycle::agents_for_meta_listing_with(agent).await { + let agents = match lifecycle::agents_for_meta_listing_with(&approval.agent).await { Ok(a) => a, Err(e) => { let _ = - lifecycle::git_update_ref(applied_dir, "refs/heads/main", prev_main_sha).await; + lifecycle::git_update_ref(applied_dir, "refs/heads/main", &prev_main_sha).await; let _ = lifecycle::git_read_tree_reset(applied_dir, "refs/heads/main").await; return ( Err(anyhow::anyhow!("agents_for_meta_listing_with: {e:#}")), None, + is_first_spawn, ); } }; if let Err(e) = crate::meta::sync_agents(&coord.hive_env(), &agents).await { - let _ = lifecycle::git_update_ref(applied_dir, "refs/heads/main", prev_main_sha).await; + let _ = lifecycle::git_update_ref(applied_dir, "refs/heads/main", &prev_main_sha).await; let _ = lifecycle::git_read_tree_reset(applied_dir, "refs/heads/main").await; return ( Err(anyhow::anyhow!("meta sync_agents for first spawn: {e:#}")), None, + is_first_spawn, ); } } coord.set_queue_step(queue_entry_id, "meta prepare_deploy"); // Phase 1 of the meta two-phase deploy: relock without committing. - if let Err(e) = crate::meta::prepare_deploy(agent).await { - let _ = lifecycle::git_update_ref(applied_dir, "refs/heads/main", prev_main_sha).await; + if let Err(e) = crate::meta::prepare_deploy(&approval.agent).await { + let _ = lifecycle::git_update_ref(applied_dir, "refs/heads/main", &prev_main_sha).await; let _ = lifecycle::git_read_tree_reset(applied_dir, "refs/heads/main").await; - return (Err(anyhow::anyhow!("meta prepare_deploy: {e:#}")), None); + return ( + Err(anyhow::anyhow!("meta prepare_deploy: {e:#}")), + None, + is_first_spawn, + ); } // Container-level rebuild (or first-time create) against meta#. @@ -842,9 +612,9 @@ async fn deploy_applied_target( // the dashboard reflects actual phase progress rather than a static // "nixos-container update" label for the whole multi-minute window. let hive = coord.hive_env(); - let paths = Coordinator::agent_paths(agent, agent_dir.to_path_buf()); + let paths = Coordinator::agent_paths(&approval.agent, agent_dir.to_path_buf()); let build_result = lifecycle::rebuild_no_meta( - agent, + &approval.agent, &hive, &paths, &|step| coord.set_queue_step(queue_entry_id, step), @@ -862,47 +632,54 @@ async fn deploy_applied_target( Ok(()) => { coord.set_queue_step(queue_entry_id, "finalize deploy"); let tag = format!("deployed/{id}"); - if let Err(e) = lifecycle::git_tag(applied_dir, &tag, target_ref).await { - tracing::warn!(%agent, %id, error = ?e, "plant deployed tag failed"); + if let Err(e) = lifecycle::git_tag(applied_dir, &tag, &proposal_ref).await { + tracing::warn!(agent = %approval.agent, %id, error = ?e, "plant deployed tag failed"); } - if let Err(e) = crate::meta::finalize_deploy(agent, finalize_sha, &tag).await { + if let Err(e) = crate::meta::finalize_deploy( + &approval.agent, + approval.fetched_sha.as_deref().unwrap_or(&proposal_ref), + &tag, + ) + .await + { // The build itself succeeded — meta lock landed but // couldn't be committed. Surface as a soft warn so the // operator can git-commit by hand if they care. - tracing::warn!(%agent, %id, error = ?e, "meta finalize_deploy failed"); + tracing::warn!(agent = %approval.agent, %id, error = ?e, "meta finalize_deploy failed"); } // Wake the agent on its next turn so claude sees the // config change took effect. Same hint pattern as // auto_update::rebuild_agent — manager approved a // proposal, agent picks up where it left off with the // new env / packages. - coord.kick_agent(agent, "config update applied"); - (Ok(()), Some(tag)) + coord.kick_agent(&approval.agent, "config update applied"); + (Ok(()), Some(tag), is_first_spawn) } Err(e) => { let tag = format!("failed/{id}"); let body = format!("{e:#}"); if let Err(te) = - lifecycle::git_tag_annotated(applied_dir, &tag, target_ref, &body).await + lifecycle::git_tag_annotated(applied_dir, &tag, &proposal_ref, &body).await { - tracing::warn!(%agent, %id, error = ?te, "annotate failed tag failed"); + tracing::warn!(agent = %approval.agent, %id, error = ?te, "annotate failed tag failed"); } // Roll main back to last known-good so the on-disk state // matches what nixos-container last successfully built. if let Err(re) = - lifecycle::git_update_ref(applied_dir, "refs/heads/main", prev_main_sha).await + lifecycle::git_update_ref(applied_dir, "refs/heads/main", &prev_main_sha).await { - tracing::warn!(%agent, %id, error = ?re, "main rollback failed"); + tracing::warn!(agent = %approval.agent, %id, error = ?re, "main rollback failed"); } if let Err(re) = lifecycle::git_read_tree_reset(applied_dir, "refs/heads/main").await { - tracing::warn!(%agent, %id, error = ?re, "rollback read-tree failed"); + tracing::warn!(agent = %approval.agent, %id, error = ?re, "rollback read-tree failed"); } // Drop the staged meta lock change so the deploy log // only ever shows successes. if let Err(ae) = crate::meta::abort_deploy().await { - tracing::warn!(%agent, %id, error = ?ae, "meta abort_deploy failed"); + tracing::warn!(agent = %approval.agent, %id, error = ?ae, "meta abort_deploy failed"); } - (Err(e), Some(tag)) + let _ = coord; + (Err(e), Some(tag), is_first_spawn) } } } @@ -1032,7 +809,6 @@ 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 2abf82bc..21a4b617 100644 --- a/hive-c0re/src/approvals.rs +++ b/hive-c0re/src/approvals.rs @@ -335,7 +335,6 @@ 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, @@ -384,7 +383,6 @@ 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", } } @@ -395,7 +393,6 @@ 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 a09ece5f..21e3f7ce 100644 --- a/hive-c0re/src/dashboard.rs +++ b/hive-c0re/src/dashboard.rs @@ -870,7 +870,6 @@ 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, @@ -940,24 +939,6 @@ 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 of the PR-based config flow — 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-c0re/src/forge.rs b/hive-c0re/src/forge.rs index e2d953c1..8a3c12b2 100644 --- a/hive-c0re/src/forge.rs +++ b/hive-c0re/src/forge.rs @@ -1225,43 +1225,6 @@ pub async fn pr_head_sha(repo: &str, pr: u64) -> Result Ok(sha.to_string()) } -/// Full `owner/name` path of an agent's config repo on the forge — the -/// `agent-configs` org mirror that the PR-merge flow reads + fast-forwards. -pub fn config_repo(agent: &str) -> String { - format!("{CONFIG_ORG}/{agent}") -} - -/// Fetch PR #`pr`'s head into the agent's applied repo via -/// `refs/pull//head` (which Forgejo always serves — a bare-sha fetch can -/// be refused by uploadpack policy). This makes the reviewed head an object -/// in the applied repo so the ancestor check in [`ff_push_to_main`], the -/// `git_update_ref(main, …)` in the deploy tail, and the eval-verify all -/// resolve it locally before the irreversible push. -/// -/// # Errors -/// `Other` on transport failure or a non-zero git exit. -pub async fn fetch_pr_head_into_applied(repo: &str, pr: u64) -> Result<(), ForgeMergeError> { - let token = core_token() - .ok_or_else(|| ForgeMergeError::Other(anyhow::anyhow!("forge core token absent")))?; - let url = tokenised_repo_url(repo, &token); - let applied = Coordinator::agent_applied_dir(repo_agent_name(repo)); - let refspec = format!("refs/pull/{pr}/head"); - let out = crate::lifecycle::git_command() - .current_dir(&applied) - .args(["fetch", "--no-tags", &url, &refspec]) - .output() - .await - .context("git fetch PR head into applied")?; - if !out.status.success() { - return Err(ForgeMergeError::Other(anyhow::anyhow!( - "git fetch {repo} {refspec} into applied failed ({}): {}", - out.status, - String::from_utf8_lossy(&out.stderr).trim() - ))); - } - Ok(()) -} - /// Fast-forward the forge repo's `main` to `sha` — THE merge in the PR flow. /// Reads `main`'s current sha (`git ls-remote … refs/heads/main`), verifies it /// is a strict ancestor of `sha` (`git merge-base --is-ancestor`, run in the diff --git a/hive-c0re/src/rebuild_queue.rs b/hive-c0re/src/rebuild_queue.rs index 55bd76f5..6a816f84 100644 --- a/hive-c0re/src/rebuild_queue.rs +++ b/hive-c0re/src/rebuild_queue.rs @@ -852,36 +852,13 @@ pub async fn run_fast_worker(coord: std::sync::Arc, - entry: &QueueEntry, - approval_id: i64, -) -> anyhow::Result<()> { - let kind = coord - .approvals - .get(approval_id) - .ok() - .flatten() - .map(|a| a.kind); - if kind == Some(hive_sh4re::ApprovalKind::MergeConfigPr) { - crate::actions::run_approval_merge_config_pr(coord, Some(entry.id), approval_id).await - } else { - crate::actions::run_approval_apply_commit(coord, Some(entry.id), approval_id).await - } -} - async fn dispatch( coord: &std::sync::Arc, entry: &QueueEntry, ) -> anyhow::Result<()> { match (entry.kind, entry.approval_id) { (QueueKind::Rebuild, Some(approval_id)) => { - dispatch_rebuild_approval(coord, entry, approval_id).await + crate::actions::run_approval_apply_commit(coord, Some(entry.id), approval_id).await } (QueueKind::Rebuild, None) => { let current_rev = diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index 7079ec94..ed121f15 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -154,12 +154,10 @@ pub struct Approval { /// 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 `ApplyCommit`: the sha - /// after the proposal fetch, tagged `proposal/` (stable for the + /// `ApplyCommit` only: the canonical hive-c0re-vouched sha after + /// the proposal fetch, tagged `proposal/`. Stable for the /// approval's lifetime — manager amends in proposed don't change - /// 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. + /// what gets built. #[serde(default, skip_serializing_if = "Option::is_none")] pub fetched_sha: Option, pub requested_at: i64, @@ -194,12 +192,6 @@ 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)]