diff --git a/docs/agent-lifecycle/config-changes.md b/docs/agent-lifecycle/config-changes.md index 17d31cf0c..a85ad59f5 100644 --- a/docs/agent-lifecycle/config-changes.md +++ b/docs/agent-lifecycle/config-changes.md @@ -338,10 +338,6 @@ once already applied: repo, and refreshes the files that follow the agent set (`agent_set::refresh`) from the current agent list. Set `HIVE_SKIP_META_MIGRATION=1` on the service to defer this phase. -- **Legacy meta repo**: deletes the leftover `.git` in - `/var/lib/hyperhive/meta/`, the `meta` remote in each proposed repo and the - unread `/var/lib/hyperhive/forge-core-token`. The files beside the `.git` - stay. The forge-side `core/meta` repo and `core` account aren't touched. No state loss in either migration: claude creds, `/state/` notes, the events DB, and both proposed + applied history all survive. The root diff --git a/docs/agent-lifecycle/persistence.md b/docs/agent-lifecycle/persistence.md index c42e24533..ecb27ad18 100644 --- a/docs/agent-lifecycle/persistence.md +++ b/docs/agent-lifecycle/persistence.md @@ -388,9 +388,8 @@ here from merged config PRs) + any other files those PRs commit. denied tag history. Under `/var/lib/hyperhive/meta/` — system-level config files, plain state -with no git history. The flake agents build from is the -swarm's `internal/meta`, not here. A hive that predates this finds a -leftover `.git` here; `migrate::run` deletes it at startup. Contents: +with no git history that anything reads. The flake agents build from is the +swarm's `internal/meta`, not here. Contents: - `topology.json` — the agent roster (`["alice", "bob", "ruth"]`). Written by `topology::reconcile` from `agent_set::refresh`; read by diff --git a/docs/scheduler/coordinator.md b/docs/scheduler/coordinator.md index fb7807758..eb7f5f6b8 100644 --- a/docs/scheduler/coordinator.md +++ b/docs/scheduler/coordinator.md @@ -320,8 +320,8 @@ The hive keeps no flake of its own. `meta_rev.rs` picks the `internal/meta` commit an agent builds from and records it; `agent_set.rs` keeps the host-side files that follow the agent set (`topology.json`, the gateway's agent-socket map and nginx include) in line with the live agents, and -`meta.rs` removes the permission seed files once the swarm has them, and -`migrate.rs` deletes the legacy `meta/` repo's leftover `.git`. +`meta.rs` keeps the one remaining commit to the legacy `meta/` git repo, the +removal of the permission seed files. --- diff --git a/hive-c0re/src/migrate.rs b/hive-c0re/src/migrate.rs index 04f04d9c4..3beb067c0 100644 --- a/hive-c0re/src/migrate.rs +++ b/hive-c0re/src/migrate.rs @@ -1,6 +1,5 @@ //! Startup convergence. Three phases, all idempotent and unguarded: -//! harness files, applied + proposed repos, the agent-set files, plus the -//! removal of the legacy meta repo's leftovers. They re-run every +//! harness files, applied + proposed repos, the agent-set files. They re-run every //! boot on purpose — each one is a no-op once its state is already //! correct. //! @@ -69,17 +68,6 @@ pub async fn run() -> Result<()> { migrate_harness_files(name); } - match remove_core_token(&crate::paths::state_root()) { - Ok(true) => tracing::info!("migration: removed the unread forge-core-token"), - Ok(false) => {} - Err(e) => tracing::warn!(error = ?e, "migration: removing the forge-core-token failed"), - } - match remove_legacy_meta_git(&crate::paths::meta_root()) { - Ok(true) => tracing::info!("migration: removed the legacy meta repo's .git"), - Ok(false) => {} - Err(e) => tracing::warn!(error = ?e, "migration: removing the legacy meta .git failed"), - } - // Phase 1 + 2: per-agent applied + proposed. tracing::debug!("migration: phase 1+2 (applied + proposed repos)"); // An archetype agent has neither repo, and seeding one here would hand it @@ -97,13 +85,6 @@ pub async fn run() -> Result<()> { tracing::warn!(%name, error = ?e, "migration: applied repo rewrite failed"); } let proposed_dir = Coordinator::agent_proposed_dir(name); - match remove_meta_remote(&proposed_dir).await { - Ok(true) => tracing::info!(%name, "migration: removed the legacy meta remote"), - Ok(false) => {} - Err(e) => { - tracing::warn!(%name, error = ?e, "migration: removing the meta remote failed"); - } - } let proposed = lifecycle::setup_proposed(&proposed_dir, name.as_str()); match tokio::time::timeout(GIT_TIMEOUT, proposed).await { Ok(Err(e)) => tracing::warn!(%name, error = ?e, "migration: setup_proposed failed"), @@ -167,42 +148,6 @@ fn migrate_harness_files(name: &hive_types::Ident) { } } -/// Remove the `.git` of the legacy per-hive meta repo from `meta`, leaving -/// the plain state beside it (`topology.json`, the swarm limits, any -/// permission seed file not yet imported). `true` when it was there. -fn remove_legacy_meta_git(meta: &Path) -> Result { - let git_dir = meta.join(".git"); - match std::fs::remove_dir_all(&git_dir) { - Ok(()) => Ok(true), - Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false), - Err(e) => Err(e).with_context(|| format!("removing {}", git_dir.display())), - } -} - -/// Remove the forge admin token file an earlier hive left in `state`; nothing -/// reads it. `true` when it was there. -fn remove_core_token(state: &Path) -> Result { - let file = state.join("forge-core-token"); - match std::fs::remove_file(&file) { - Ok(()) => Ok(true), - Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false), - Err(e) => Err(e).with_context(|| format!("removing {}", file.display())), - } -} - -/// Remove the `meta` remote from the proposed repo at `dir`. `true` when it -/// was there; a missing remote, or a `dir` that is no repo, is `false`. -async fn remove_meta_remote(dir: &Path) -> Result { - let mut cmd = lifecycle::git_command(); - cmd.current_dir(dir).args(["remote", "get-url", "meta"]); - let probe = output_with_timeout(cmd, GIT_TIMEOUT, "git remote get-url meta").await?; - if !probe.status.success() { - return Ok(false); - } - raw_git(dir, &["remote", "remove", "meta"]).await?; - Ok(true) -} - async fn enumerate_agents() -> Result> { let containers = lifecycle::list().await?; Ok(containers @@ -303,79 +248,6 @@ async fn raw_git(dir: &Path, args: &[&str]) -> Result<()> { mod tests { use super::*; - const STATE_FILES: [&str; 3] = [ - "topology.json", - "swarm-resource-limits.json", - "capabilities.json", - ]; - - #[test] - fn legacy_meta_git_goes_and_the_state_beside_it_stays() { - let dir = tempfile::tempdir().expect("tempdir"); - std::fs::create_dir_all(dir.path().join(".git/objects")).expect("mkdir .git"); - for name in STATE_FILES { - std::fs::write(dir.path().join(name), "{}").expect("write state"); - } - - assert!(remove_legacy_meta_git(dir.path()).expect("first")); - assert!(!dir.path().join(".git").exists()); - for name in STATE_FILES { - assert!(dir.path().join(name).exists(), "{name} stays"); - } - assert!( - !remove_legacy_meta_git(dir.path()).expect("second"), - "no-op" - ); - assert!(dir.path().join("topology.json").exists()); - } - - #[tokio::test] - async fn meta_remote_goes_and_the_others_stay() { - let dir = tempfile::tempdir().expect("tempdir"); - raw_git(dir.path(), &["init", "-q", "--initial-branch=main"]) - .await - .expect("init"); - for (name, url) in [ - ("meta", "http://forge/core/meta.git"), - ("applied", "/applied/x/.git"), - ] { - raw_git(dir.path(), &["remote", "add", name, url]) - .await - .expect("add remote"); - } - - assert!(remove_meta_remote(dir.path()).await.expect("first")); - let remotes = lifecycle::git_command() - .current_dir(dir.path()) - .arg("remote") - .output() - .await - .expect("list"); - assert_eq!(String::from_utf8_lossy(&remotes.stdout).trim(), "applied"); - assert!( - !remove_meta_remote(dir.path()).await.expect("second"), - "no-op" - ); - } - - #[test] - fn core_token_goes_once_and_its_neighbours_stay() { - let dir = tempfile::tempdir().expect("tempdir"); - std::fs::write(dir.path().join("forge-core-token"), "x").expect("write token"); - std::fs::write(dir.path().join("forge-other"), "x").expect("write neighbour"); - - assert!(remove_core_token(dir.path()).expect("first")); - assert!(!dir.path().join("forge-core-token").exists()); - assert!(dir.path().join("forge-other").exists(), "neighbour stays"); - assert!(!remove_core_token(dir.path()).expect("second"), "no-op"); - } - - #[tokio::test] - async fn a_dir_that_is_no_repo_has_no_meta_remote() { - let dir = tempfile::tempdir().expect("tempdir"); - assert!(!remove_meta_remote(dir.path()).await.expect("probe")); - } - #[tokio::test] async fn unreadable_flake_is_left_untouched() { let dir = tempfile::tempdir().expect("tempdir");