diff --git a/docs/turn-loop.md b/docs/turn-loop.md
index cd3e7ab5..52b32eb9 100644
--- a/docs/turn-loop.md
+++ b/docs/turn-loop.md
@@ -342,14 +342,6 @@ meta's.
one-shot otherwise. Even self-targeted schedules go through
approval (use `remind` for unapproved self-wake). Long downtime
fires once per recurring row on resume (catch-up clamp).
-- `edit_schedule(id, body?, description?, interval_seconds?, next_fire_at_unix?, targets_add?, targets_remove?)` —
- partial-update a schedule (#474/#478). Pass only the fields to
- change; absent fields are left alone. `targets_add` / `targets_remove`
- mutate the recipient list in the same transaction; re-adding a
- previously-cancelled target drops its tombstone + history (fresh
- start). Clearing a scalar (e.g. `interval_seconds: null`) flips
- recurring→one-shot. Refuses cancelled rows. Same authorization as
- `cancel_schedule`.
- `cancel_schedule(id, targets?)` — cancel a schedule. Omit
`targets` / pass empty to cancel the whole schedule; pass a list
to cancel just those recipients (auto-cancels when every target
diff --git a/frontend/packages/dashboard/src/dashboard.css b/frontend/packages/dashboard/src/dashboard.css
index c7b53f9d..1c00ae6c 100644
--- a/frontend/packages/dashboard/src/dashboard.css
+++ b/frontend/packages/dashboard/src/dashboard.css
@@ -1164,18 +1164,10 @@ summary:hover { color: var(--purple); }
text-indent: 0;
}
.live .msgrow .msg-body {
- /* #485: body takes a full flex line of its own beneath the
- metadata chips (ts / arrow / from / sep / to). Previously the
- body sat inline with `flex: 1 1 0`, eating whatever the chips
- left — which on a long timestamp + agent names + arrows meant
- the body started ~30ch in and wrapped awkwardly. Pushing
- `flex-basis: 100%` forces the body to wrap to its own line in
- the existing `flex-wrap: wrap` row, where it can use the full
- width down to the row's content edge.
- `min-width: 0` still applies so `word-break: break-word`
- actually kicks in instead of forcing the row wider than its
- container. */
- flex: 1 1 100%;
+ flex: 1 1 0;
+ /* min-width: 0 lets the body shrink below its longest token so
+ `word-break: break-word` actually kicks in instead of forcing
+ the whole flex line wider than the container. */
min-width: 0;
}
.live .msgrow.sent .msg-arrow { color: var(--cyan); }
diff --git a/frontend/packages/dashboard/src/flow.html b/frontend/packages/dashboard/src/flow.html
index 407f3b85..195f1c14 100644
--- a/frontend/packages/dashboard/src/flow.html
+++ b/frontend/packages/dashboard/src/flow.html
@@ -29,10 +29,6 @@
◆ SYST3M ◆
-
- ◆ SCH3DUL3S ◆
-
-
diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs
index 956f9ae9..0ff15d86 100644
--- a/hive-c0re/src/dashboard.rs
+++ b/hive-c0re/src/dashboard.rs
@@ -72,7 +72,6 @@ pub async fn serve(port: u16, coord: Arc) -> Result<()> {
.route("/cancel-reminder/{id}", post(post_cancel_reminder))
.route("/retry-reminder/{id}", post(post_retry_reminder))
.route("/request-spawn", post(post_request_spawn))
- .route("/api/topology/set-parent", post(post_set_parent))
.route("/op-send", post(post_op_send))
.route("/meta-update", post(post_meta_update))
.route("/api/schedules", get(api_schedules).post(post_schedule_new))
@@ -850,21 +849,6 @@ struct RequestSpawnForm {
name: String,
}
-/// `POST /api/topology/set-parent` body. `child` is required.
-/// `new_parent` may be:
-/// - absent or empty / whitespace-only → promote to root,
-/// - non-empty → new parent's logical name.
-///
-/// (The CLI surface gates "no parent specified" behind an explicit
-/// `--root` flag for safety; the HTTP surface is permissive
-/// because the dashboard form encodes "no value" as the empty
-/// string for the optional radio-group input.)
-#[derive(Deserialize)]
-struct SetParentForm {
- child: String,
- new_parent: Option,
-}
-
#[derive(Deserialize)]
struct AnswerForm {
answer: String,
@@ -1862,45 +1846,6 @@ async fn post_request_spawn(
}
}
-/// `POST /api/topology/set-parent` — operator-driven parent move
-/// (#486). Form fields: `child` (required, agent name), `new_parent`
-/// (optional — empty / absent string ⇒ promote to root). Refuses
-/// cycles, unknown agents, and reparenting the manager. On success
-/// re-emits container snapshots so the dashboard tree repaints
-/// without a refresh.
-async fn post_set_parent(
- State(state): State,
- Form(form): Form,
-) -> Response {
- let child = form.child.trim().to_owned();
- if child.is_empty() {
- return error_response("set-parent: `child` required");
- }
- // Empty / whitespace-only `new_parent` ⇒ promote to root. Web
- // forms submit the empty string for a "no value" radio button,
- // so this is the ergonomic encoding.
- let new_parent = form
- .new_parent
- .as_deref()
- .map(str::trim)
- .filter(|s| !s.is_empty())
- .map(str::to_owned);
- match crate::topology::set_parent(&child, new_parent.as_deref()) {
- Ok(()) => {
- tracing::info!(
- child = %child,
- new_parent = ?new_parent,
- "operator: set-parent via dashboard"
- );
- // Topology drives ContainerView.parent; refresh the
- // snapshot so connected viewers see the new tree.
- state.coord.rescan_containers_and_emit().await;
- (StatusCode::OK, "ok").into_response()
- }
- Err(e) => error_response(&format!("set-parent {child} failed: {e}")),
- }
-}
-
async fn post_rebuild(State(state): State, AxumPath(name): AxumPath) -> Response {
let logical = strip_container_prefix(&name);
state.coord.rebuild_queue.enqueue(
diff --git a/hive-c0re/src/main.rs b/hive-c0re/src/main.rs
index 9b00cc91..3b9f0e77 100644
--- a/hive-c0re/src/main.rs
+++ b/hive-c0re/src/main.rs
@@ -102,23 +102,6 @@ enum Cmd {
Approve { id: i64 },
/// Deny a pending request by id.
Deny { id: i64 },
- /// Move an agent in the topology tree (#486). Set `--parent` to
- /// a new parent agent name; pass `--root` to promote the agent
- /// to root (no parent). Refuses cycles, unknown agents, and
- /// any attempt to reparent the manager.
- SetParent {
- child: String,
- /// New parent agent name. Mutually exclusive with `--root`.
- /// Exactly one of `--parent` / `--root` is required — clap
- /// rejects both-absent calls so a fat-fingered
- /// `hive-c0re set-parent alice` doesn't silently promote
- /// alice to root (argus flag on PR #492).
- #[arg(long, conflicts_with = "root", required_unless_present = "root")]
- parent: Option,
- /// Promote `child` to root (no parent).
- #[arg(long)]
- root: bool,
- },
}
#[tokio::main]
@@ -160,20 +143,6 @@ async fn main() -> Result<()> {
render(client::request(&cli.socket, HostRequest::Approve { id }).await?)
}
Cmd::Deny { id } => render(client::request(&cli.socket, HostRequest::Deny { id }).await?),
- Cmd::SetParent {
- child,
- parent,
- root,
- } => {
- let new_parent = if root { None } else { parent };
- render(
- client::request(
- &cli.socket,
- HostRequest::SetParent { child, new_parent },
- )
- .await?,
- )
- }
}
}
diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs
index 30be0364..42ccc5fb 100644
--- a/hive-c0re/src/server.rs
+++ b/hive-c0re/src/server.rs
@@ -185,17 +185,6 @@ async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse {
actions::deny(&coord, *id, None).await?;
HostResponse::success()
}
- HostRequest::SetParent { child, new_parent } => {
- tracing::info!(%child, ?new_parent, "set_parent");
- crate::topology::set_parent(child, new_parent.as_deref())
- .map_err(anyhow::Error::msg)?;
- // ContainerView.parent is read from topology.json — a
- // change here means every container row potentially
- // moves in the dashboard tree. Rescan + diff-emit so
- // open viewers repaint without polling.
- coord.rescan_containers_and_emit().await;
- HostResponse::success()
- }
})
}
.await;
diff --git a/hive-c0re/src/topology.rs b/hive-c0re/src/topology.rs
index 7830e9a9..527f2cdb 100644
--- a/hive-c0re/src/topology.rs
+++ b/hive-c0re/src/topology.rs
@@ -123,72 +123,6 @@ pub fn default_seed(agent_names: &[String]) -> BTreeMap>
out
}
-/// Pure validation + apply for [`set_parent`]. Splits off so tests
-/// can exercise the rules (cycle / unknown / manager-protect) on
-/// an in-memory `BTreeMap` without touching the on-disk
-/// `topology.json`. Returns either the post-move map (caller
-/// writes it back) or a user-readable error string.
-pub fn apply_set_parent(
- topo: &BTreeMap>,
- child: &str,
- new_parent: Option<&str>,
-) -> Result>, String> {
- if child == crate::lifecycle::MANAGER_NAME {
- return Err("cannot reparent the manager — it is structurally root".to_owned());
- }
- if !topo.contains_key(child) {
- return Err(format!("unknown agent: {child}"));
- }
- if let Some(p) = new_parent {
- if !topo.contains_key(p) {
- return Err(format!("unknown parent: {p}"));
- }
- if p == child {
- return Err("an agent cannot be its own parent".to_owned());
- }
- // Cycle check: walk `p`'s ancestors in the EXISTING map. If
- // we hit `child`, then making `child`'s parent = `p` would
- // close the loop (child → … → p → child).
- let mut cur = p.to_owned();
- for _ in 0..32 {
- if cur == child {
- return Err(format!(
- "cycle: {p} is in {child}'s subtree (would create a loop)"
- ));
- }
- let Some(next) = topo.get(&cur).cloned().flatten() else {
- break;
- };
- cur = next;
- }
- }
- let mut next = topo.clone();
- next.insert(child.to_owned(), new_parent.map(str::to_owned));
- Ok(next)
-}
-
-/// Operator-driven parent move (#486 / #487). Set `child`'s parent
-/// to `new_parent` (or `None` to promote to root). See
-/// [`apply_set_parent`] for the validation rules. The operator-set
-/// parent sticks across `reconcile()` calls (which preserves
-/// existing entries).
-///
-/// No bind-mount / container churn today — the hierarchy is
-/// currently logical-only (see #486 comment 5042). Once
-/// sub-manager bind mounts land alongside #361, the caller adds
-/// an umount-old / mount-new / restart-cascade step on top.
-pub fn set_parent(child: &str, new_parent: Option<&str>) -> Result<(), String> {
- let current = read();
- // Idempotent no-op fast path: skip the disk write when nothing
- // changes. apply_set_parent still runs to surface validation
- // errors (e.g. unknown child) so the caller gets a real signal.
- let next = apply_set_parent(¤t, child, new_parent)?;
- if next == current {
- return Ok(());
- }
- write(&next).map_err(|e| format!("write topology.json: {e}"))
-}
-
/// Reconcile `topology.json` against the current agent set. Adds an
/// entry (default: parent = manager, manager itself = root) for any
/// agent missing from the file; removes entries for agents no longer
@@ -257,85 +191,4 @@ mod tests {
let seed = default_seed(&[]);
assert!(seed.is_empty());
}
-
- fn topo_three_level() -> BTreeMap> {
- let mut m = BTreeMap::new();
- m.insert(crate::lifecycle::MANAGER_NAME.to_owned(), None);
- m.insert(
- "alice".to_owned(),
- Some(crate::lifecycle::MANAGER_NAME.to_owned()),
- );
- m.insert("bob".to_owned(), Some("alice".to_owned()));
- m.insert("carol".to_owned(), Some("alice".to_owned()));
- m
- }
-
- #[test]
- fn apply_set_parent_promotes_to_root() {
- let next = apply_set_parent(&topo_three_level(), "alice", None).unwrap();
- assert_eq!(next.get("alice"), Some(&None));
- }
-
- #[test]
- fn apply_set_parent_reparents_under_sibling_subtree() {
- // bob and carol both under alice; move carol under bob.
- let next = apply_set_parent(&topo_three_level(), "carol", Some("bob")).unwrap();
- assert_eq!(next.get("carol"), Some(&Some("bob".to_owned())));
- }
-
- #[test]
- fn apply_set_parent_refuses_manager_move() {
- let err =
- apply_set_parent(&topo_three_level(), crate::lifecycle::MANAGER_NAME, None).unwrap_err();
- assert!(err.contains("manager"), "err = {err}");
- }
-
- #[test]
- fn apply_set_parent_refuses_unknown_child() {
- let err = apply_set_parent(&topo_three_level(), "nobody", Some("alice")).unwrap_err();
- assert!(err.contains("unknown agent"), "err = {err}");
- }
-
- #[test]
- fn apply_set_parent_refuses_unknown_parent() {
- let err = apply_set_parent(&topo_three_level(), "bob", Some("nobody")).unwrap_err();
- assert!(err.contains("unknown parent"), "err = {err}");
- }
-
- #[test]
- fn apply_set_parent_refuses_self() {
- let err = apply_set_parent(&topo_three_level(), "alice", Some("alice")).unwrap_err();
- assert!(err.contains("own parent"), "err = {err}");
- }
-
- #[test]
- fn apply_set_parent_refuses_cycle() {
- // bob's parent is alice; trying to make alice's parent =
- // bob would close the loop alice → bob → alice.
- let err = apply_set_parent(&topo_three_level(), "alice", Some("bob")).unwrap_err();
- assert!(err.contains("cycle"), "err = {err}");
- }
-
- #[test]
- fn apply_set_parent_refuses_deep_cycle() {
- // Three-deep chain: manager → alice → bob → carol. Moving
- // alice under carol would create the loop alice → carol → bob → alice.
- let mut topo = BTreeMap::new();
- topo.insert(crate::lifecycle::MANAGER_NAME.to_owned(), None);
- topo.insert(
- "alice".to_owned(),
- Some(crate::lifecycle::MANAGER_NAME.to_owned()),
- );
- topo.insert("bob".to_owned(), Some("alice".to_owned()));
- topo.insert("carol".to_owned(), Some("bob".to_owned()));
- let err = apply_set_parent(&topo, "alice", Some("carol")).unwrap_err();
- assert!(err.contains("cycle"), "err = {err}");
- }
-
- #[test]
- fn apply_set_parent_is_idempotent_noop() {
- // bob is already under alice — same value returned.
- let next = apply_set_parent(&topo_three_level(), "bob", Some("alice")).unwrap();
- assert_eq!(next, topo_three_level());
- }
}
diff --git a/hive-forge/src/verbs/diff.rs b/hive-forge/src/verbs/diff.rs
index e35793b9..aa64c5cd 100644
--- a/hive-forge/src/verbs/diff.rs
+++ b/hive-forge/src/verbs/diff.rs
@@ -1,15 +1,4 @@
//! `diff [repo]` — print the unified diff for a PR.
-//!
-//! By default the hunks for known autogenerated lockfiles
-//! (`flake.lock`, `Cargo.lock`, `package-lock.json`, …) are
-//! collapsed to a `[: +N -M (autogenerated; pass --full for
-//! content)]` placeholder so a `flake.lock` rev bump doesn't drown
-//! the human-authored changes in 5 000 lines of lock churn
-//! (#222). The header (`diff --git`, `index`, `---`, `+++`,
-//! rename / mode metadata) is preserved so the reader can still
-//! see WHICH lockfiles changed; the +/- counts give a `diff
-//! --stat`-style magnitude (excluding the `@@` hunk header).
-//! Pass `--full` to dump the unfiltered diff.
use anyhow::Result;
use clap::Args as ClapArgs;
@@ -20,394 +9,11 @@ use crate::client::Client;
pub struct Args {
/// PR number.
number: u64,
- /// Print the unfiltered diff including autogenerated-file
- /// hunks (`flake.lock`, `Cargo.lock`, etc.). Default is to
- /// collapse those hunks to a placeholder so the human-authored
- /// changes aren't drowned in lock churn.
- #[arg(long)]
- full: bool,
}
pub fn run(client: &Client, args: Args) -> Result<()> {
let repo = client.repo();
let diff = client.get_text(&format!("/repos/{repo}/pulls/{}.diff", args.number), "text/plain")?;
- let out = if args.full {
- diff
- } else {
- collapse_autogenerated(&diff)
- };
- print!("{out}");
+ print!("{diff}");
Ok(())
}
-
-/// Walk a unified diff line-by-line. For each per-file section
-/// whose target path matches a known autogenerated file
-/// (`is_autogenerated`), drop every line from the first hunk
-/// header (`@@`) onward and emit a single `diff --stat`-style
-/// `[file.lock: +N -M (autogenerated, --full for content)]`
-/// placeholder before the next file. Non-autogenerated files
-/// pass through unchanged.
-fn collapse_autogenerated(diff: &str) -> String {
- let mut out = String::with_capacity(diff.len());
- let mut state: Option = None;
-
- for line in diff.lines() {
- if let Some(rest) = line.strip_prefix("diff --git ") {
- // New file section — flush prior collapse counter.
- if let Some(s) = state.take() {
- out.push_str(&s.placeholder());
- }
- let path = parse_diff_git_path(rest);
- if let Some(p) = path.as_deref()
- && is_autogenerated(p)
- {
- state = Some(CollapseState::new(p.to_owned()));
- }
- out.push_str(line);
- out.push('\n');
- continue;
- }
- if let Some(s) = state.as_mut() {
- // First `@@` marks the boundary between file header and
- // hunk content; everything from this point is
- // suppressed (and tallied) while `state` is Some.
- if !s.in_body && line.starts_with("@@") {
- s.in_body = true;
- }
- if s.in_body {
- // The hunk header `@@` itself counts as a body line
- // for the +/− tally only via its descendant content
- // lines; skip it for the counters.
- if !line.starts_with("@@") {
- match line.as_bytes().first() {
- Some(b'+') => s.added += 1,
- Some(b'-') => s.removed += 1,
- _ => {}
- }
- }
- continue;
- }
- }
- out.push_str(line);
- out.push('\n');
- }
- if let Some(s) = state.take() {
- out.push_str(&s.placeholder());
- }
- out
-}
-
-/// Per-file accumulator for the collapsed-hunk placeholder. Tracks
-/// the file's display name plus `+`/`−` line counts (excluding the
-/// `@@` hunk header), so the placeholder shows operator-meaningful
-/// magnitude instead of "N lines omitted" (which mixed context +
-/// added + removed indistinguishably).
-struct CollapseState {
- path: String,
- in_body: bool,
- added: u32,
- removed: u32,
-}
-
-impl CollapseState {
- fn new(path: String) -> Self {
- Self {
- path,
- in_body: false,
- added: 0,
- removed: 0,
- }
- }
-
- fn placeholder(&self) -> String {
- format!(
- "[{}: +{} -{} (autogenerated; pass --full for content)]\n",
- self.path, self.added, self.removed
- )
- }
-}
-
-/// `rest` is everything after `diff --git `, e.g. `a/foo b/foo`
-/// or `"a/path with space" "b/path with space"`. Return the
-/// post-rename (`b/`-side) path so renames report the new path.
-///
-/// Git uses C-style quoting (`\"`, `\\`, octal escapes) for paths
-/// with spaces or unusual bytes — we don't unescape because we
-/// only need the bytewise file name for the lockfile allowlist
-/// match. We do parse the quoted-vs-unquoted form correctly so a
-/// whitespace-containing path doesn't break `split_whitespace`.
-fn parse_diff_git_path(rest: &str) -> Option {
- if let Some(after_open) = rest.strip_prefix('"') {
- // Quoted form: `"a/" "b/"`. Find the closing
- // quote of the a-side (skipping `\"` escapes so a path
- // containing `"` doesn't terminate early).
- let mut iter = after_open.char_indices();
- let mut a_close = None;
- loop {
- let Some((i, c)) = iter.next() else { break };
- if c == '\\' {
- // Skip the next char — it's part of the escape.
- iter.next();
- continue;
- }
- if c == '"' {
- a_close = Some(i);
- break;
- }
- }
- let a_close = a_close?;
- // After the a-side closing quote, we expect `" "b/...`.
- // `after_open` skipped the leading `"`, so the b-side
- // starts in `after_open[a_close + 1..]` — strip leading
- // space + opening quote + `b/`, then strip trailing `"`.
- let after_a = after_open.get(a_close + 1..)?;
- let after_a = after_a.strip_prefix(' ')?;
- let b_inside = after_a.strip_prefix('"')?;
- // Find b-side's closing quote with the same escape rule.
- let mut iter = b_inside.char_indices();
- let mut b_close = None;
- loop {
- let Some((i, c)) = iter.next() else { break };
- if c == '\\' {
- iter.next();
- continue;
- }
- if c == '"' {
- b_close = Some(i);
- break;
- }
- }
- let b_close = b_close?;
- let b_path = b_inside.get(..b_close)?;
- Some(b_path.strip_prefix("b/").unwrap_or(b_path).to_owned())
- } else {
- // Unquoted: paths have no whitespace, so `split_whitespace`
- // gives exactly two tokens.
- let token = rest.split_whitespace().nth(1)?;
- Some(token.strip_prefix("b/").unwrap_or(token).to_owned())
- }
-}
-
-/// File-name match against a small whitelist of well-known
-/// machine-generated lockfiles. We deliberately don't pattern-match
-/// extensions like `*.lock` because some real human-authored files
-/// use that suffix (e.g. `keep.lock` config markers); explicit
-/// listing avoids surprising the user.
-fn is_autogenerated(path: &str) -> bool {
- let name = path.rsplit('/').next().unwrap_or(path);
- matches!(
- name,
- "flake.lock"
- | "Cargo.lock"
- | "package-lock.json"
- | "pnpm-lock.yaml"
- | "yarn.lock"
- | "Gemfile.lock"
- | "poetry.lock"
- | "Pipfile.lock"
- | "composer.lock"
- | "go.sum"
- )
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn detects_known_lockfiles() {
- assert!(is_autogenerated("flake.lock"));
- assert!(is_autogenerated("a/flake.lock"));
- assert!(is_autogenerated("hive-c0re/Cargo.lock"));
- assert!(is_autogenerated("frontend/packages/dashboard/package-lock.json"));
- assert!(!is_autogenerated("src/main.rs"));
- assert!(!is_autogenerated("Cargo.toml"));
- // Suffix-only files we deliberately don't match — keep
- // `is_autogenerated` allowlist-driven, not pattern-driven.
- assert!(!is_autogenerated("config/keep.lock"));
- }
-
- #[test]
- fn collapse_replaces_lockfile_hunks_with_placeholder() {
- let diff = "\
-diff --git a/Cargo.lock b/Cargo.lock
-index 1111..2222 100644
---- a/Cargo.lock
-+++ b/Cargo.lock
-@@ -1,3 +1,3 @@
- [[package]]
--name = \"old\"
-+name = \"new\"
-diff --git a/src/main.rs b/src/main.rs
-index 3333..4444 100644
---- a/src/main.rs
-+++ b/src/main.rs
-@@ -1,2 +1,2 @@
--fn main() {}
-+fn main() { println!(\"hi\"); }
-";
- let out = collapse_autogenerated(diff);
- assert!(out.contains("diff --git a/Cargo.lock"));
- assert!(out.contains("--- a/Cargo.lock"));
- assert!(out.contains("+++ b/Cargo.lock"));
- assert!(!out.contains("[[package]]"));
- // Stat-style placeholder: 1 added (`+name = "new"`), 1
- // removed (`-name = "old"`); context line and `@@` header
- // don't count.
- assert!(
- out.contains("[Cargo.lock: +1 -1"),
- "expected stat placeholder, got: {out}"
- );
- // Non-lockfile file passes through untouched.
- assert!(out.contains("fn main() {}"));
- assert!(out.contains("println!(\"hi\")"));
- }
-
- #[test]
- fn collapse_passes_normal_diff_through() {
- let diff = "\
-diff --git a/src/lib.rs b/src/lib.rs
-index aaaa..bbbb 100644
---- a/src/lib.rs
-+++ b/src/lib.rs
-@@ -1,1 +1,1 @@
--fn old() {}
-+fn new() {}
-";
- let out = collapse_autogenerated(diff);
- assert_eq!(out, diff);
- }
-
- #[test]
- fn collapse_handles_trailing_lockfile() {
- // Lockfile is the LAST file in the diff — exercises the
- // post-loop flush path.
- let diff = "\
-diff --git a/README.md b/README.md
-index aaaa..bbbb 100644
---- a/README.md
-+++ b/README.md
-@@ -1,1 +1,1 @@
--old
-+new
-diff --git a/flake.lock b/flake.lock
-index 1111..2222 100644
---- a/flake.lock
-+++ b/flake.lock
-@@ -1,2 +1,2 @@
- lock-line-one
--lock-line-two
-+lock-line-two-bumped
-";
- let out = collapse_autogenerated(diff);
- assert!(out.contains("-old"));
- assert!(out.contains("+new"));
- assert!(out.contains("diff --git a/flake.lock"));
- assert!(!out.contains("lock-line-one"));
- assert!(
- out.contains("[flake.lock: +1 -1"),
- "expected stat placeholder, got: {out}"
- );
- }
-
- #[test]
- fn collapse_counts_distinguish_added_and_removed() {
- // Big asymmetric churn — flake.lock rev bump with several
- // adds and one removal in the worker fields. Verifies
- // we don't conflate `+`/`-` totals.
- let diff = "\
-diff --git a/flake.lock b/flake.lock
-index 1111..2222 100644
---- a/flake.lock
-+++ b/flake.lock
-@@ -1,5 +1,8 @@
- {
-- \"old\": 1
-+ \"new\": 1,
-+ \"another\": 2,
-+ \"and\": 3,
-+ \"more\": 4
- }
-";
- let out = collapse_autogenerated(diff);
- assert!(
- out.contains("[flake.lock: +4 -1"),
- "expected +4 -1, got: {out}"
- );
- }
-
- #[test]
- fn parse_diff_git_path_picks_b_side() {
- assert_eq!(
- parse_diff_git_path("a/foo b/foo"),
- Some("foo".to_owned())
- );
- assert_eq!(
- parse_diff_git_path("a/old.txt b/new.txt"),
- Some("new.txt".to_owned())
- );
- assert_eq!(
- parse_diff_git_path("a/dir/Cargo.lock b/dir/Cargo.lock"),
- Some("dir/Cargo.lock".to_owned())
- );
- }
-
- #[test]
- fn parse_diff_git_path_handles_quoted_paths_with_spaces() {
- // Git quotes paths with spaces / unusual chars per
- // `core.quotePath`. The b-side path must survive intact.
- assert_eq!(
- parse_diff_git_path("\"a/foo bar\" \"b/foo bar\""),
- Some("foo bar".to_owned())
- );
- // Rename with quoted sides.
- assert_eq!(
- parse_diff_git_path("\"a/old name\" \"b/new name\""),
- Some("new name".to_owned())
- );
- // Lockfile inside a directory whose name has a space.
- assert_eq!(
- parse_diff_git_path("\"a/dir with space/Cargo.lock\" \"b/dir with space/Cargo.lock\""),
- Some("dir with space/Cargo.lock".to_owned())
- );
- }
-
- #[test]
- fn parse_diff_git_path_handles_escaped_quote_inside_path() {
- // Git escapes embedded `"` as `\"`. The closing-quote
- // search must skip these so it doesn't terminate early.
- // Path is literally `a/has"quote` / `b/has"quote`.
- let rest = r#""a/has\"quote" "b/has\"quote""#;
- assert_eq!(
- parse_diff_git_path(rest),
- // Bytewise: backslash + quote stay in the result
- // because we don't unescape (allowlist match is by
- // file name, never contains escapes).
- Some(r#"has\"quote"#.to_owned())
- );
- }
-
- #[test]
- fn collapse_recognises_lockfile_in_quoted_path() {
- // Path "a/odd dir/Cargo.lock" forces git to use the
- // quoted form. We must still detect it as a lockfile.
- let diff = "\
-diff --git \"a/odd dir/Cargo.lock\" \"b/odd dir/Cargo.lock\"
-index 1111..2222 100644
---- \"a/odd dir/Cargo.lock\"
-+++ \"b/odd dir/Cargo.lock\"
-@@ -1,1 +1,1 @@
--old
-+new
-";
- let out = collapse_autogenerated(diff);
- // Content must be suppressed (lockfile detected).
- assert!(!out.contains("old\n"), "lock content leaked: {out}");
- assert!(!out.contains("+new"), "lock content leaked: {out}");
- // Placeholder uses the parsed b-side path (without quotes).
- assert!(
- out.contains("[odd dir/Cargo.lock: +1 -1"),
- "expected stat placeholder for quoted-path lockfile, got: {out}"
- );
- }
-}
diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs
index 684a9496..e5411178 100644
--- a/hive-sh4re/src/lib.rs
+++ b/hive-sh4re/src/lib.rs
@@ -46,15 +46,6 @@ pub enum HostRequest {
Approve { id: i64 },
/// Deny a pending request by id.
Deny { id: i64 },
- /// Move an agent in the topology tree (#486). Pass `new_parent =
- /// None` to promote the agent to root, or `Some(name)` to set a
- /// new parent. Refuses cycles, unknown agents, and any attempt
- /// to reparent the manager (which is structurally root).
- /// Pure topology-json edit today; bind-mount work follows in #361.
- SetParent {
- child: String,
- new_parent: Option,
- },
}
#[derive(Debug, Clone, Serialize, Deserialize)]