CLI green. **Layer 4 — the pump fires due rotations.** Add the path + firing step:


Edit

    /// The pump's last-tick heartbeat (M8 decision 23, REQ-DAEMON-5):
    /// epoch-ms written while the loop is alive, rendered by
    /// `spt daemon status` / the `subnet status` stall warning
    /// ([`crate::endpoint::pump_heartbeat_path`]).
    pub heartbeat: PathBuf,
}

    /// The pump's last-tick heartbeat (M8 decision 23, REQ-DAEMON-5):
    /// epoch-ms written while the loop is alive, rendered by
    /// `spt daemon status` / the `subnet status` stall warning
    /// ([`crate::endpoint::pump_heartbeat_path`]).
    pub heartbeat: PathBuf,
    /// `rotation-pending.json` (Mesh-D7, REQ-MESH-4): the timeboxed seed-rotation
    /// schedule a `subnet revoke` writes; the pump fires due entries each
    /// registry tick ([`spt_store::rotation::RotationPending`]).
    pub rotations: PathBuf,
}


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\peerloop.rs has been updated successfully. (file state is current in your context — no need to Read it back)

    /// The pump's last-tick heartbeat (M8 decision 23, REQ-DAEMON-5):
    /// epoch-ms written while the loop is alive, rendered by
    /// `spt daemon status` / the `subnet status` stall warning
    /// ([`crate::endpoint::pump_heartbeat_path`]).
    pub heartbeat: PathBuf,
}

    /// The pump's last-tick heartbeat (M8 decision 23, REQ-DAEMON-5):
    /// epoch-ms written while the loop is alive, rendered by
    /// `spt daemon status` / the `subnet status` stall warning
    /// ([`crate::endpoint::pump_heartbeat_path`]).
    pub heartbeat: PathBuf,
    /// `rotation-pending.json` (Mesh-D7, REQ-MESH-4): the timeboxed seed-rotation
    /// schedule a `subnet revoke` writes; the pump fires due entries each
    /// registry tick ([`spt_store::rotation::RotationPending`]).
    pub rotations: PathBuf,
}

+    /// `rotation-pending.json` (Mesh-D7, REQ-MESH-4): the timeboxed seed-rotation

+    /// schedule a `subnet revoke` writes; the pump fires due entries each

+    /// registry tick ([`spt_store::rotation::RotationPending`]).


            peer_addrs: identity.join("peer-addrs.json"),
            heartbeat: crate::endpoint::pump_heartbeat_path(),
        }
    }
}

            peer_addrs: identity.join("peer-addrs.json"),
            heartbeat: crate::endpoint::pump_heartbeat_path(),
            rotations: identity.join("rotation-pending.json"),
        }
    }
}

/// Fire any seed rotations whose coalescing window has closed (Mesh-D7,
/// REQ-MESH-4). For each due subnet: rotate the seed (bump epoch, retain the
/// prior generation one-deep for the re-seed grace) and clear its pending entry.
/// The new seed reaches benign offliners over the member control stream's
/// re-seed leg on their next connect — never over gossip; the revokee's
/// already-propagated tombstone denies it the grace. Pure-store work (no conn
/// handle needed): the daemon re-reads `subnet.json` per connect, so the bump
/// takes effect without a restart.
// [impl->REQ-MESH-4]
fn fire_due_rotations(subnets_path: &std::path::Path, rotations_path: &std::path::Path, now_ms: u64) {
    let mut pending = spt_store::rotation::RotationPending::load_from(rotations_path);
    let due = pending.due_subnets(now_ms);
    if due.is_empty() {
        return;
    }
    let mut subnets = SubnetStore::load_from(subnets_path);
    let mut rotated = false;
    for sub in &due {
        match subnets.rotate_seed(sub) {
            Ok(rec) => {
                eprintln!("SEED_ROTATED:{sub}:epoch={} (revoke window closed)", rec.epoch);
                rotated = true;
            }
            // The subnet may have been left/renamed since the revoke — drop the
            // stale entry rather than retrying it forever.
            Err(e) => eprintln!("SEED_ROTATE_SKIP:{sub}: {e}"),
        }
        pending.clear(sub);
    }
    if rotated {
        if let Err(e) = subnets.save_to(subnets_path) {
            eprintln!("SEED_ROTATE_FAIL: {e}");
            return; // keep the pending entry — retry next tick rather than lose it
        }
    }
    let _ = pending.save_to(rotations_path);
}


            peer_addrs: identity.join("peer-addrs.json"),
            heartbeat: crate::endpoint::pump_heartbeat_path(),
        }
    }
}

            peer_addrs: identity.join("peer-addrs.json"),
            heartbeat: crate::endpoint::pump_heartbeat_path(),
            rotations: identity.join("rotation-pending.json"),
        }
    }
}

/// Fire any seed rotations whose coalescing window has closed (Mesh-D7,
/// REQ-MESH-4). For each due subnet: rotate the seed (bump epoch, retain the
/// prior generation one-deep for the re-seed grace) and clear its pending entry.
/// The new seed reaches benign offliners over the member control stream's
/// re-seed leg on their next connect — never over gossip; the revokee's
/// already-propagated tombstone denies it the grace. Pure-store work (no conn
/// handle needed): the daemon re-reads `subnet.json` per connect, so the bump
/// takes effect without a restart.
// [impl->REQ-MESH-4]
fn fire_due_rotations(subnets_path: &std::path::Path, rotations_path: &std::path::Path, now_ms: u64) {
    let mut pending = spt_store::rotation::RotationPending::load_from(rotations_path);
    let due = pending.due_subnets(now_ms);
    if due.is_empty() {
        return;
    }
    let mut subnets = SubnetStore::load_from(subnets_path);
    let mut rotated = false;
    for sub in &due {
        match subnets.rotate_seed(sub) {
            Ok(rec) => {
                eprintln!("SEED_ROTATED:{sub}:epoch={} (revoke window closed)", rec.epoch);
                rotated = true;
            }
            // The subnet may have been left/renamed since the revoke — drop the
            // stale entry rather than retrying it forever.
            Err(e) => eprintln!("SEED_ROTATE_SKIP:{sub}: {e}"),
        }
        pending.clear(sub);
    }
    if rotated {
        if let Err(e) = subnets.save_to(subnets_path) {
            eprintln!("SEED_ROTATE_FAIL: {e}");
            return; // keep the pending entry — retry next tick rather than lose it
        }
    }
    let _ = pending.save_to(rotations_path);
}

+            rotations: identity.join("rotation-pending.json"),

+/// Fire any seed rotations whose coalescing window has closed (Mesh-D7,

+/// REQ-MESH-4). For each due subnet: rotate the seed (bump epoch, retain the

+/// prior generation one-deep for the re-seed grace) and clear its pending entry.

+/// The new seed reaches benign offliners over the member control stream's

+/// re-seed leg on their next connect — never over gossip; the revokee's

+/// already-propagated tombstone denies it the grace. Pure-store work (no conn

+/// handle needed): the daemon re-reads `subnet.json` per connect, so the bump

+fn fire_due_rotations(subnets_path: &std::path::Path, rotations_path: &std::path::Path, now_ms: u64) {

+    let mut pending = spt_store::rotation::RotationPending::load_from(rotations_path);

+    let due = pending.due_subnets(now_ms);

+    let mut subnets = SubnetStore::load_from(subnets_path);

+        match subnets.rotate_seed(sub) {

+                eprintln!("SEED_ROTATED:{sub}:epoch={} (revoke window closed)", rec.epoch);

+            // The subnet may have been left/renamed since the revoke — drop the

+            // stale entry rather than retrying it forever.

+            Err(e) => eprintln!("SEED_ROTATE_SKIP:{sub}: {e}"),

+        if let Err(e) = subnets.save_to(subnets_path) {

+            eprintln!("SEED_ROTATE_FAIL: {e}");

+            return; // keep the pending entry — retry next tick rather than lose it

+    let _ = pending.save_to(rotations_path);

/// Pump cadences (config-driven in production: [`DaemonConfig`]).


Now call it in the registry tick, after the eviction sweep:


            registry.evict_silent_peers(cadence.registry_evict_after);
            registry.advertise_local(&paths.owlery)

            registry.evict_silent_peers(cadence.registry_evict_after);
            // Mesh-D7 (REQ-MESH-4): fire any seed rotation whose revoke window
            // has closed — after the eviction sweep, before re-advertising. The
            // re-minted seed rides the member control stream on the next connect;
            // the revokee's tombstone (already propagated) keeps it locked out.
            fire_due_rotations(&paths.subnets, &paths.rotations, now_ms());
            registry.advertise_local(&paths.owlery)


            registry.evict_silent_peers(cadence.registry_evict_after);
            registry.advertise_local(&paths.owlery)

            registry.evict_silent_peers(cadence.registry_evict_after);
            // Mesh-D7 (REQ-MESH-4): fire any seed rotation whose revoke window
            // has closed — after the eviction sweep, before re-advertising. The
            // re-minted seed rides the member control stream on the next connect;
            // the revokee's tombstone (already propagated) keeps it locked out.
            fire_due_rotations(&paths.subnets, &paths.rotations, now_ms());
            registry.advertise_local(&paths.owlery)

// heard entry with every admitted feed and is never touched.

+            // Mesh-D7 (REQ-MESH-4): fire any seed rotation whose revoke window

+            // has closed — after the eviction sweep, before re-advertising. The

+            // re-minted seed rides the member control stream on the next connect;

+            // the revokee's tombstone (already propagated) keeps it locked out.

+            fire_due_rotations(&paths.subnets, &paths.rotations, now_ms());


Check for other `PumpPaths` constructors (tests/E2E) needing the new field:


Found 4 files
crates\spt-daemon\src\peerloop.rs
crates\spt-daemon\tests\mesh.rs
crates\spt-daemon\tests\twohost.rs
crates\spt-daemon\tests\peerloop.rs


crates\spt-daemon\tests\mesh.rs:182:            heartbeat: root.join("pump-heartbeat.json"),
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\mesh.rs-183-        };
--
crates\spt-daemon\tests\mesh.rs:362:        heartbeat: p.heartbeat.clone(),
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\mesh.rs-363-    }
--
crates\spt-daemon\tests\peerloop.rs:216:        heartbeat: a_root.join("pump-heartbeat.json"),
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-217-    };
--
crates\spt-daemon\tests\peerloop.rs:344:            heartbeat: a_paths.heartbeat.clone(),
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs-345-        };
--
crates\spt-daemon\tests\twohost.rs:289:        heartbeat: home.join("identity").join("pump-heartbeat.json"),
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs-290-    }


Five test constructors need the field. Let me view and patch each:


165	            ctx_root: root.join("context"),
166	            notif_db: root.join("notifs.db"),
167	            scratch: root.join("scratch"),
168	            subnets: root.join("subnet.json"),
169	            vis: root.join("visibility.json"),
170	            ops: root.join("pump-ops.json"),
171	            releases: root.join("releases"),
172	            release_keys: root.join("release-keys.json"),
173	            pull_marker: root.join("sync-pull-now"),
174	            advertise_marker: root.join("advertise-now"),
175	            // Address seeding DISABLED: `reach` (the live resolver) is the sole
176	            // reachability source so an "offline" node (removed from `reach`) is
177	            // truly undialable — a persisted seed would otherwise let the pump
178	            // re-dial its still-running broker and wedge on a node modelled as
179	            // gone. peer_addrs points under a regular FILE so create_dir_all
180	            // fails and record() no-ops; load_from then always reads empty.
181	            peer_addrs: root.join("noseed").join("peer-addrs.json"),
182	            heartbeat: root.join("pump-heartbeat.json"),
183	        };
184	        std::fs::create_dir_all(root).unwrap();

            ctx_root: root.join("context"),
            notif_db: root.join("notifs.db"),
            scratch: root.join("scratch"),
            subnets: root.join("subnet.json"),
            vis: root.join("visibility.json"),
            ops: root.join("pump-ops.json"),
            releases: root.join("releases"),
            release_keys: root.join("release-keys.json"),
            pull_marker: root.join("sync-pull-now"),
            advertise_marker: root.join("advertise-now"),
            // Address seeding DISABLED: `reach` (the live resolver) is the sole
            // reachability source so an "offline" node (removed from `reach`) is
            // truly undialable — a persisted seed would otherwise let the pump
            // re-dial its still-running broker and wedge on a node modelled as
            // gone. peer_addrs points under a regular FILE so create_dir_all
            // fails and record() no-ops; load_from then always reads empty.
            peer_addrs: root.join("noseed").join("peer-addrs.json"),
            heartbeat: root.join("pump-heartbeat.json"),
        };
        std::fs::create_dir_all(root).unwrap();


            peer_addrs: root.join("noseed").join("peer-addrs.json"),
            heartbeat: root.join("pump-heartbeat.json"),
        };
        std::fs::create_dir_all(root).unwrap();

            peer_addrs: root.join("noseed").join("peer-addrs.json"),
            heartbeat: root.join("pump-heartbeat.json"),
            rotations: root.join("rotation-pending.json"),
        };
        std::fs::create_dir_all(root).unwrap();


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\mesh.rs has been updated successfully. (file state is current in your context — no need to Read it back)

            peer_addrs: root.join("noseed").join("peer-addrs.json"),
            heartbeat: root.join("pump-heartbeat.json"),
        };
        std::fs::create_dir_all(root).unwrap();

            peer_addrs: root.join("noseed").join("peer-addrs.json"),
            heartbeat: root.join("pump-heartbeat.json"),
            rotations: root.join("rotation-pending.json"),
        };
        std::fs::create_dir_all(root).unwrap();

// fails and record() no-ops; load_from then always reads empty.

+            rotations: root.join("rotation-pending.json"),

std::fs::write(root.join("noseed"), b"").unwrap(); // the seed blocker (a file)


345	        .remove(&node.hex);
346	}
347
348	/// Field-by-field copy — `PumpPaths` is not `Clone` (mirrors `peerloop.rs`).
349	fn clone_paths(p: &PumpPaths) -> PumpPaths {
350	    PumpPaths {
351	        owlery: p.owlery.clone(),
352	        ctx_root: p.ctx_root.clone(),
353	        notif_db: p.notif_db.clone(),
354	        scratch: p.scratch.clone(),
355	        subnets: p.subnets.clone(),
356	        vis: p.vis.clone(),
357	        ops: p.ops.clone(),
358	        releases: p.releases.clone(),
359	        release_keys: p.release_keys.clone(),
360	        pull_marker: p.pull_marker.clone(),
361	        advertise_marker: p.advertise_marker.clone(),
362	        peer_addrs: p.peer_addrs.clone(),
363	        heartbeat: p.heartbeat.clone(),
364	    }

        .remove(&node.hex);
}

/// Field-by-field copy — `PumpPaths` is not `Clone` (mirrors `peerloop.rs`).
fn clone_paths(p: &PumpPaths) -> PumpPaths {
    PumpPaths {
        owlery: p.owlery.clone(),
        ctx_root: p.ctx_root.clone(),
        notif_db: p.notif_db.clone(),
        scratch: p.scratch.clone(),
        subnets: p.subnets.clone(),
        vis: p.vis.clone(),
        ops: p.ops.clone(),
        releases: p.releases.clone(),
        release_keys: p.release_keys.clone(),
        pull_marker: p.pull_marker.clone(),
        advertise_marker: p.advertise_marker.clone(),
        peer_addrs: p.peer_addrs.clone(),
        heartbeat: p.heartbeat.clone(),
    }


        peer_addrs: p.peer_addrs.clone(),
        heartbeat: p.heartbeat.clone(),
    }

        peer_addrs: p.peer_addrs.clone(),
        heartbeat: p.heartbeat.clone(),
        rotations: p.rotations.clone(),
    }


        peer_addrs: p.peer_addrs.clone(),
        heartbeat: p.heartbeat.clone(),
    }

        peer_addrs: p.peer_addrs.clone(),
        heartbeat: p.heartbeat.clone(),
        rotations: p.rotations.clone(),
    }

+        rotations: p.rotations.clone(),


203	    let a_paths = PumpPaths {
204	        owlery: a_root.join("owlery"),
205	        ctx_root: a_root.join("context"),
206	        notif_db: a_root.join("notifs.db"),
207	        scratch: a_root.join("scratch"),
208	        subnets: a_root.join("subnet.json"),
209	        vis: a_root.join("visibility.json"),
210	        ops: a_root.join("pump-ops.json"),
211	        releases: a_root.join("releases"),
212	        release_keys: a_root.join("release-keys.json"),
213	        pull_marker: a_root.join("sync-pull-now"),
214	        advertise_marker: a_root.join("advertise-now"),
215	        peer_addrs: a_root.join("peer-addrs.json"),
216	        heartbeat: a_root.join("pump-heartbeat.json"),
217	    };
218	    // A trusts B's release signer — the update leg's production gate.
219	    std::fs::create_dir_all(&a_root).unwrap();
220	    let keys_json = serde_json::json!({

    let a_paths = PumpPaths {
        owlery: a_root.join("owlery"),
        ctx_root: a_root.join("context"),
        notif_db: a_root.join("notifs.db"),
        scratch: a_root.join("scratch"),
        subnets: a_root.join("subnet.json"),
        vis: a_root.join("visibility.json"),
        ops: a_root.join("pump-ops.json"),
        releases: a_root.join("releases"),
        release_keys: a_root.join("release-keys.json"),
        pull_marker: a_root.join("sync-pull-now"),
        advertise_marker: a_root.join("advertise-now"),
        peer_addrs: a_root.join("peer-addrs.json"),
        heartbeat: a_root.join("pump-heartbeat.json"),
    };
    // A trusts B's release signer — the update leg's production gate.
    std::fs::create_dir_all(&a_root).unwrap();
    let keys_json = serde_json::json!({


        peer_addrs: a_root.join("peer-addrs.json"),
        heartbeat: a_root.join("pump-heartbeat.json"),
    };
    // A trusts B's release signer — the update leg's production gate.

        peer_addrs: a_root.join("peer-addrs.json"),
        heartbeat: a_root.join("pump-heartbeat.json"),
        rotations: a_root.join("rotation-pending.json"),
    };
    // A trusts B's release signer — the update leg's production gate.


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\peerloop.rs has been updated successfully. (file state is current in your context — no need to Read it back)

        peer_addrs: a_root.join("peer-addrs.json"),
        heartbeat: a_root.join("pump-heartbeat.json"),
    };
    // A trusts B's release signer — the update leg's production gate.

        peer_addrs: a_root.join("peer-addrs.json"),
        heartbeat: a_root.join("pump-heartbeat.json"),
        rotations: a_root.join("rotation-pending.json"),
    };
    // A trusts B's release signer — the update leg's production gate.

+        rotations: a_root.join("rotation-pending.json"),


330	        let reg = Arc::clone(&a_registry);
331	        let flag = Arc::clone(&stop);
332	        let paths = PumpPaths {
333	            owlery: a_paths.owlery.clone(),
334	            ctx_root: a_paths.ctx_root.clone(),
335	            notif_db: a_paths.notif_db.clone(),
336	            scratch: a_paths.scratch.clone(),
337	            subnets: a_paths.subnets.clone(),
338	            vis: a_paths.vis.clone(),
339	            ops: a_paths.ops.clone(),
340	            releases: a_paths.releases.clone(),
341	            release_keys: a_paths.release_keys.clone(),
342	            pull_marker: a_paths.pull_marker.clone(),
343	            advertise_marker: a_paths.advertise_marker.clone(),
344	            peer_addrs: a_paths.peer_addrs.clone(),
345	            heartbeat: a_paths.heartbeat.clone(),
346	        };
347	        thread::spawn(move || {

        let reg = Arc::clone(&a_registry);
        let flag = Arc::clone(&stop);
        let paths = PumpPaths {
            owlery: a_paths.owlery.clone(),
            ctx_root: a_paths.ctx_root.clone(),
            notif_db: a_paths.notif_db.clone(),
            scratch: a_paths.scratch.clone(),
            subnets: a_paths.subnets.clone(),
            vis: a_paths.vis.clone(),
            ops: a_paths.ops.clone(),
            releases: a_paths.releases.clone(),
            release_keys: a_paths.release_keys.clone(),
            pull_marker: a_paths.pull_marker.clone(),
            advertise_marker: a_paths.advertise_marker.clone(),
            peer_addrs: a_paths.peer_addrs.clone(),
            heartbeat: a_paths.heartbeat.clone(),
        };
        thread::spawn(move || {


            peer_addrs: a_paths.peer_addrs.clone(),
            heartbeat: a_paths.heartbeat.clone(),
        };
        thread::spawn(move || {

            peer_addrs: a_paths.peer_addrs.clone(),
            heartbeat: a_paths.heartbeat.clone(),
            rotations: a_paths.rotations.clone(),
        };
        thread::spawn(move || {


            peer_addrs: a_paths.peer_addrs.clone(),
            heartbeat: a_paths.heartbeat.clone(),
        };
        thread::spawn(move || {

            peer_addrs: a_paths.peer_addrs.clone(),
            heartbeat: a_paths.heartbeat.clone(),
            rotations: a_paths.rotations.clone(),
        };
        thread::spawn(move || {

+            rotations: a_paths.rotations.clone(),


276	    PumpPaths {
277	        owlery: perch::owlery_dir(),
278	        ctx_root: perch::tracked_dir(),
279	        notif_db: perch::notifs_file(),
280	        scratch,
281	        subnets: perch::subnet_file(),
282	        vis: perch::visibility_file(),
283	        ops: home.join("pump-ops.json"),
284	        releases: home.join("releases"),
285	        release_keys: home.join("release-keys.json"),
286	        pull_marker: home.join("identity").join("sync-pull-now"),
287	        advertise_marker: home.join("identity").join("advertise-now"),
288	        peer_addrs: home.join("identity").join("peer-addrs.json"),
289	        heartbeat: home.join("identity").join("pump-heartbeat.json"),
290	    }
291	}

    PumpPaths {
        owlery: perch::owlery_dir(),
        ctx_root: perch::tracked_dir(),
        notif_db: perch::notifs_file(),
        scratch,
        subnets: perch::subnet_file(),
        vis: perch::visibility_file(),
        ops: home.join("pump-ops.json"),
        releases: home.join("releases"),
        release_keys: home.join("release-keys.json"),
        pull_marker: home.join("identity").join("sync-pull-now"),
        advertise_marker: home.join("identity").join("advertise-now"),
        peer_addrs: home.join("identity").join("peer-addrs.json"),
        heartbeat: home.join("identity").join("pump-heartbeat.json"),
    }
}


        peer_addrs: home.join("identity").join("peer-addrs.json"),
        heartbeat: home.join("identity").join("pump-heartbeat.json"),
    }
}

        peer_addrs: home.join("identity").join("peer-addrs.json"),
        heartbeat: home.join("identity").join("pump-heartbeat.json"),
        rotations: home.join("identity").join("rotation-pending.json"),
    }
}


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs has been updated successfully. (file state is current in your context — no need to Read it back)

        peer_addrs: home.join("identity").join("peer-addrs.json"),
        heartbeat: home.join("identity").join("pump-heartbeat.json"),
    }
}

        peer_addrs: home.join("identity").join("peer-addrs.json"),
        heartbeat: home.join("identity").join("pump-heartbeat.json"),
        rotations: home.join("identity").join("rotation-pending.json"),
    }
}

+        rotations: home.join("identity").join("rotation-pending.json"),


Now build the daemon with the pump changes and run the broader daemon test suite:


Bash

cargo build -p spt-daemon --tests 2>&1 | tail -15

Build daemon + tests with pump rotation changes


   Compiling spt-daemon v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 24.15s


Builds. Let me add a focused unit test for `fire_due_rotations` (pump firing + force-skip). Check for an existing test module in peerloop.rs:


grep -n "mod tests\|#\[cfg(test)\]" crates/spt-daemon/src/peerloop.rs | tail -5

Check for inline test module in peerloop


819:#[cfg(test)]
820:mod tests {


819	#[cfg(test)]
820	mod tests {
821	    use super::*;
822
823	    // The pump's cadence gate must prime on the first tick AND never compute
824	    // an instant before now (the underflow panic that killed the peer pump on
825	    // a sub-24h-uptime Windows host). `due` takes an Option and only ever runs
826	    // `now.duration_since(past)` forward — there is no `Instant - Duration`
827	    // arithmetic left to underflow.
828	    // [unit->REQ-HAZARD-INSTANT-UNDERFLOW]
829	    #[test]
830	    fn first_tick_is_due_and_never_underflows() {
831	        let now = Instant::now();
832	        // A day-long cadence on a process that may have started milliseconds
833	        // ago: the never-run leg is due regardless, with no subtraction-from-
834	        // instant anywhere in the path.
835	        assert!(due(None, now, Duration::from_secs(86_400)));
836	        // A leg that just ran is not due until its cadence elapses.
837	        assert!(!due(Some(now), now, Duration::from_secs(86_400)));
838	        // And it becomes due once enough has elapsed.
839	        let earlier = now - Duration::from_millis(50);
840	        assert!(due(Some(earlier), now, Duration::from_millis(10)));
841	        assert!(!due(Some(earlier), now, Duration::from_millis(500)));
842	    }
843
844	    // [unit->REQ-MESH-3] the widened fan-out: the push target is EVERY roster
845	    // member of the subnet (not the directly-paired set), excluding self and
846	    // tombstoned members. This is the seam whose widening closes the staggered
847	    // A→B→C repro — own rows reach all members directly, never relayed.
848	    #[test]

#[cfg(test)]
mod tests {
    use super::*;

    // The pump's cadence gate must prime on the first tick AND never compute
    // an instant before now (the underflow panic that killed the peer pump on
    // a sub-24h-uptime Windows host). `due` takes an Option and only ever runs
    // `now.duration_since(past)` forward — there is no `Instant - Duration`
    // arithmetic left to underflow.
    // [unit->REQ-HAZARD-INSTANT-UNDERFLOW]
    #[test]
    fn first_tick_is_due_and_never_underflows() {
        let now = Instant::now();
        // A day-long cadence on a process that may have started milliseconds
        // ago: the never-run leg is due regardless, with no subtraction-from-
        // instant anywhere in the path.
        assert!(due(None, now, Duration::from_secs(86_400)));
        // A leg that just ran is not due until its cadence elapses.
        assert!(!due(Some(now), now, Duration::from_secs(86_400)));
        // And it becomes due once enough has elapsed.
        let earlier = now - Duration::from_millis(50);
        assert!(due(Some(earlier), now, Duration::from_millis(10)));
        assert!(!due(Some(earlier), now, Duration::from_millis(500)));
    }

    // [unit->REQ-MESH-3] the widened fan-out: the push target is EVERY roster
    // member of the subnet (not the directly-paired set), excluding self and
    // tombstoned members. This is the seam whose widening closes the staggered
    // A→B→C repro — own rows reach all members directly, never relayed.
    #[test]


Edit

    // [unit->REQ-MESH-3] the widened fan-out: the push target is EVERY roster
    // member of the subnet (not the directly-paired set), excluding self and
    // tombstoned members. This is the seam whose widening closes the staggered
    // A→B→C repro — own rows reach all members directly, never relayed.
    #[test]

    // [unit->REQ-MESH-4] the pump fires a seed rotation only once its window has
    // closed: a due subnet rotates (epoch bumps, prior seed retained one-deep)
    // and its pending entry clears; a not-yet-due subnet is untouched and stays
    // scheduled. This is the daemon-side half of the coalescing window.
    #[test]
    fn fire_due_rotations_rotates_due_and_leaves_pending() {
        let dir = tempfile::tempdir().unwrap();
        let subnets_path = dir.path().join("subnet.json");
        let rotations_path = dir.path().join("rotation-pending.json");

        let mut subnets = SubnetStore::default();
        let before_home = subnets.create_subnet("home").unwrap().clone();
        subnets.create_subnet("work").unwrap();
        subnets.save_to(&subnets_path).unwrap();

        let mut pending = spt_store::rotation::RotationPending::default();
        pending.coalesce("home", &["aa".to_string()], 1_000); // due at now=1_000
        pending.coalesce("work", &["bb".to_string()], 5_000); // not yet due
        pending.save_to(&rotations_path).unwrap();

        fire_due_rotations(&subnets_path, &rotations_path, 1_000);

        let after = SubnetStore::load_from(&subnets_path);
        let home = after.find("home").unwrap();
        assert_eq!(home.epoch, before_home.epoch + 1, "due subnet rotated");
        assert_eq!(
            home.prev_seed_hex.as_deref(),
            Some(before_home.seed_hex.as_str()),
            "prior seed retained for the grace"
        );
        assert_eq!(after.find("work").unwrap().epoch, 1, "not-due subnet untouched");

        let pend = spt_store::rotation::RotationPending::load_from(&rotations_path);
        assert!(!pend.subnets.contains_key("home"), "fired entry cleared");
        assert!(pend.subnets.contains_key("work"), "pending entry kept");
    }

    // [unit->REQ-MESH-3] the widened fan-out: the push target is EVERY roster
    // member of the subnet (not the directly-paired set), excluding self and
    // tombstoned members. This is the seam whose widening closes the staggered
    // A→B→C repro — own rows reach all members directly, never relayed.
    #[test]


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\peerloop.rs has been updated successfully. (file state is current in your context — no need to Read it back)

    // [unit->REQ-MESH-4] the pump fires a seed rotation only once its window has
    // closed: a due subnet rotates (epoch bumps, prior seed retained one-deep)
    // and its pending entry clears; a not-yet-due subnet is untouched and stays
    // scheduled. This is the daemon-side half of the coalescing window.
    #[test]
    fn fire_due_rotations_rotates_due_and_leaves_pending() {
        let dir = tempfile::tempdir().unwrap();
        let subnets_path = dir.path().join("subnet.json");
        let rotations_path = dir.path().join("rotation-pending.json");

        let mut subnets = SubnetStore::default();
        let before_home = subnets.create_subnet("home").unwrap().clone();
        subnets.create_subnet("work").unwrap();
        subnets.save_to(&subnets_path).unwrap();

        let mut pending = spt_store::rotation::RotationPending::default();
        pending.coalesce("home", &["aa".to_string()], 1_000); // due at now=1_000
        pending.coalesce("work", &["bb".to_string()], 5_000); // not yet due
        pending.save_to(&rotations_path).unwrap();

        fire_due_rotations(&subnets_path, &rotations_path, 1_000);

        let after = SubnetStore::load_from(&subnets_path);
        let home = after.find("home").unwrap();
        assert_eq!(home.epoch, before_home.epoch + 1, "due subnet rotated");
        assert_eq!(
            home.prev_seed_hex.as_deref(),
            Some(before_home.seed_hex.as_str()),
            "prior seed retained for the grace"
        );
        assert_eq!(after.find("work").unwrap().epoch, 1, "not-due subnet untouched");

        let pend = spt_store::rotation::RotationPending::load_from(&rotations_path);
        assert!(!pend.subnets.contains_key("home"), "fired entry cleared");
        assert!(pend.subnets.contains_key("work"), "pending entry kept");
    }

    // [unit->REQ-MESH-3] the widened fan-out: the push target is EVERY roster
    // member of the subnet (not the directly-paired set), excluding self and
    // tombstoned members. This is the seam whose widening closes the staggered
    // A→B→C repro — own rows reach all members directly, never relayed.
    #[test]

+    // [unit->REQ-MESH-4] the pump fires a seed rotation only once its window has

+    // closed: a due subnet rotates (epoch bumps, prior seed retained one-deep)

+    // and its pending entry clears; a not-yet-due subnet is untouched and stays

+    // scheduled. This is the daemon-side half of the coalescing window.

+    fn fire_due_rotations_rotates_due_and_leaves_pending() {

+        let dir = tempfile::tempdir().unwrap();

+        let subnets_path = dir.path().join("subnet.json");

+        let rotations_path = dir.path().join("rotation-pending.json");

+        let mut subnets = SubnetStore::default();

+        let before_home = subnets.create_subnet("home").unwrap().clone();

+        subnets.create_subnet("work").unwrap();

+        subnets.save_to(&subnets_path).unwrap();

+        let mut pending = spt_store::rotation::RotationPending::default();

+        pending.coalesce("home", &["aa".to_string()], 1_000); // due at now=1_000

+        pending.coalesce("work", &["bb".to_string()], 5_000); // not yet due

+        pending.save_to(&rotations_path).unwrap();

+        fire_due_rotations(&subnets_path, &rotations_path, 1_000);

+        let after = SubnetStore::load_from(&subnets_path);

+        let home = after.find("home").unwrap();

+        assert_eq!(home.epoch, before_home.epoch + 1, "due subnet rotated");

+            home.prev_seed_hex.as_deref(),

+            Some(before_home.seed_hex.as_str()),

+            "prior seed retained for the grace"

+        assert_eq!(after.find("work").unwrap().epoch, 1, "not-due subnet untouched");

+        let pend = spt_store::rotation::RotationPending::load_from(&rotations_path);

+        assert!(!pend.subnets.contains_key("home"), "fired entry cleared");

+        assert!(pend.subnets.contains_key("work"), "pending entry kept");


Bash

cargo test -p spt-daemon --lib peerloop::tests::fire_due_rotations_rotates_due_and_leaves_pending 2>&1 | tail -12

Run the pump rotation-firing unit test


   Compiling spt-daemon v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 5.45s
     Running unittests src\lib.rs (target\debug\deps\spt_daemon-26a17645c4a18da2.exe)

running 1 test
test peerloop::tests::fire_due_rotations_rotates_due_and_leaves_pending ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 217 filtered out; finished in 0.01s


All five layers green. **Layer 5 — finalize traceability + docs + full verification.** Activate REQ-MESH-4:


Edit

required_stages = []  # activate at mesh execution. impl = revoke CLI (list) + tombstone+window scheduler (coalesce, --force-rotate-seed) + confidential seed push over member conns + force-drop sweep + re-seed-only restricted handshake (prior-epoch ∧ roster-gated). unit = window coalescing (N revokes→1 bump) + re-seed grants benign-N-1-on-roster / denies revoked-off-roster / denies N-2 + seed-never-in-gossip + force-rotate skips window. int = revoke-then-offliner-returns auto-heals on the rig.

required_stages = ["impl", "unit", "int"]  # Mesh-D7 activated. impl = revoke CLI (list, elevation-gated, own-refuse) + propagating tombstone + coalescing-window scheduler (rotation.rs: coalesce/due_subnets, --force-rotate-seed inline) + pump fire_due_rotations + 3-valued seed-proof grading (current+prior ProofSet, grade_subnet) + confidential SeedTransfer push/adopt over the member control stream (rotate_seed retains prev one-deep, adopt_rotation). NOTE deviations from the JIT plan: (a) re-seed delivery is INLINE on the proof exchange (a reseed-only conn registers with an empty proven set purely to deliver the seed, then is replaced when the healed peer reconnects full) rather than a separate dispatcher-gated stream — stronger fail-closed (the conn serves nothing) with zero new broker IPC, which D5 deferred; (b) force-drop of a revokee's LINGERING conn is deferred — the propagating tombstone (app gate) + rotation (connect gate) already lock the revokee out, and the int test confirms denial without it. unit = window coalescing (N revokes→1 bump, earliest deadline) + grade truth table (exact∧present⇒full, N-1∧present⇒reseed, tombstoned/off-roster/N-2⇒denied) + seed-never-in-gossip codec assert + ProofSet/SeedTransfer codec round-trips + adopt_rotation idempotence + fire_due_rotations. int = benign offliner re-seeded across a rotation + revoked node denied and never re-seeded (reseed.rs).


The file C:\Users\decid\Documents\projects\spt-core\traceable-reqs.toml has been updated successfully. (file state is current in your context — no need to Read it back)

title = "Revoke + timeboxed seed rotation + re-seed grace: `spt subnet revoke <node>...` (list, elevation-gated, revoke-only) writes roster tombstones immediately, then schedules ONE seed rotation (re-mint seed, bump seed_epoch, push new seed CONFIDENTIALLY over member-auth'd TLS connections — never in roster/registry gossip — force-drop revokees) at the close of a coalescing window (default 1h); further revokes in the window join the same rotation (one epoch bump). `--force-rotate-seed` rotates immediately (compromised-node path). RE-SEED GRACE: a node proving the immediately-prior epoch (N-1) AND still on the roster gets a re-seed-only restricted connection (auto-heals a benign offliner); revoked/off-roster denied; ≥2 stale → re-pair."

-required_stages = []  # activate at mesh execution. impl = revoke CLI (list) + tombstone+window scheduler (coalesce, --force-rotate-seed) + confidential seed push over member conns + force-drop sweep + re-seed-only restricted handshake (prior-epoch ∧ roster-gated). unit = window coalescing (N revokes→1 bump) + re-seed grants benign-N-1-on-roster / denies revoked-off-roster / denies N-2 + seed-never-in-gossip + force-rotate skips window. int = revoke-then-offliner-returns auto-heals on the rig.

+required_stages = ["impl", "unit", "int"]  # Mesh-D7 activated. impl = revoke CLI (list, elevation-gated, own-refuse) + propagating tombstone + coalescing-window scheduler (rotation.rs: coalesce/due_subnets, --force-rotate-seed inline) + pump fire_due_rotations + 3-valued seed-proof grading (current+prior ProofSet, grade_subnet) + confidential SeedTransfer push/adopt over the member control stream (rotate_seed retains prev one-deep, adopt_rotation). NOTE deviations from the JIT plan: (a) re-seed delivery is INLINE on the proof exchange (a reseed-only conn registers with an empty proven set purely to deliver the seed, then is replaced when the healed peer reconnects full) rather than a separate dispatcher-gated stream — stronger fail-closed (the conn serves nothing) with zero new broker IPC, which D5 deferred; (b) force-drop of a revokee's LINGERING conn is deferred — the propagating tombstone (app gate) + rotation (connect gate) already lock the revokee out, and the int test confirms denial without it. unit = window coalescing (N revokes→1 bump, earliest deadline) + grade truth table (exact∧present⇒full, N-1∧present⇒reseed, tombstoned/off-roster/N-2⇒denied) + seed-never-in-gossip codec assert + ProofSet/SeedTransfer codec round-trips + adopt_rotation idempotence + fire_due_rotations. int = benign offliner re-seeded across a rotation + revoked node denied and never re-seeded (reseed.rs).

title = "Hard cutover from pairwise trust: delete peers.json + the is_trusted authorization path (no migration — expendable test fleet, re-pairs fresh under the new model, user decision 2026-06-08). Warn-on-change DEMOTED from a gate to an awareness notice anchored on machine_id (not label): 'machine M, last seen as K1, now presents K2' — fires the same event as the REQ-SUBNET-7 re-pair overwrite. The TrustStore/peers.json code and its call sites are removed, not left dead."


Bash

traceable-reqs check 2>&1 | tail -30; echo "EXIT=$?"

Run traceable-reqs check, show raw exit


  [OK] REQ-SUBNET-4  required: [impl, unit]  stages: +doc +impl +unit -int
       Subnet membership mutations elevation-gated (create = seed reveal; join = trust-boundary enrollment)
  [OK] REQ-SUBNET-5  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Per-subnet serve-state: spt subnet detach <NAME> [--save] / attach <NAME> [--save] — daemon keeps running, stops/starts advertising + connecting for that subnet (peer pump + responder selective); --save persists the startup default in daemon config; the all-attached banner gains per-subnet states (M8 decision 6, --save renamed from --auto per decision 25 session)
  [OK] REQ-SUBNET-6  required: [impl, unit]  stages: -doc +impl +unit -int
       Trust lifecycle verbs, elevation-gated: spt subnet leave <NAME> (membership exit) and spt subnet prune <node> (removes a dead identity's trust + registry rows, killing its dead dials; trust mutation = security surface, REQ-PAIR-6 gate machinery) (M8 decisions 6-7)
  [OK] REQ-SUBNET-7  required: [impl, unit]  stages: -doc +impl +unit -int
       Per-machine re-pair trust overwrite: registry rows carry a hashed stable machine identifier (OS machine id /etc/machine-id|MachineGuid, domain-separated SHA-256 before gossip, spt-minted persisted UUID fallback; additive serde-default field — old rows parse clean); a COMPLETED pairing ceremony presenting the same node label AND machine id as an existing trusted row evicts the superseded identity's trust + registry rows on the seed-holder and replicates the eviction; a gossiped claim alone never evicts trust (M8 decisions 13, 22)
  [OK] REQ-SUBNET-8  required: [impl, unit]  stages: -doc +impl +unit -int
       Status render honesty: zero-subnet text is daemon-aware ('No subnets registered — this node is standalone.' + daemon-running-dependent blurb, never implying messaging works while the daemon is down); hint footer prints on bare spt subnet only (status drops it); a stalled pump is surfaced in subnet status, never rendered implied-healthy (M8 decisions 11-12, 23)
  [OK] REQ-TERM-1  required: [impl, unit]  stages: -doc +impl +unit -int
       Process-supervisor terminal wrapper hosting broker PTYs
  [OK] REQ-TERM-2  required: [impl, unit]  stages: -doc +impl +unit -int
       session-surface abstraction; send-keys + send-line injection
  [OK] REQ-TERM-3  required: [impl, unit]  stages: -doc +impl +unit -int
       Byte-stream remote terminal streaming for v1
  [OK] REQ-TERM-4  required: [impl, unit, int]  stages: +doc +impl +unit +int
       Live activity buffer (PTY digest): adapter-supplied patterns over broker PTY, spt digest pull + delta-stream, opt-in Path-B log
  [OK] REQ-UPD-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Peer-propagated update over P2P
  [OK] REQ-UPD-2  required: [impl, unit]  stages: -doc +impl +unit -int
       All binaries signature-verified before handoff
  [OK] REQ-UPD-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
       No endpoint process terminates/suspends during self-update
  [OK] REQ-UPD-4  required: [impl, unit]  stages: -doc +impl +unit -int
       Update gated on user confirmation by default; opt-in full-auto
  [OK] REQ-UPD-5  required: [impl, unit]  stages: -doc +impl +unit -int
       spt-core ripple-updates registered adapters
  [OK] REQ-UPD-6  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       Platform-targeted update sets and debug rollout: signed multi-platform update metadata, recipient platform selection, channel-scoped monotonic counters, debug-channel opt-in via release-key overlay, local staging plus pull-based peer propagation, and maintainer-only convergence tooling (ADR-0016)
EXIT=0


traceable-reqs check 2>&1 | grep -A1 "REQ-MESH-4"; echo "---"; traceable-reqs check 2>&1 | grep -iE "fail|missing|error|\[X\]|\[!\]" | head; echo "EXIT=$?"

Confirm REQ-MESH-4 covered + no failures


  [OK] REQ-MESH-4  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Revoke + timeboxed seed rotation + re-seed grace: `spt subnet revoke <node>...` (list, elevation-gated, revoke-only) writes roster tombstones immediately, then schedules ONE seed rotation (re-mint seed, bump seed_epoch, push new seed CONFIDENTIALLY over member-auth'd TLS connections — never in roster/registry gossip — force-drop revokees) at the close of a coalescing window (default 1h); further revokes in the window join the same rotation (one epoch bump). `--force-rotate-seed` rotates immediately (compromised-node path). RE-SEED GRACE: a node proving the immediately-prior epoch (N-1) AND still on the roster gets a re-seed-only restricted connection (auto-heals a benign offliner); revoked/off-roster denied; ≥2 stale → re-pair.
---
       Peer address seeding, both cold starts: durable peer-addrs.json (identity dir) maps peer pubkey → last-known dialable address; the pump's resolver consults it FIRST with id-only discovery fallback on miss or dial failure (a stale addr never strands a peer); written by the pairing ceremony (both sides, from the live connection) and by the pump on successful connect; post-join first sync and post-restart resync converge in seconds, not ~1 min (M8 decisions 14, 20)
       A surfaced concurrent context pair is durably preserved (both versions, tracked artifacts) until a strictly dominating write clears it; no reconcile failure path discards an unmerged version (6.6, ADR-0013)
       Concurrent SQLite openers (registry/spool) must not fail with 'database is locked' (4.7)
       Stale registry entries degrade to fallback, never hard-fail (4.3)
       NTP TOTP offset: the pairing ceremony queries NTP at ceremony time (both sides) and applies the derived offset to the TOTP calculation in-process only; system-clock fallback when NTP is unreachable (offline LAN pairing unaffected — NTP failure never blocks a pairing that succeeds today); never sets the OS clock; no background sync loop (M8 decision 18; field trigger: enlyzeam clock >1 min off exceeds the ±1 window)
EXIT=0


REQ-MESH-4 fully covered (impl+unit+int). The other grep hits are requirement *titles* containing "fail"/"error", not failures (exit 0). Now regenerate docs (revoke `--help` changed the CLI reference) and verify the drift gate:


cargo run -p xtask -- gen 2>&1 | tail -8 && echo "---CHECK---" && cargo run -p xtask -- check 2>&1 | tail -12

Regenerate docs then run the drift check


   Compiling spt-daemon v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
   Compiling xtask v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\xtask)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 5.64s
     Running `target\debug\xtask.exe gen`
xtask gen: wrote C:\Users\decid\Documents\projects\spt-core\docs-site/src/cli/reference.md
---CHECK---
   Compiling tokio-websockets v0.13.2
   Compiling reqwest v0.13.4
   Compiling hickory-resolver v0.26.0-beta.4
   Compiling noq v0.18.0
   Compiling iroh-relay v0.98.0
   Compiling iroh v0.98.2
   Compiling spt-net v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
   Compiling spt-daemon v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
   Compiling xtask v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\xtask)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 25.02s
     Running `target\debug\xtask.exe check`
xtask check: OK


Docs regenerated, drift gate OK. Now the full verification gauntlet — workspace tests, clippy, and `--no-default-features`:


cargo test --workspace 2>&1 | grep -E "test result|error\[|error:|FAILED|running [0-9]+ test" | tail -60; echo "EXIT=${PIPESTATUS[0]}"

Run full workspace test suite


running 2 tests
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.22s
running 2 tests
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s
running 4 tests
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.28s
running 2 tests
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.21s
running 3 tests
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.49s
running 2 tests
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 2 tests
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.26s
running 2 tests
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.66s
running 49 tests
test result: ok. 49 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.38s
running 28 tests
test result: ok. 28 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.31s
running 2 tests
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.23s
running 121 tests
test result: ok. 120 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.21s
running 54 tests
test result: ok. 54 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
running 26 tests
test result: ok. 26 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.32s
running 151 tests
test result: ok. 151 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 7.82s
running 8 tests
test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
running 1 test
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.09s
running 2 tests
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
running 2 tests
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s
running 2 tests
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.01s
running 1 test
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
running 1 test
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
EXIT=0


Full workspace suite green (EXIT=0). Now clippy + `--no-default-features`:


cargo clippy --workspace --all-targets 2>&1 | grep -E "warning:|error" | grep -v "generated " | head -30; echo "CLIPPY_EXIT=${PIPESTATUS[0]}"

Run clippy across the workspace


cargo build --workspace --no-default-features 2>&1 | grep -E "error|warning: unused" | head -20; echo "NODEF_EXIT=${PIPESTATUS[0]}"

Build workspace with no default features


All gates green: clippy clean, `--no-default-features` clean, full suite + traceability + docs-drift all pass. Let me review the diff and check the project's commit trailer:


git log -1 --format='%b' 6843a8f | tail -5; echo "===STATUS==="; git status --short; echo "===STAT==="; git diff --stat

Inspect commit trailer + working tree changes


Green: workspace tests, clippy -D warnings, traceable-reqs check, and
--no-default-features all exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

===STATUS===
 M crates/spt-daemon/src/peerloop.rs
 M crates/spt-daemon/src/seedproofx.rs
 M crates/spt-daemon/tests/mesh.rs
 M crates/spt-daemon/tests/peerloop.rs
 M crates/spt-daemon/tests/rosterprop.rs
 M crates/spt-daemon/tests/seedproofx.rs
 M crates/spt-daemon/tests/twohost.rs
 M crates/spt-net/src/net/mesh/seedproof.rs
 M crates/spt-net/src/net/pairing/wire.rs
 M crates/spt-store/src/lib.rs
 M crates/spt-store/src/perch.rs
 M crates/spt-store/src/subnet.rs
 M crates/spt/src/cli.rs
 M docs-site/src/cli/reference.md
 M traceable-reqs.toml
?? crates/spt-daemon/tests/reseed.rs
?? crates/spt-store/src/rotation.rs
===STAT===
warning: in the working copy of 'crates/spt-daemon/src/seedproofx.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/mesh.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/peerloop.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/rosterprop.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/seedproofx.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/twohost.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-net/src/net/mesh/seedproof.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-store/src/lib.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-store/src/perch.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-store/src/subnet.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt/src/cli.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs-site/src/cli/reference.md', LF will be replaced by CRLF the next time Git touches it
 crates/spt-daemon/src/peerloop.rs        |  85 +++++
 crates/spt-daemon/src/seedproofx.rs      | 590 +++++++++++++++++++++++++++----
 crates/spt-daemon/tests/mesh.rs          |   2 +
 crates/spt-daemon/tests/peerloop.rs      |   2 +
 crates/spt-daemon/tests/rosterprop.rs    |  18 +-
 crates/spt-daemon/tests/seedproofx.rs    |   1 +
 crates/spt-daemon/tests/twohost.rs       |   1 +
 crates/spt-net/src/net/mesh/seedproof.rs |  92 +++++
 crates/spt-net/src/net/pairing/wire.rs   |   1 +
 crates/spt-store/src/lib.rs              |   1 +
 crates/spt-store/src/perch.rs            |  10 +
 crates/spt-store/src/subnet.rs           | 109 +++++-
 crates/spt/src/cli.rs                    | 195 ++++++++++
 docs-site/src/cli/reference.md           |  28 ++
 traceable-reqs.toml                      |   2 +-
 15 files changed, 1060 insertions(+), 77 deletions(-)


Everything is green and complete. Let me stage and commit with the project trailer:


cd C:/Users/decid/Documents/projects/spt-core && git add -A && git commit -q -F - <<'EOF'
feat(mesh): subnet revoke + timeboxed rotation + re-seed grace (REQ-MESH-4) — Mesh-D7

The membership-removal lifecycle. `spt subnet revoke <node>...` (list,
elevation-gated, own-identity refuses) writes a PROPAGATING roster tombstone
now and schedules ONE seed rotation at the close of a coalescing window
(default 1h); revokes in the window join the same entry (one epoch bump).
`--force-rotate-seed` rotates inline (compromised path). The daemon pump fires
due rotations each registry tick.

Re-seed grace (the inseparable other half): the seed-proof verify goes
three-valued per subnet — Full (exact-epoch, non-revoked), ReseedOnly (proved
the immediately-prior epoch, present, non-revoked), Denied (≥2 stale,
off-roster, tombstoned, forged). `rotate_seed` now retains the prior seed
one-deep; both sides send current+prior proofs (the new ProofSet frame) so a
benign offliner on N-1 and a rotator on N overlap on N-1. The rotator pushes
its current seed CONFIDENTIALLY over the member control stream's SeedTransfer
leg (never roster/registry gossip); the offliner adopts it and reconnects full.
A revoked node is denied even holding the prior seed — it must re-pair.

Deviations from MESH-D7-PLAN (documented on REQ-MESH-4):
- Re-seed delivery is INLINE on the proof exchange (a reseed-only conn
  registers with an empty proven set purely to deliver the seed, then is
  replaced when the healed peer reconnects full) rather than a separate
  dispatcher-gated restricted stream — stronger fail-closed (the conn serves
  nothing) with zero new broker IPC, which D5 deferred. The legs run over the
  symmetric Hello-agreed shared set, not the asymmetric grade, so a one-sided
  tombstone never desyncs the stream.
- Force-drop of a revokee's LINGERING conn is deferred: the propagating
  tombstone (app gate) + rotation (connect gate) already lock it out, confirmed
  by the int test.

Evidence: store layer (subnet prev_seed/adopt_rotation, rotation.rs
coalesce/due_subnets), 3-valued grade truth table, ProofSet/SeedTransfer codecs,
seed-never-in-gossip assert, fire_due_rotations, and the reseed.rs acceptance
(benign offliner re-seeded across a rotation; revoked node denied and never
re-seeded). traceable-reqs REQ-MESH-4 [impl,unit,int]; D2/D4/D6 regression,
clippy, --no-default-features, and the docs-drift gate all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EOF
git log --oneline -1

Commit the Mesh-D7 implementation


warning: in the working copy of 'crates/spt-daemon/src/seedproofx.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/mesh.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/peerloop.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/rosterprop.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/seedproofx.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/twohost.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-net/src/net/mesh/seedproof.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-store/src/lib.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-store/src/perch.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-store/src/subnet.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt/src/cli.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs-site/src/cli/reference.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/reseed.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-store/src/rotation.rs', LF will be replaced by CRLF the next time Git touches it
618845d feat(mesh): subnet revoke + timeboxed rotation + re-seed grace (REQ-MESH-4) — Mesh-D7


Mesh-D7 done. Committed `618845d` on dev-freeform.

**What shipped — membership-removal lifecycle (REQ-MESH-4 `[impl,unit,int]`):**

- **`spt subnet revoke <node>...`** — list form, elevation-gated, own-identity refuses. Propagating tombstone now + ONE coalescing-window rotation (default 1h; revokes in window → one epoch bump). `--force-rotate-seed` = rotate inline.
- **Pump fires due rotations** each registry tick (`fire_due_rotations`).
- **3-valued seed-proof grade** — Full / ReseedOnly / Denied. `rotate_seed` retains prior seed one-deep; both sides send current+prior proofs (new `ProofSet` frame) so a benign N-1 offliner and an N rotator overlap on N-1.
- **Confidential re-seed** — rotator pushes current seed over the member control stream's `SeedTransfer` leg (never gossip); offliner adopts, reconnects full. Revoked node denied even holding old seed → must re-pair.

**Two deviations from JIT plan (documented on REQ-MESH-4):**
1. Re-seed delivery **inline** on the proof exchange (reseed-only conn registers empty-proven just to deliver, then replaced on re-dial) — stronger fail-closed, zero new broker IPC (which D5 deferred). Legs run over symmetric Hello-shared set so one-sided tombstone never desyncs stream.
2. Force-drop of revokee's lingering conn **deferred** — tombstone (app gate) + rotation (connect gate) already lock out; int test confirms.

**Green:** spt-store 151 + grade truth table + codecs + seed-never-in-gossip + `reseed.rs` acceptance (offliner re-seeded; revokee denied) + D2/D4/D6 regression + traceable-reqs + docs-drift + clippy + `--no-default-features`.

Commit local only — not pushed (you control merge to main). Want me push dev-freeform to fire CI? Next: **D8** = `status --nodes` probe concurrency (REQ-MESH-6, independent polish).


create JIT plan for D8, then /commune with immediate next steps and broad summary of the project's status + end goal. we'll resume in the next session


<owl_reachability_notice>
You have an active owl perch. When spawning subagents or doing work, use run_in_background: true so you stay reachable for incoming messages.
</owl_reachability_notice>


CAVEMAN MODE ACTIVE (full). Drop articles/filler/pleasantries/hedging. Fragments OK. Code/commits/security: write normal.


Create D8 plan. First verify current REQ-SUBNET-5 probe loop — is it serial?


152:    /// an existing offline instance back is `relink` / `persistent` /
257:    /// save fires and persistent shells cascade offline with it.
438:    /// starting offline (the launch + bind handshake brings it online).
498:    /// Bring an existing offline (persistent) instance back online: re-spawns
543:        /// Per-node rows: label, online/offline, [online endpoints/total].
607:    /// window join the same rotation (one epoch bump). Benign offliners
616:        /// compromised-node path (a benign offliner may then fall behind and
1214:/// shells offline"). Two legs through existing machinery: (1) soft-stop the
1810:        // Going offline (the --once listener ends here): drop the perch's
1867:    // Going offline: wake the pump's registry leg so peers see the flip
2231:/// plan decision 3). Wires the real store, clock, and elevation probe into the
2310:/// owlery) is still a member, and an offline peer whose rows the 4.10 eviction
2422:/// named rule is absent, say so with the exact fix; when the probe itself
2423:/// fails or times out, say nothing (never alarm off a broken probe).
2450:/// `netsh` probe (KNOWN-HAZARDS 5.3: every subprocess has a timeout — a
2451:/// wedged netsh costs 3s, never a hang). `None` = probe failed/timed out
2452:/// (the caller stays silent rather than alarm on a broken probe).
2519:/// over its inputs (the probe is the caller's impure follow-up).
2523:/// (`[0/0]`): a fresh box has no endpoints yet, and an offline peer's rows
2526:/// offline; heard within `stale_after_ms` → online; otherwise (including
2527:/// a rowless member — absence is not evidence) → probe.
2597:/// Render one subnet's `--nodes` table from probe-settled rows.
2636:            if *online { "online" } else { "offline" },
2647:/// offline — fail-quiet, this is a status view.
2663:/// Subnet-scoped liveness probe for one node, BOUNDED. The `--nodes` view
2666:/// on the M8 acceptance rig). A probe that does not complete within
2667:/// `PROBE_TIMEOUT` reads as offline — a node that cannot finish a handshake in
2668:/// a couple seconds is offline for liveness purposes. The probe is now
2669:/// subnet-scoped (REQ-SUBNET-5): it dials (proves reachable) THEN asks the
2670:/// peer "serving `subnet`?", so a detached-but-alive peer reads offline for

[Showing results with pagination = limit: 30]


2663	/// Subnet-scoped liveness probe for one node, BOUNDED. The `--nodes` view
2664	/// dials this synchronously per silent node; an id-only dial to a dead peer
2665	/// otherwise runs the full iroh discovery+connect timeout (~30s, observed live
2666	/// on the M8 acceptance rig). A probe that does not complete within
2667	/// `PROBE_TIMEOUT` reads as offline — a node that cannot finish a handshake in
2668	/// a couple seconds is offline for liveness purposes. The probe is now
2669	/// subnet-scoped (REQ-SUBNET-5): it dials (proves reachable) THEN asks the
2670	/// peer "serving `subnet`?", so a detached-but-alive peer reads offline for
2671	/// that subnet (was the criterion-5 detach-liveness bug — a raw dial proved
2672	/// only the box was up).
2673	// [impl->REQ-SUBNET-1]
2674	// [impl->REQ-SUBNET-5]
2675	fn probe_node(node_hex: &str, subnet: &str) -> bool {
2676	    const PROBE_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(2500);
2677	    let hex = node_hex.to_string();
2678	    let sub = subnet.to_string();
2679	    run_bounded(PROBE_TIMEOUT, false, move || {
2680	        crate::wansend::probe_node_serving(&hex, &sub)
2681	    })
2682	}
2683
2684	/// Render the coming-online banner's "spt subnets joined" section + the
2685	/// concluding divider: header, underline, one `NAME: <state>` row per held
2686	/// subnet (`(none)` when empty), divider. Pure for tests.
2687	// [impl->REQ-DAEMON-3]
2688	fn render_subnets_joined(rows: &[(String, &'static str)]) -> String {
2689	    let mut out = String::from("spt subnets joined\n──────────────────\n");
2690	    if rows.is_empty() {
2691	        out.push_str("  (none)\n");
2692	    } else {
2693	        for (name, state) in rows {
2694	            out.push_str(&format!("  {name}: {state}\n"));
2695	        }
2696	    }
2697	    out.push_str("────────────────────────────────────────\n");
2698	    out
2699	}
2700
2701	/// Ensure the per-machine daemon is up for a daemon-dependent command —
2702	/// silent when it already runs; otherwise announce the transition on
2703	/// stderr (stdout stays payload-pure) and conclude with a divider before
2704	/// the command's own output (grill 2026-06-06). The membership-creating
2705	/// verbs call this because **membership implies reachability** (CONTEXT
2706	/// §subnet attachment): a sole seed-holder with no responder daemon is a
2707	/// contradiction of the subnet's purpose.
2708	// [impl->REQ-SUBNET-2] [impl->REQ-DAEMON-3]
2709	fn ensure_daemon_announced() -> Result<(), i32> {
2710	    if spt_daemon::is_running() {
2711	        return Ok(());
2712	    }
2713	    eprintln!("spt-core daemon is offline - coming online...");
2714	    if let Err(e) = spt_daemon::ensure_running() {
2715	        eprintln!("DAEMON_START_FAIL: {e}");
2716	        return Err(1);
2717	    }
2718	    let ident = match (
2719	        spt_store::hostlabel::os_hostname(),
2720	        spt_store::nodeid::load_or_create()
2721	            .ok()
2722	            .map(|i| i.public_key().to_hex()),
2723	    ) {
2724	        (Some(l), Some(hex)) => {
2725	            format!("{l} ({}…)", spt_net::net::registry::key_prefix(&hex))
2726	        }
2727	        (None, Some(hex)) => format!("{}…", spt_net::net::registry::key_prefix(&hex)),
2728	        _ => "this node".to_string(),
2729	    };
2730	    eprintln!("spt-core daemon: online - {ident}");
2731	    // Attachment states per subnet (M8 decision 6, REQ-SUBNET-5): detached
2732	    // is a chosen per-subnet state; a net-less daemon (endpoint bind
2733	    // failed) serves nothing — "no connection", never conflated with a
2734	    // deliberate detach (CONTEXT §subnet attachment).
2735	    let net_up = spt_daemon::brain::Brain::cold_start(&spt_daemon::broker_socket_name(), now_ms())
2736	        .ok()
2737	        .and_then(|mut b| b.net_status().ok())
2738	        .map(|s| s.enabled)
2739	        .unwrap_or(true); // can't ask ⇒ assume serving rather than alarm
2740	    let att = spt_store::attachment::AttachmentStore::load();
2741	    let rows: Vec<(String, &'static str)> = SubnetStore::load()
2742	        .subnets
2743	        .iter()
2744	        .map(|s| {
2745	            let state = if att.is_detached(&s.name) {
2746	                "detached"
2747	            } else if net_up {
2748	                "attached"
2749	            } else {
2750	                "no connection"
2751	            };
2752	            (s.name.clone(), state)
2753	        })
2754	        .collect();
2755	    eprint!("{}", render_subnets_joined(&rows));
2756	    // Blocked-inbound honesty on the coming-online banner too (M8 root
2757	    // cause 3, REQ-INSTALL-7): a fresh user-scope install learns NOW, not
2758	    // after a silent NO_SEED_HOLDER join failure.
2759	    if let Some(w) = inbound_block_hint() {
2760	        eprint!("{w}");
2761	    }
2762	    Ok(())
2763	}
2764
2765	/// `spt subnet [status [NAME] [--nodes]]` — the membership view (REQ-SUBNET-1,
2766	/// M7 plan decisions 5 + 6; M8 decisions 11–12, 23). Read-only and ungated:
2767	/// prints membership shape, never secrets. `hints` = the bare-`spt subnet`
2768	/// invocation (the footer rides only that view).
2769	// [impl->REQ-SUBNET-1]
2770	// [impl->REQ-SUBNET-8]
2771	fn cmd_subnet_status(name: Option<String>, nodes: bool, hints: bool) -> i32 {
2772	    let store = SubnetStore::load();

/// Subnet-scoped liveness probe for one node, BOUNDED. The `--nodes` view
/// dials this synchronously per silent node; an id-only dial to a dead peer
/// otherwise runs the full iroh discovery+connect timeout (~30s, observed live
/// on the M8 acceptance rig). A probe that does not complete within
/// `PROBE_TIMEOUT` reads as offline — a node that cannot finish a handshake in
/// a couple seconds is offline for liveness purposes. The probe is now
/// subnet-scoped (REQ-SUBNET-5): it dials (proves reachable) THEN asks the
/// peer "serving `subnet`?", so a detached-but-alive peer reads offline for
/// that subnet (was the criterion-5 detach-liveness bug — a raw dial proved
/// only the box was up).
// [impl->REQ-SUBNET-1]
// [impl->REQ-SUBNET-5]
fn probe_node(node_hex: &str, subnet: &str) -> bool {
    const PROBE_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(2500);
    let hex = node_hex.to_string();
    let sub = subnet.to_string();
    run_bounded(PROBE_TIMEOUT, false, move || {
        crate::wansend::probe_node_serving(&hex, &sub)
    })
}

/// Render the coming-online banner's "spt subnets joined" section + the
/// concluding divider: header, underline, one `NAME: <state>` row per held
/// subnet (`(none)` when empty), divider. Pure for tests.
// [impl->REQ-DAEMON-3]
fn render_subnets_joined(rows: &[(String, &'static str)]) -> String {
    let mut out = String::from("spt subnets joined\n──────────────────\n");
    if rows.is_empty() {
        out.push_str("  (none)\n");
    } else {
        for (name, state) in rows {
            out.push_str(&format!("  {name}: {state}\n"));
        }
    }
    out.push_str("────────────────────────────────────────\n");
    out
}

/// Ensure the per-machine daemon is up for a daemon-dependent command —
/// silent when it already runs; otherwise announce the transition on
/// stderr (stdout stays payload-pure) and conclude with a divider before
/// the command's own output (grill 2026-06-06). The membership-creating
/// verbs call this because **membership implies reachability** (CONTEXT
/// §subnet attachment): a sole seed-holder with no responder daemon is a
/// contradiction of the subnet's purpose.
// [impl->REQ-SUBNET-2] [impl->REQ-DAEMON-3]
fn ensure_daemon_announced() -> Result<(), i32> {
    if spt_daemon::is_running() {
        return Ok(());
    }
    eprintln!("spt-core daemon is offline - coming online...");
    if let Err(e) = spt_daemon::ensure_running() {
        eprintln!("DAEMON_START_FAIL: {e}");
        return Err(1);
    }
    let ident = match (
        spt_store::hostlabel::os_hostname(),
        spt_store::nodeid::load_or_create()
            .ok()
            .map(|i| i.public_key().to_hex()),
    ) {
        (Some(l), Some(hex)) => {
            format!("{l} ({}…)", spt_net::net::registry::key_prefix(&hex))
        }
        (None, Some(hex)) => format!("{}…", spt_net::net::registry::key_prefix(&hex)),
        _ => "this node".to_string(),
    };
    eprintln!("spt-core daemon: online - {ident}");
    // Attachment states per subnet (M8 decision 6, REQ-SUBNET-5): detached
    // is a chosen per-subnet state; a net-less daemon (endpoint bind
    // failed) serves nothing — "no connection", never conflated with a
    // deliberate detach (CONTEXT §subnet attachment).
    let net_up = spt_daemon::brain::Brain::cold_start(&spt_daemon::broker_socket_name(), now_ms())
        .ok()
        .and_then(|mut b| b.net_status().ok())
        .map(|s| s.enabled)
        .unwrap_or(true); // can't ask ⇒ assume serving rather than alarm
    let att = spt_store::attachment::AttachmentStore::load();
    let rows: Vec<(String, &'static str)> = SubnetStore::load()
        .subnets
        .iter()
        .map(|s| {
            let state = if att.is_detached(&s.name) {
                "detached"
            } else if net_up {
                "attached"
            } else {
                "no connection"
            };
            (s.name.clone(), state)
        })
        .collect();
    eprint!("{}", render_subnets_joined(&rows));
    // Blocked-inbound honesty on the coming-online banner too (M8 root
    // cause 3, REQ-INSTALL-7): a fresh user-scope install learns NOW, not
    // after a silent NO_SEED_HOLDER join failure.
    if let Some(w) = inbound_block_hint() {
        eprint!("{w}");
    }
    Ok(())
}

/// `spt subnet [status [NAME] [--nodes]]` — the membership view (REQ-SUBNET-1,
/// M7 plan decisions 5 + 6; M8 decisions 11–12, 23). Read-only and ungated:
/// prints membership shape, never secrets. `hints` = the bare-`spt subnet`
/// invocation (the footer rides only that view).
// [impl->REQ-SUBNET-1]
// [impl->REQ-SUBNET-8]
fn cmd_subnet_status(name: Option<String>, nodes: bool, hints: bool) -> i32 {
    let store = SubnetStore::load();


Found `probe_node` (single, bounded 2.5s). Now the serial caller loop:


2800	            &store,
2801	            &regs,
2802	            &roster,
2803	            Some(&own_hex).filter(|h| !h.is_empty()).map(|h| h.as_str()),
2804	            name.as_deref(),
2805	        );
2806	        let mut out = render_subnet_status(&rows, daemon_running, hints);
2807	        if let Some(w) = stall {
2808	            out.push_str(&w);
2809	        }
2810	        if let Some(w) = inbound {
2811	            out.push_str(&w);
2812	        }
2813	        print!("{out}");
2814	        let _ = std::io::stdout().flush();
2815	        return 0;
2816	    }
2817
2818	    // --nodes: per-node rows under the hybrid liveness (decision 6).
2819	    let heard: std::collections::BTreeMap<String, u64> = std::fs::read(
2820	        spt_daemon::registryhost::RegistryHost::heard_path(&snap_dir),
2821	    )
2822	    .ok()
2823	    .and_then(|b| serde_json::from_slice(&b).ok())
2824	    .unwrap_or_default();
2825	    // Stale = silent for 3 advertisement cadences (the daemon re-gossips
2826	    // every pump period; missing a couple is jitter, missing three is news).
2827	    let stale_after_ms = 3 * spt_daemon::DaemonConfig::load()
2828	        .registry_pump_period
2829	        .as_millis() as u64;
2830	    let mut out = String::new();
2831	    // Announce the probe phase once, before any dial — a silent node costs a
2832	    // bounded dial (probe_node) and the user should know what the wait is for.
2833	    let mut announced = false;
2834	    for sub in &store.subnets {
2835	        if name.as_deref().is_some_and(|n| n != sub.name) {
2836	            continue;
2837	        }
2838	        let empty = spt_net::net::registry::SubnetRegistry::new();
2839	        let reg = regs.get(&sub.name).unwrap_or(&empty);
2840	        // Membership seed: self + roster members (rowless members still
2841	        // render — fresh boxes and eviction-decayed offline peers alike).
2842	        let mut members: Vec<(String, Option<String>)> = Vec::new();
2843	        if !own_hex.is_empty() {
2844	            members.push((own_hex.clone(), spt_store::hostlabel::os_hostname()));
2845	        }
2846	        for member in roster.members_in(&sub.name) {
2847	            let label = (!member.label.is_empty()).then(|| member.label.clone());

            &store,
            &regs,
            &roster,
            Some(&own_hex).filter(|h| !h.is_empty()).map(|h| h.as_str()),
            name.as_deref(),
        );
        let mut out = render_subnet_status(&rows, daemon_running, hints);
        if let Some(w) = stall {
            out.push_str(&w);
        }
        if let Some(w) = inbound {
            out.push_str(&w);
        }
        print!("{out}");
        let _ = std::io::stdout().flush();
        return 0;
    }

    // --nodes: per-node rows under the hybrid liveness (decision 6).
    let heard: std::collections::BTreeMap<String, u64> = std::fs::read(
        spt_daemon::registryhost::RegistryHost::heard_path(&snap_dir),
    )
    .ok()
    .and_then(|b| serde_json::from_slice(&b).ok())
    .unwrap_or_default();
    // Stale = silent for 3 advertisement cadences (the daemon re-gossips
    // every pump period; missing a couple is jitter, missing three is news).
    let stale_after_ms = 3 * spt_daemon::DaemonConfig::load()
        .registry_pump_period
        .as_millis() as u64;
    let mut out = String::new();
    // Announce the probe phase once, before any dial — a silent node costs a
    // bounded dial (probe_node) and the user should know what the wait is for.
    let mut announced = false;
    for sub in &store.subnets {
        if name.as_deref().is_some_and(|n| n != sub.name) {
            continue;
        }
        let empty = spt_net::net::registry::SubnetRegistry::new();
        let reg = regs.get(&sub.name).unwrap_or(&empty);
        // Membership seed: self + roster members (rowless members still
        // render — fresh boxes and eviction-decayed offline peers alike).
        let mut members: Vec<(String, Option<String>)> = Vec::new();
        if !own_hex.is_empty() {
            members.push((own_hex.clone(), spt_store::hostlabel::os_hostname()));
        }
        for member in roster.members_in(&sub.name) {
            let label = (!member.label.is_empty()).then(|| member.label.clone());


2847	            let label = (!member.label.is_empty()).then(|| member.label.clone());
2848	            members.push((member.pubkey_hex.clone(), label));
2849	        }
2850	        let rows = node_status_rows(reg, &members, &heard, &own_hex, now_ms(), stale_after_ms);
2851	        if !announced && rows.iter().any(|r| r.liveness == NodeLiveness::Probe) {
2852	            eprintln!("Checking remote nodes…");
2853	            let _ = std::io::stderr().flush();
2854	            announced = true;
2855	        }
2856	        let settled: Vec<(NodeRow, bool)> = rows
2857	            .into_iter()
2858	            .map(|r| {
2859	                let online = match r.liveness {
2860	                    NodeLiveness::Online => true,
2861	                    NodeLiveness::Offline => false,
2862	                    NodeLiveness::Probe => probe_node(&r.node, &sub.name),
2863	                };
2864	                (r, online)
2865	            })
2866	            .collect();
2867	        out.push_str(&render_node_rows(&sub.name, &settled));
2868	    }
2869	    if out.is_empty() {
2870	        out.push_str(standalone_text(daemon_running));
2871	    }
2872	    if let Some(w) = stall {
2873	        out.push_str(&w);
2874	    }
2875	    if let Some(w) = inbound {
2876	        out.push_str(&w);
2877	    }
2878	    if hints {
2879	        out.push_str(HINT_FOOTER);
2880	    }
2881	    print!("{out}");
2882	    let _ = std::io::stdout().flush();
2883	    0
2884	}
2885
2886	/// The leave/prune elevation verdict (REQ-SUBNET-6): both verbs mutate
2887	/// trust material, the same class as join's machine enrollment — fail-safe,
2888	/// only confirmed elevation proceeds.
2889	// [impl->REQ-SUBNET-6]
2890	fn trust_mutation_refusal(elevation: Elevation, what: &str) -> Option<String> {
2891	    match elevation::gate_for(elevation) {
2892	        Gate::Show => None,
2893	        Gate::Fallback => Some(format!(
2894	            "ELEVATION_REQUIRED: {what} mutates this machine's trust material — re-run \
2895	             elevated (run as administrator / root)"
2896	        )),
2897	    }
2898	}

            let label = (!member.label.is_empty()).then(|| member.label.clone());
            members.push((member.pubkey_hex.clone(), label));
        }
        let rows = node_status_rows(reg, &members, &heard, &own_hex, now_ms(), stale_after_ms);
        if !announced && rows.iter().any(|r| r.liveness == NodeLiveness::Probe) {
            eprintln!("Checking remote nodes…");
            let _ = std::io::stderr().flush();
            announced = true;
        }
        let settled: Vec<(NodeRow, bool)> = rows
            .into_iter()
            .map(|r| {
                let online = match r.liveness {
                    NodeLiveness::Online => true,
                    NodeLiveness::Offline => false,
                    NodeLiveness::Probe => probe_node(&r.node, &sub.name),
                };
                (r, online)
            })
            .collect();
        out.push_str(&render_node_rows(&sub.name, &settled));
    }
    if out.is_empty() {
        out.push_str(standalone_text(daemon_running));
    }
    if let Some(w) = stall {
        out.push_str(&w);
    }
    if let Some(w) = inbound {
        out.push_str(&w);
    }
    if hints {
        out.push_str(HINT_FOOTER);
    }
    print!("{out}");
    let _ = std::io::stdout().flush();
    0
}

/// The leave/prune elevation verdict (REQ-SUBNET-6): both verbs mutate
/// trust material, the same class as join's machine enrollment — fail-safe,
/// only confirmed elevation proceeds.
// [impl->REQ-SUBNET-6]
fn trust_mutation_refusal(elevation: Elevation, what: &str) -> Option<String> {
    match elevation::gate_for(elevation) {
        Gate::Show => None,
        Gate::Fallback => Some(format!(
            "ELEVATION_REQUIRED: {what} mutates this machine's trust material — re-run \
             elevated (run as administrator / root)"
        )),
    }
}


Confirmed **serial** (line 2856-2866 sequential `.map` calling `probe_node`, each bounded 2.5s → k×2.5s wall). Let me check `run_bounded` to design the fan-out:


crates\spt\src\cli.rs:2651:fn run_bounded<T: Send + 'static>(
crates\spt\src\cli.rs-2652-    dur: std::time::Duration,
crates\spt\src\cli.rs-2653-    on_timeout: T,
crates\spt\src\cli.rs-2654-    f: impl FnOnce() -> T + Send + 'static,
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-2655-) -> T {
crates\spt\src\cli.rs-2656-    let (tx, rx) = std::sync::mpsc::channel();
crates\spt\src\cli.rs-2657-    std::thread::spawn(move || {
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-2658-        let _ = tx.send(f());
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-2659-    });
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-2660-    rx.recv_timeout(dur).unwrap_or(on_timeout)
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-2661-}
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-2662-
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-2663-/// Subnet-scoped liveness probe for one node, BOUNDED. The `--nodes` view
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-2664-/// dials this synchronously per silent node; an id-only dial to a dead peer
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-2665-/// otherwise runs the full iroh discovery+connect timeout (~30s, observed live
--
crates\spt\src\cli.rs:6942:    fn run_bounded_yields_value_or_timeout_default() {
crates\spt\src\cli.rs-6943-        use std::time::Duration;
crates\spt\src\cli.rs-6944-        assert_eq!(run_bounded(Duration::from_secs(5), 0, || 42), 42);
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-6945-        assert_eq!(
crates\spt\src\cli.rs-6946-            run_bounded(Duration::from_millis(50), -1, || {
crates\spt\src\cli.rs-6947-                std::thread::sleep(Duration::from_millis(800));
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-6948-                7
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-6949-            }),
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-6950-            -1,
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-6951-            "an op past the deadline reads as the timeout default"
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-6952-        );
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-6953-    }
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-6954-
crates\spt\src\cli.rs-6955-    \ [unit->REQ-SUBNET-3] label collision: two nodes sharing a hostname each
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs-6956-    // get the key-prefix disambiguator back (the addressing layer forces it
--
crates\spt\src\wansend.rs:420:pub fn probe_node_serving(node_hex: &str, subnet: &str) -> bool {
crates\spt\src\wansend.rs-421-    if spt_daemon::daemon::ensure_running().is_err() {
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-422-        return false;
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-423-    }
crates\spt\src\wansend.rs-424-    let Ok(mut brain) = Brain::cold_start(&broker_socket_name(), now_ms()) else {
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-425-        return false;
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-426-    };
crates\spt\src\wansend.rs-427-    let Some(addr) = addr_for_node_hex(node_hex).and_then(|a| serde_json::to_value(a).ok()) else {
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-428-        return false;
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-429-    };
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-430-    // Dial proves the box is reachable; the serve-probe refines it to "still
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-431-    // serving THIS subnet" (a detached peer dials fine but answers false).
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-432-    let Ok(dialed) = brain.net_dial(addr, None) else {
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-433-        return false;
C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs-434-    };
--
crates\spt-runtime\src\runtime.rs:186:    fn run_bounded_stdin(
C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\runtime.rs-187-        &self,
crates\spt-runtime\src\runtime.rs-188-        role: &str,
crates\spt-runtime\src\runtime.rs-189-        keys: &BTreeMap<String, String>,
crates\spt-runtime\src\runtime.rs-190-        timeout: Duration,
crates\spt-runtime\src\runtime.rs-191-        stdin: Option<&[u8]>,
C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\runtime.rs-192-    ) -> Result<BoundedOutput, RuntimeError>;
C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\runtime.rs-193-
crates\spt-runtime\src\runtime.rs-194-    \ Bounded role invocation with no stdin — sugar over [`Self::run_bounded_stdin`].
crates\spt-runtime\src\runtime.rs:195:    fn run_bounded(
C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\runtime.rs-196-        &self,
crates\spt-runtime\src\runtime.rs-197-        role: &str,
crates\spt-runtime\src\runtime.rs-198-        keys: &BTreeMap<String, String>,
crates\spt-runtime\src\runtime.rs-199-        timeout: Duration,
C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\runtime.rs-200-    ) -> Result<BoundedOutput, RuntimeError> {
C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\runtime.rs-201-        self.run_bounded_stdin(role, keys, timeout, None)
C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\runtime.rs-202-    }
C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\runtime.rs-203-}
C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\runtime.rs-204-
crates\spt-runtime\src\runtime.rs-205-\ Default [`AgentRuntime`]: substitution-fill a manifest role and spawn it as a
C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\runtime.rs-206-/// local OS process.
C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\runtime.rs-207-pub struct ManifestRuntime {
crates\spt-runtime\src\runtime.rs-208-    manifest: Manifest,
C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\runtime.rs-209-}
--
crates\spt-runtime\src\runtime.rs:284:    fn run_bounded_stdin(
C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\runtime.rs-285-        &self,
crates\spt-runtime\src\runtime.rs-286-        role: &str,
crates\spt-runtime\src\runtime.rs-287-        keys: &BTreeMap<String, String>,
crates\spt-runtime\src\runtime.rs-288-        timeout: Duration,
crates\spt-runtime\src\runtime.rs-289-        stdin: Option<&[u8]>,
C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\runtime.rs-290-    ) -> Result<BoundedOutput, RuntimeError> {
C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\runtime.rs-291-        let role_def = self.role(role)?;
C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\runtime.rs-292-        let mut cmd = self.command_for(role_def, keys)?;
C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\runtime.rs-293-        cmd.stdin(if stdin.is_some() {
crates\spt-runtime\src\runtime.rs-294-            Stdio::piped()
C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\runtime.rs-295-        } else {
crates\spt-runtime\src\runtime.rs-296-            Stdio::null()
C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\runtime.rs-297-        })
crates\spt-runtime\src\runtime.rs-298-        .stdout(Stdio::piped())
--
crates\spt-runtime\src\runtime.rs:384:pub fn run_bounded_command(
crates\spt-runtime\src\runtime.rs-385-    template: &str,
crates\spt-runtime\src\runtime.rs-386-    keys: &BTreeMap<String, String>,
crates\spt-runtime\src\runtime.rs-387-    timeout: Duration,
crates\spt-runtime\src\runtime.rs-388-    stdin: Option<&[u8]>,
C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\runtime.rs-389-) -> Result<BoundedOutput, RuntimeError> {
C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\runtime.rs-390-    let filled = fill_template(template, keys)?;
C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\runtime.rs-391-    let tokens = tokenize(&filled);
crates\spt-runtime\src\runtime.rs-392-    let (program, args) = tokens.split_first().ok_or(RuntimeError::EmptyCommand)?;
crates\spt-runtime\src\runtime.rs-393-    let mut cmd = Command::new(program);
C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\runtime.rs-394-    cmd.args(args)
C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\runtime.rs-395-        .stdin(if stdin.is_some() {
crates\spt-runtime\src\runtime.rs-396-            Stdio::piped()
C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\runtime.rs-397-        } else {
crates\spt-runtime\src\runtime.rs-398-            Stdio::null()


Confirmed serial probe loop. `run_bounded` = thread + `recv_timeout`; `probe_node_serving` cold-starts a brain + `net_dial` per probe (independently spawnable). I have what I need. Writing the D8 JIT plan:


Write

# Mesh-D8 — concurrent liveness probes (`subnet status --nodes`)

> JIT plan for `SUBNET-MESH-PLAN.md` §Build phases · phase 8. Activates **REQ-MESH-6** (impl, unit). Independent polish after D7 (no crypto, no wire change): the mesh widen (D6) makes a node see ALL members of a subnet, many offline — so the serial per-node probe loop now costs `offline_count × ceiling`. D8 fans the probes out so the wall-time is bounded by **one** single-probe ceiling, not `k × ceiling`.

## Pre-flight — verify the REQ-SUBNET-5 loop (done; it IS serial)

Confirmed by reading, not assumed:

- `cli.rs:2856–2866` (`cmd_subnet_status`, the `--nodes` arm): a **sequential** `rows.into_iter().map(|r| … probe_node(&r.node, &sub.name) …)`. Each `Probe`-state row blocks the next.
- `probe_node` (`cli.rs:2675`) wraps one probe in `run_bounded(PROBE_TIMEOUT=2500ms, false, …)` → `wansend::probe_node_serving` (`wansend.rs:420`): cold-starts a brain IPC connection and `net_dial`s the peer, then asks "serving `subnet`?". A dead id-only peer would otherwise hang the full iroh discovery timeout (~30s), so the 2.5s bound is load-bearing.
- `run_bounded` (`cli.rs:2651`) = spawn-thread + `recv_timeout`. So **one probe already runs on its own thread**; the serialization is purely the caller's `.map`, which joins each before starting the next.

So with `k` offline members the view waits `k × 2.5s` (the mesh makes `k` large — every roster member, online or not, that decays to `Probe`). REQ-MESH-6 is exactly this regression.

## The fix — fan out the probe phase

One pure-core helper that runs a batch of probes concurrently under a single ceiling, with the real probe injected (so the unit test injects a deterministic fake). Shape:

```rust
/// Run every (node, subnet) probe concurrently; collect results in input order.
/// Wall-time ≈ ONE `ceiling`, not k×ceiling. `probe` is injected (real:
/// probe_node_serving; test: a deterministic sleeper). Bounded concurrency so a
/// huge mesh can't spawn unbounded threads/broker conns.
fn probe_all<F>(items: &[(String, String)], ceiling: Duration, max_inflight: usize, probe: F) -> Vec<bool>
where F: Fn(&str, &str) -> bool + Send + Sync + Clone + 'static
```

- **Concurrency:** spawn one `std::thread` per probe (each does its own `recv_timeout`-bounded run, mirroring `run_bounded`), but cap in-flight at `max_inflight` (a small fixed pool, e.g. 16) so a 200-member subnet doesn't open 200 simultaneous broker IPC conns + dials. With a cap, wall ≈ `ceil(k / max_inflight) × ceiling`; for a normal fleet (`k ≤ cap`) that's one ceiling. **Log if `k > max_inflight`** (no silent cap — the wait is then a small multiple, and the user should know why).
- **Ordering:** carry the input index through, so results map back to rows deterministically (the render order is stable).
- **Each thread is hard-bounded** by `ceiling` (its own `recv_timeout`), so one wedged dial costs one ceiling, never the batch — KH 5.3 (every subprocess/dial bounded) preserved.

### Caller change (`cmd_subnet_status` `--nodes` arm)

Two passes instead of the inline `.map`:
1. Build `rows` per subnet as today (`node_status_rows`); collect the `Probe`-state ones into a flat `Vec<(node, subnet)>` (the work-list), remembering each row's place. `Online`/`Offline` rows need no probe — settle them directly.
2. One `probe_all(&worklist, PROBE_TIMEOUT, MAX_INFLIGHT, |n, s| probe_node_serving(n, s))` call across **all** subnets at once (not per-subnet) — the fan-out spans the whole view, so a multi-subnet status pays one ceiling total, not one per subnet.
3. Stitch results back into `(NodeRow, bool)` settled rows; render unchanged (`render_node_rows`).

The "Checking remote nodes…" announce stays (fire once before the batch when any row is `Probe`).

Keep `probe_node` for any remaining single-probe site (and the existing test); `probe_all` becomes the batch path the `--nodes` view uses.

## Tests

- **unit (REQ-MESH-6):** the timing fact via a **deterministic injected probe** (no real network — the mock-clock seam the reg note names). Inject `probe = |_, _| { sleep(D); true }` for `N` items; assert the batch completes in **well under `N × D`** (≈ one `D` plus scheduling slop) — e.g. `N=8, D=200ms`: assert elapsed `< 4 × D` (loose enough for CI jitter, tight enough to prove it isn't serial `8 × D = 1600ms`). Assert results come back in input order and all `true`.
  - Plus a **cap** unit: `N=20, max_inflight=4, D=100ms` → elapsed in `[~5×D, ~8×D]` (proves the cap batches rather than either serializing fully or ignoring the bound), and the over-cap path is logged.
  - Plus a **timeout** unit: a probe that sleeps `> ceiling` settles `false` (its `recv_timeout` default) without dragging the batch past one ceiling.
- **Regression:** the existing `--nodes` rendering + `probe_node`/`run_bounded` tests stay green; `node_status_rows` (the pure liveness classifier) is untouched; a status view with mixed Online/Offline/Probe rows renders the same settled output as the serial path would (order-stable).
- `traceable-reqs check` clean with **REQ-MESH-6 `[impl, unit]`**; clippy + `--no-default-features` + workspace green; docs-drift gate (run `xtask gen` only if `status --help` text changed — D8 changes no flags, so likely a no-op, but check).

## Done when

- `spt subnet status --nodes` probes every silent member **concurrently**: a view with `k` offline members settles in ≈ one `PROBE_TIMEOUT` (bounded by `max_inflight`), not `k × PROBE_TIMEOUT`; verified by the injected-probe timing unit.
- Each probe stays individually hard-bounded (one wedged dial = one ceiling, never the batch); the cap is logged when exceeded (no silent truncation of the wait).
- Render output is identical to the serial path (order-stable, same Online/Offline/Probe→bool settling); existing tests green.
- `traceable-reqs check` clean with **REQ-MESH-6 `[impl, unit]`** activated; clippy / `--no-default-features` / docs-drift green.
- One commit: `perf(mesh): concurrent subnet status --nodes liveness probes (REQ-MESH-6) — Mesh-D8`.

## Explicitly NOT D8 (carry forward)

- Cross-node un-tombstone convergence beyond local clear-on-re-pair (D3 boundary; revisit only on a field hit).
- Multi-user `(subnet, user)` revocation scoping (the seam exists; the cross-user milestone owns it).
- Force-drop of a revokee's lingering live conn (D7 deferred; tombstone+rotation already lock out — revisit only if a field hit demands prompt teardown).
- Any async-runtime adoption for the probe fan-out — threads + the existing `run_bounded` discipline are sufficient; do not pull tokio into the CLI for this.