866	        )
867	        .await
868	        .expect("initiator pairs");
869	        conn.close(0u32.into(), b"done");
870
871	        let (resp_trust, resp_outcome) = resp_task.await.expect("responder task");
872
873	        assert_eq!(
874	            init_outcome,
875	            TrustOutcome::Pinned,
876	            "initiator pins responder"
877	        );
878	        assert_eq!(
879	            resp_outcome,
880	            TrustOutcome::Pinned,
881	            "responder pins initiator"
882	        );
883	        // Each side trusts the OTHER's real pubkey within the subnet.
884	        assert!(init_trust.is_trusted("home", &resp_pub.to_hex()));
885	        assert!(resp_trust.is_trusted("home", &init_pub.to_hex()));
886	        assert!(
887	            !init_trust.is_trusted("home", &init_pub.to_hex()),
888	            "self not pinned"
889	        );
890	        // [unit->REQ-PAIR-5] the joiner received the subnet seed (it is now a full member /
891	        // future seed-holder): same name, seed, and the responder's epoch (3).
892	        let joined = init_subnets.find("home").expect("joiner gained the subnet");
893	        assert_eq!(joined.seed_bytes(), Some(seed), "seed transferred intact");
894	        assert_eq!(joined.epoch, 3, "epoch transferred");
895	    }

        )
        .await
        .expect("initiator pairs");
        conn.close(0u32.into(), b"done");

        let (resp_trust, resp_outcome) = resp_task.await.expect("responder task");

        assert_eq!(
            init_outcome,
            TrustOutcome::Pinned,
            "initiator pins responder"
        );
        assert_eq!(
            resp_outcome,
            TrustOutcome::Pinned,
            "responder pins initiator"
        );
        // Each side trusts the OTHER's real pubkey within the subnet.
        assert!(init_trust.is_trusted("home", &resp_pub.to_hex()));
        assert!(resp_trust.is_trusted("home", &init_pub.to_hex()));
        assert!(
            !init_trust.is_trusted("home", &init_pub.to_hex()),
            "self not pinned"
        );
        // [unit->REQ-PAIR-5] the joiner received the subnet seed (it is now a full member /
        // future seed-holder): same name, seed, and the responder's epoch (3).
        let joined = init_subnets.find("home").expect("joiner gained the subnet");
        assert_eq!(joined.seed_bytes(), Some(seed), "seed transferred intact");
        assert_eq!(joined.epoch, 3, "epoch transferred");
    }


resources --> new flag under `spt endpoint list`: `--detail` --- change the `spt subnet attach/detach --auto` flag to `--save`. clearer semantics.


Update CLI flags for endpoint and subnet commands


Edit

        assert_eq!(
            resp_outcome,
            TrustOutcome::Pinned,
            "responder pins initiator"
        );
        // Each side trusts the OTHER's real pubkey within the subnet.
        assert!(init_trust.is_trusted("home", &resp_pub.to_hex()));

        assert_eq!(
            resp_outcome.trust,
            TrustOutcome::Pinned,
            "responder pins initiator"
        );
        // The outcome reports the ceremony facts the M8 re-pair overwrite
        // consumes: the joiner's authenticated identity + the subnet. No
        // intro was sent on this pre-M8-shaped call.
        assert_eq!(resp_outcome.joiner_hex, init_pub.to_hex());
        assert_eq!(resp_outcome.subnet, "home");
        assert!(resp_outcome.intro.is_none(), "no intro sent");
        // Each side trusts the OTHER's real pubkey within the subnet.
        assert!(init_trust.is_trusted("home", &resp_pub.to_hex()));


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

        assert_eq!(
            resp_outcome,
            TrustOutcome::Pinned,
            "responder pins initiator"
        );
        // Each side trusts the OTHER's real pubkey within the subnet.
        assert!(init_trust.is_trusted("home", &resp_pub.to_hex()));

        assert_eq!(
            resp_outcome.trust,
            TrustOutcome::Pinned,
            "responder pins initiator"
        );
        // The outcome reports the ceremony facts the M8 re-pair overwrite
        // consumes: the joiner's authenticated identity + the subnet. No
        // intro was sent on this pre-M8-shaped call.
        assert_eq!(resp_outcome.joiner_hex, init_pub.to_hex());
        assert_eq!(resp_outcome.subnet, "home");
        assert!(resp_outcome.intro.is_none(), "no intro sent");
        // Each side trusts the OTHER's real pubkey within the subnet.
        assert!(init_trust.is_trusted("home", &resp_pub.to_hex()));

+        // The outcome reports the ceremony facts the M8 re-pair overwrite

+        // consumes: the joiner's authenticated identity + the subnet. No

+        // intro was sent on this pre-M8-shaped call.

+        assert_eq!(resp_outcome.joiner_hex, init_pub.to_hex());

+        assert_eq!(resp_outcome.subnet, "home");

+        assert!(resp_outcome.intro.is_none(), "no intro sent");


1255	        let init_outcome = run_initiator(
1256	            &conn,
1257	            init_pub,
1258	            "home",
1259	            &code,
1260	            &mut init_trust,
1261	            &mut init_subnets,
1262	            NOW,
1263	            0, // step_back: the normal attempt
1264	            None, // intro: pre-M8 joiner shape
1265	        )
1266	        .await
1267	        .expect("re-pair succeeds");
1268	        conn.close(0u32.into(), b"done");
1269	        let resp_outcome = resp_task.await.expect("responder task");
1270
1271	        assert_eq!(
1272	            init_outcome,
1273	            TrustOutcome::AlreadyTrusted,
1274	            "idempotent trust"
1275	        );
1276	        assert_eq!(
1277	            resp_outcome,
1278	            TrustOutcome::Pinned,
1279	            "responder's first sight of joiner"
1280	        );
1281	        // The pre-existing seed record is untouched (Exists ignored, not overwritten).
1282	        let kept = init_subnets.find("home").expect("still a member");
1283	        assert_eq!(kept.seed_bytes(), Some(existing_seed), "existing seed kept");
1284	        assert_eq!(kept.epoch, 7, "existing epoch kept");
1285	    }
1286
1287	    // [unit->REQ-PAIR-1] the frame codec round-trips every variant and rejects a
1288	    // truncated body without panicking (the codec is pre-trust attacker-reachable).
1289	    #[test]
1290	    fn frame_codec_round_trips_and_rejects_garbage() {
1291	        let cases = [
1292	            enc_hello("home fleet", 57_037_037),
1293	            enc_announce(42),
1294	            enc_spake1(b"msg-a-bytes"),
1295	            enc_spake2(b"msg-b", &[7u8; TAG_LEN]),
1296	            enc_confirm(&[9u8; TAG_LEN]),
1297	            enc_done(true),
1298	            enc_done(false),
1299	            enc_seed(&[5u8; TOTP_SEED_LEN], 9),
1300	            enc_abort(A_BACKOFF, 17),
1301	        ];
1302	        for body in cases {
1303	            decode_frame(&body).expect("valid frame decodes");
1304	        }
1305	        // A Hello claiming an 8-byte step but missing it is truncated, not a panic.
1306	        let mut bad = vec![T_HELLO];
1307	        push_field(&mut bad, b"home");
1308	        bad.extend_from_slice(&[0u8; 3]); // only 3 of the 8 step bytes
1309	        assert!(matches!(
1310	            decode_frame(&bad),
1311	            Err(PairWireError::Protocol(_))
1312	        ));
1313	        // Unknown tag is rejected.
1314	        assert!(matches!(

        let init_outcome = run_initiator(
            &conn,
            init_pub,
            "home",
            &code,
            &mut init_trust,
            &mut init_subnets,
            NOW,
            0, // step_back: the normal attempt
            None, // intro: pre-M8 joiner shape
        )
        .await
        .expect("re-pair succeeds");
        conn.close(0u32.into(), b"done");
        let resp_outcome = resp_task.await.expect("responder task");

        assert_eq!(
            init_outcome,
            TrustOutcome::AlreadyTrusted,
            "idempotent trust"
        );
        assert_eq!(
            resp_outcome,
            TrustOutcome::Pinned,
            "responder's first sight of joiner"
        );
        // The pre-existing seed record is untouched (Exists ignored, not overwritten).
        let kept = init_subnets.find("home").expect("still a member");
        assert_eq!(kept.seed_bytes(), Some(existing_seed), "existing seed kept");
        assert_eq!(kept.epoch, 7, "existing epoch kept");
    }

    // [unit->REQ-PAIR-1] the frame codec round-trips every variant and rejects a
    // truncated body without panicking (the codec is pre-trust attacker-reachable).
    #[test]
    fn frame_codec_round_trips_and_rejects_garbage() {
        let cases = [
            enc_hello("home fleet", 57_037_037),
            enc_announce(42),
            enc_spake1(b"msg-a-bytes"),
            enc_spake2(b"msg-b", &[7u8; TAG_LEN]),
            enc_confirm(&[9u8; TAG_LEN]),
            enc_done(true),
            enc_done(false),
            enc_seed(&[5u8; TOTP_SEED_LEN], 9),
            enc_abort(A_BACKOFF, 17),
        ];
        for body in cases {
            decode_frame(&body).expect("valid frame decodes");
        }
        // A Hello claiming an 8-byte step but missing it is truncated, not a panic.
        let mut bad = vec![T_HELLO];
        push_field(&mut bad, b"home");
        bad.extend_from_slice(&[0u8; 3]); // only 3 of the 8 step bytes
        assert!(matches!(
            decode_frame(&bad),
            Err(PairWireError::Protocol(_))
        ));
        // Unknown tag is rejected.
        assert!(matches!(


        assert_eq!(
            resp_outcome,
            TrustOutcome::Pinned,
            "responder's first sight of joiner"
        );

        assert_eq!(
            resp_outcome.trust,
            TrustOutcome::Pinned,
            "responder's first sight of joiner"
        );


        assert_eq!(
            resp_outcome,
            TrustOutcome::Pinned,
            "responder's first sight of joiner"
        );

        assert_eq!(
            resp_outcome.trust,
            TrustOutcome::Pinned,
            "responder's first sight of joiner"
        );


        let cases = [
            enc_hello("home fleet", 57_037_037),
            enc_announce(42),

        let cases = [
            enc_hello("home fleet", 57_037_037, None),
            // The M8 intro-carrying Hello round-trips too.
            enc_hello(
                "home fleet",
                57_037_037,
                Some(&NodeIntro {
                    label: "HFENDULEAM".into(),
                    machine_id: "ab".repeat(32),
                }),
            ),
            enc_announce(42),


        let cases = [
            enc_hello("home fleet", 57_037_037),
            enc_announce(42),

        let cases = [
            enc_hello("home fleet", 57_037_037, None),
            // The M8 intro-carrying Hello round-trips too.
            enc_hello(
                "home fleet",
                57_037_037,
                Some(&NodeIntro {
                    label: "HFENDULEAM".into(),
                    machine_id: "ab".repeat(32),
                }),
            ),
            enc_announce(42),

+            enc_hello("home fleet", 57_037_037, None),

+            // The M8 intro-carrying Hello round-trips too.

+                    label: "HFENDULEAM".into(),

+                    machine_id: "ab".repeat(32),


Ratify M8-PLAN.md — fold A–I resolutions in, status DRAFT→RATIFIED

Move resolved decisions A-G + new H/I into ratified table; update D1-D5 deliverable text (NTP, debug-converge, peer-addrs.json, pump liveness, hazard entry); update acceptance; commit.

Mint M8 requirements in traceable-reqs.toml (rule 3)

REQ-CLI-* (endpoint noun, daemon noun, hot-path), REQ-SUBNET-* (attach/detach, prune, re-pair overwrite, status render), REQ-INSTALL-* (Linux elevation leg, firewall, OS-service), NTP/TOTP offset, pump liveness, addr seeding, event advertisement, debug-converge activation (REQ-UPD-6 int), epoch-reset REQ-HAZARD-* (stages=[]). Activate only what D1 starts; rest staged per rule 5.

D1 — endpoint + daemon nouns (breaking CLI reorg)

Namespace skeletons; move fork/suspend/wake/shutdown/rename/stop/digest + access (1:1, decision F) + description under endpoint; merged endpoint list (SELF pinned, subnet-grouped); notify → subnet notify; daemon run|stop|status (status renders pump heartbeat per H); help groups; how-to topics; cli/reference.md regen.

D2 — subnet serve-state, trust lifecycle, status UX

attach/detach machinery + --auto persistence; subnet leave (elevation); subnet prune (elevation, BIGNET 09ef831e fixture); re-pair trust overwrite (machine id per G: OS id hashed, UUID fallback; additive serde field); status render fixes (decisions 11/12). Disband deferred (A).

D3 — install & elevation hardening

Linux: install.sh symlink to /usr/local/bin + first-sudo account election + KH 5.7 verify. Windows: firewall rule (elevated leg) + blocked-inbound self-detection render. OS-service per B: systemd user service + enable-linger; Windows scheduled task at-logon. E2E rungs env-gated.

D4 — convergence polish + pump resilience

peer-addrs.json seeding per E/14 (both cold starts, id-only fallback); event-driven advertisement (decision 15); NTP TOTP-offset per C/C2 (ceremony-time, system-clock fallback); pump supervision per H (panic catch + capped-backoff restart, heartbeat file); xtask debug-converge watcher per D (REQ-UPD-6 int).

D5 — docs + CI + acceptance prep

Docs-site verb sweep; quickstart/how-to deltas; README; llms.txt; reference regen; KNOWN-HAZARDS epoch-reset entry per I; quickstart_e2e + twohost ladder updates; traceable-reqs check + xtask check green.


    // [unit->REQ-PAIR-1] the frame codec round-trips every variant and rejects a
    // truncated body without panicking (the codec is pre-trust attacker-reachable).
    #[test]
    fn frame_codec_round_trips_and_rejects_garbage() {

    // [unit->REQ-SUBNET-7] the Hello intro is additive on the wire: absent
    // decodes None (the pre-M8 joiner), present round-trips both fields,
    // and a HALF-written intro is a protocol error — never a half-claim.
    #[test]
    fn hello_intro_is_additive_and_roundtrips() {
        // Absent → None.
        match decode_frame(&enc_hello("home", 7, None)).expect("decodes") {
            Frame::Hello { intro, .. } => assert!(intro.is_none()),
            _ => panic!("expected Hello"),
        }
        // Present → both fields intact.
        let sent = NodeIntro {
            label: "KITSUBITO".into(),
            machine_id: "cd".repeat(32),
        };
        match decode_frame(&enc_hello("home", 7, Some(&sent))).expect("decodes") {
            Frame::Hello {
                subnet_name,
                step,
                intro,
            } => {
                assert_eq!(subnet_name, "home");
                assert_eq!(step, 7);
                assert_eq!(intro, Some(sent));
            }
            _ => panic!("expected Hello"),
        }
        // Half an intro (label field only) refuses, never half-claims.
        let mut half = vec![T_HELLO];
        push_field(&mut half, b"home");
        half.extend_from_slice(&7u64.to_be_bytes());
        push_field(&mut half, b"KITSUBITO");
        assert!(matches!(
            decode_frame(&half),
            Err(PairWireError::Protocol(_))
        ));
    }

    // [unit->REQ-PAIR-1] the frame codec round-trips every variant and rejects a
    // truncated body without panicking (the codec is pre-trust attacker-reachable).
    #[test]
    fn frame_codec_round_trips_and_rejects_garbage() {


    // [unit->REQ-SUBNET-7] the Hello intro is additive on the wire: absent
    // decodes None (the pre-M8 joiner), present round-trips both fields,
    // and a HALF-written intro is a protocol error — never a half-claim.
    #[test]
    fn hello_intro_is_additive_and_roundtrips() {
        // Absent → None.
        match decode_frame(&enc_hello("home", 7, None)).expect("decodes") {
            Frame::Hello { intro, .. } => assert!(intro.is_none()),
            _ => panic!("expected Hello"),
        }
        // Present → both fields intact.
        let sent = NodeIntro {
            label: "KITSUBITO".into(),
            machine_id: "cd".repeat(32),
        };
        match decode_frame(&enc_hello("home", 7, Some(&sent))).expect("decodes") {
            Frame::Hello {
                subnet_name,
                step,
                intro,
            } => {
                assert_eq!(subnet_name, "home");
                assert_eq!(step, 7);
                assert_eq!(intro, Some(sent));
            }
            _ => panic!("expected Hello"),
        }
        // Half an intro (label field only) refuses, never half-claims.
        let mut half = vec![T_HELLO];
        push_field(&mut half, b"home");
        half.extend_from_slice(&7u64.to_be_bytes());
        push_field(&mut half, b"KITSUBITO");
        assert!(matches!(
            decode_frame(&half),
            Err(PairWireError::Protocol(_))
        ));
    }

    // [unit->REQ-PAIR-1] the frame codec round-trips every variant and rejects a
    // truncated body without panicking (the codec is pre-trust attacker-reachable).
    #[test]
    fn frame_codec_round_trips_and_rejects_garbage() {

assert_eq!(kept.epoch, 7, "existing epoch kept");

+    // [unit->REQ-SUBNET-7] the Hello intro is additive on the wire: absent

+    // decodes None (the pre-M8 joiner), present round-trips both fields,

+    // and a HALF-written intro is a protocol error — never a half-claim.

+    fn hello_intro_is_additive_and_roundtrips() {

+        match decode_frame(&enc_hello("home", 7, None)).expect("decodes") {

+            Frame::Hello { intro, .. } => assert!(intro.is_none()),

+            _ => panic!("expected Hello"),

+        // Present → both fields intact.

+            machine_id: "cd".repeat(32),

+        match decode_frame(&enc_hello("home", 7, Some(&sent))).expect("decodes") {

+                assert_eq!(subnet_name, "home");

+                assert_eq!(intro, Some(sent));

+        // Half an intro (label field only) refuses, never half-claims.

+        push_field(&mut half, b"home");

+        half.extend_from_slice(&7u64.to_be_bytes());

+        push_field(&mut half, b"KITSUBITO");

+            Err(PairWireError::Protocol(_))


PowerShell

cargo test -q -p spt-net pairing::wire 2>&1 | Select-Object -Last 6

Run wire tests


running 9 tests
.........
test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 93 filtered out; finished in 0.20s


250	/// real endpoint, and persist the results (peer trust-pinned; the
251	/// transferred seed makes this node a full member / future seed-holder).
252	/// One attempt — the CLI owns the re-prompt loop.
253	// [impl->REQ-SUBNET-2]
254	pub async fn join_ceremony(
255	    endpoint: &NetEndpoint,
256	    subnet_name: &str,
257	    code: &str,
258	    responder_addr: Option<spt_net::net::endpoint::EndpointAddr>,
259	) -> Result<String, JoinFail> {
260	    let mut trust = TrustStore::load();
261	    let mut subnets = SubnetStore::load();
262	    let conn = connect_seed_holder(endpoint, subnet_name, responder_addr.clone()).await?;
263	    let attempt = tokio::time::timeout(
264	        CEREMONY_TIMEOUT,
265	        run_initiator(
266	            &conn,
267	            endpoint.node_id(),
268	            subnet_name,
269	            code,
270	            &mut trust,
271	            &mut subnets,
272	            now_secs(),
273	            0,
274	        ),
275	    )
276	    .await
277	    .map_err(|_| JoinFail::Other(format!("ceremony exceeded {CEREMONY_TIMEOUT_SECS}s")))?;
278	    let outcome = match attempt {
279	        Ok(o) => o,
280	        // The LATE-CODE retry (field report 2026-06-06): the joiner holds no
281	        // seed, so a code read just before a rotation and entered just after
282	        // binds to the wrong step and reads as "wrong code" — before
283	        // bouncing that back at the user, retry the SAME code announced one
284	        // step back (the responder's ±1 window exists for exactly this
285	        // tolerance). The responder runs one ceremony per connection, so the
286	        // retry redials a FRESH one, after the limiter's first-failure
287	        // backoff (1s) has lapsed. Any retry failure surfaces the FIRST
288	        // failure's class — the retry was a guess on the user's behalf, not
289	        // a second user attempt.
290	        Err(PairWireError::Pairing(first)) => {
291	            let first_fail = classify_wire_error(PairWireError::Pairing(first));
292	            drop(conn);
293	            tokio::time::sleep(Duration::from_millis(1_200)).await;
294	            let Ok(retry_conn) = connect_seed_holder(endpoint, subnet_name, responder_addr).await
295	            else {
296	                return Err(first_fail);
297	            };
298	            match tokio::time::timeout(
299	                CEREMONY_TIMEOUT,
300	                run_initiator(
301	                    &retry_conn,
302	                    endpoint.node_id(),
303	                    subnet_name,
304	                    code,
305	                    &mut trust,
306	                    &mut subnets,
307	                    now_secs(),
308	                    1,
309	                ),
310	            )
311	            .await
312	            {
313	                Ok(Ok(o)) => {
314	                    eprintln!("(code had just rotated — accepted against the previous step)");
315	                    o
316	                }
317	                _ => return Err(first_fail),
318	            }
319	        }
320	        Err(e) => return Err(classify_wire_error(e)),
321	    };
322	    trust
323	        .save()
324	        .map_err(|e| JoinFail::Other(format!("trust save: {e}")))?;
325	    subnets
326	        .save()
327	        .map_err(|e| JoinFail::Other(format!("seed save: {e}")))?;
328	    Ok(format!("{outcome:?}"))
329	}
330
331	/// Reach a seed-holder: dial `responder_addr` directly when given (the
332	/// static-`ip:port` rig seam), else sweep the rendezvous until the search
333	/// deadline — one sweep races the ±1 window's tokens (bounded probes), and a
334	/// sweep that straddles a rotation simply misses; the next one derives fresh
335	/// tokens. ~2.5 windows of trying before the NoSeedHolder verdict (M7
336	/// acceptance fix).
337	// [impl->REQ-SUBNET-2]
338	async fn connect_seed_holder(
339	    endpoint: &NetEndpoint,
340	    subnet_name: &str,
341	    responder_addr: Option<spt_net::net::endpoint::EndpointAddr>,
342	) -> Result<Connection, JoinFail> {
343	    match responder_addr {
344	        Some(addr) => endpoint
345	            .connect_with_alpn(addr, spt_net::net::pairing::wire::SPT_PAIR_ALPN)
346	            .await
347	            .map_err(|e| JoinFail::NoSeedHolder(e.to_string())),
348	        None => {
349	            const SEARCH_DEADLINE: Duration = Duration::from_secs(75);
350	            let start = std::time::Instant::now();
351	            loop {
352	                match dial_via_rendezvous(endpoint, subnet_name, time_step(now_secs())).await {
353	                    Ok(conn) => return Ok(conn),
354	                    Err(e) if start.elapsed() >= SEARCH_DEADLINE => {
355	                        return Err(JoinFail::NoSeedHolder(e.to_string()));
356	                    }
357	                    Err(_) => tokio::time::sleep(Duration::from_millis(500)).await,
358	                }
359	            }

/// real endpoint, and persist the results (peer trust-pinned; the
/// transferred seed makes this node a full member / future seed-holder).
/// One attempt — the CLI owns the re-prompt loop.
// [impl->REQ-SUBNET-2]
pub async fn join_ceremony(
    endpoint: &NetEndpoint,
    subnet_name: &str,
    code: &str,
    responder_addr: Option<spt_net::net::endpoint::EndpointAddr>,
) -> Result<String, JoinFail> {
    let mut trust = TrustStore::load();
    let mut subnets = SubnetStore::load();
    let conn = connect_seed_holder(endpoint, subnet_name, responder_addr.clone()).await?;
    let attempt = tokio::time::timeout(
        CEREMONY_TIMEOUT,
        run_initiator(
            &conn,
            endpoint.node_id(),
            subnet_name,
            code,
            &mut trust,
            &mut subnets,
            now_secs(),
            0,
        ),
    )
    .await
    .map_err(|_| JoinFail::Other(format!("ceremony exceeded {CEREMONY_TIMEOUT_SECS}s")))?;
    let outcome = match attempt {
        Ok(o) => o,
        // The LATE-CODE retry (field report 2026-06-06): the joiner holds no
        // seed, so a code read just before a rotation and entered just after
        // binds to the wrong step and reads as "wrong code" — before
        // bouncing that back at the user, retry the SAME code announced one
        // step back (the responder's ±1 window exists for exactly this
        // tolerance). The responder runs one ceremony per connection, so the
        // retry redials a FRESH one, after the limiter's first-failure
        // backoff (1s) has lapsed. Any retry failure surfaces the FIRST
        // failure's class — the retry was a guess on the user's behalf, not
        // a second user attempt.
        Err(PairWireError::Pairing(first)) => {
            let first_fail = classify_wire_error(PairWireError::Pairing(first));
            drop(conn);
            tokio::time::sleep(Duration::from_millis(1_200)).await;
            let Ok(retry_conn) = connect_seed_holder(endpoint, subnet_name, responder_addr).await
            else {
                return Err(first_fail);
            };
            match tokio::time::timeout(
                CEREMONY_TIMEOUT,
                run_initiator(
                    &retry_conn,
                    endpoint.node_id(),
                    subnet_name,
                    code,
                    &mut trust,
                    &mut subnets,
                    now_secs(),
                    1,
                ),
            )
            .await
            {
                Ok(Ok(o)) => {
                    eprintln!("(code had just rotated — accepted against the previous step)");
                    o
                }
                _ => return Err(first_fail),
            }
        }
        Err(e) => return Err(classify_wire_error(e)),
    };
    trust
        .save()
        .map_err(|e| JoinFail::Other(format!("trust save: {e}")))?;
    subnets
        .save()
        .map_err(|e| JoinFail::Other(format!("seed save: {e}")))?;
    Ok(format!("{outcome:?}"))
}

/// Reach a seed-holder: dial `responder_addr` directly when given (the
/// static-`ip:port` rig seam), else sweep the rendezvous until the search
/// deadline — one sweep races the ±1 window's tokens (bounded probes), and a
/// sweep that straddles a rotation simply misses; the next one derives fresh
/// tokens. ~2.5 windows of trying before the NoSeedHolder verdict (M7
/// acceptance fix).
// [impl->REQ-SUBNET-2]
async fn connect_seed_holder(
    endpoint: &NetEndpoint,
    subnet_name: &str,
    responder_addr: Option<spt_net::net::endpoint::EndpointAddr>,
) -> Result<Connection, JoinFail> {
    match responder_addr {
        Some(addr) => endpoint
            .connect_with_alpn(addr, spt_net::net::pairing::wire::SPT_PAIR_ALPN)
            .await
            .map_err(|e| JoinFail::NoSeedHolder(e.to_string())),
        None => {
            const SEARCH_DEADLINE: Duration = Duration::from_secs(75);
            let start = std::time::Instant::now();
            loop {
                match dial_via_rendezvous(endpoint, subnet_name, time_step(now_secs())).await {
                    Ok(conn) => return Ok(conn),
                    Err(e) if start.elapsed() >= SEARCH_DEADLINE => {
                        return Err(JoinFail::NoSeedHolder(e.to_string()));
                    }
                    Err(_) => tokio::time::sleep(Duration::from_millis(500)).await,
                }
            }


Edit

pub async fn join_ceremony(
    endpoint: &NetEndpoint,
    subnet_name: &str,
    code: &str,
    responder_addr: Option<spt_net::net::endpoint::EndpointAddr>,
) -> Result<String, JoinFail> {
    let mut trust = TrustStore::load();
    let mut subnets = SubnetStore::load();
    let conn = connect_seed_holder(endpoint, subnet_name, responder_addr.clone()).await?;
    let attempt = tokio::time::timeout(
        CEREMONY_TIMEOUT,
        run_initiator(
            &conn,
            endpoint.node_id(),
            subnet_name,
            code,
            &mut trust,
            &mut subnets,
            now_secs(),
            0,
        ),
    )
    .await

pub async fn join_ceremony(
    endpoint: &NetEndpoint,
    subnet_name: &str,
    code: &str,
    responder_addr: Option<spt_net::net::endpoint::EndpointAddr>,
) -> Result<String, JoinFail> {
    let mut trust = TrustStore::load();
    let mut subnets = SubnetStore::load();
    // The joiner's self-intro (M8 decision 13, REQ-SUBNET-7): label +
    // hashed machine id — what lets the seed-holder recognize this MACHINE
    // re-pairing under a fresh key and evict the superseded identity.
    // Best-effort: a label-less or id-less box just sends none (no
    // overwrite; manual prune covers it).
    // [impl->REQ-SUBNET-7]
    let intro = match (
        spt_store::hostlabel::os_hostname(),
        crate::machineid::machine_id_hash(),
    ) {
        (Some(label), Some(machine_id)) => Some(spt_net::net::pairing::wire::NodeIntro {
            label,
            machine_id,
        }),
        _ => None,
    };
    let conn = connect_seed_holder(endpoint, subnet_name, responder_addr.clone()).await?;
    let attempt = tokio::time::timeout(
        CEREMONY_TIMEOUT,
        run_initiator(
            &conn,
            endpoint.node_id(),
            subnet_name,
            code,
            &mut trust,
            &mut subnets,
            now_secs(),
            0,
            intro.as_ref(),
        ),
    )
    .await


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

pub async fn join_ceremony(
    endpoint: &NetEndpoint,
    subnet_name: &str,
    code: &str,
    responder_addr: Option<spt_net::net::endpoint::EndpointAddr>,
) -> Result<String, JoinFail> {
    let mut trust = TrustStore::load();
    let mut subnets = SubnetStore::load();
    let conn = connect_seed_holder(endpoint, subnet_name, responder_addr.clone()).await?;
    let attempt = tokio::time::timeout(
        CEREMONY_TIMEOUT,
        run_initiator(
            &conn,
            endpoint.node_id(),
            subnet_name,
            code,
            &mut trust,
            &mut subnets,
            now_secs(),
            0,
        ),
    )
    .await

pub async fn join_ceremony(
    endpoint: &NetEndpoint,
    subnet_name: &str,
    code: &str,
    responder_addr: Option<spt_net::net::endpoint::EndpointAddr>,
) -> Result<String, JoinFail> {
    let mut trust = TrustStore::load();
    let mut subnets = SubnetStore::load();
    // The joiner's self-intro (M8 decision 13, REQ-SUBNET-7): label +
    // hashed machine id — what lets the seed-holder recognize this MACHINE
    // re-pairing under a fresh key and evict the superseded identity.
    // Best-effort: a label-less or id-less box just sends none (no
    // overwrite; manual prune covers it).
    // [impl->REQ-SUBNET-7]
    let intro = match (
        spt_store::hostlabel::os_hostname(),
        crate::machineid::machine_id_hash(),
    ) {
        (Some(label), Some(machine_id)) => Some(spt_net::net::pairing::wire::NodeIntro {
            label,
            machine_id,
        }),
        _ => None,
    };
    let conn = connect_seed_holder(endpoint, subnet_name, responder_addr.clone()).await?;
    let attempt = tokio::time::timeout(
        CEREMONY_TIMEOUT,
        run_initiator(
            &conn,
            endpoint.node_id(),
            subnet_name,
            code,
            &mut trust,
            &mut subnets,
            now_secs(),
            0,
            intro.as_ref(),
        ),
    )
    .await

+    // The joiner's self-intro (M8 decision 13, REQ-SUBNET-7): label +

+    // re-pairing under a fresh key and evict the superseded identity.

+    // Best-effort: a label-less or id-less box just sends none (no

+    // overwrite; manual prune covers it).

+        spt_store::hostlabel::os_hostname(),

+        crate::machineid::machine_id_hash(),

+        (Some(label), Some(machine_id)) => Some(spt_net::net::pairing::wire::NodeIntro {

&mut subnets,
now_secs(),
0,
+            intro.as_ref(),
),
)
.await


                run_initiator(
                    &retry_conn,
                    endpoint.node_id(),
                    subnet_name,
                    code,
                    &mut trust,
                    &mut subnets,
                    now_secs(),
                    1,
                ),

                run_initiator(
                    &retry_conn,
                    endpoint.node_id(),
                    subnet_name,
                    code,
                    &mut trust,
                    &mut subnets,
                    now_secs(),
                    1,
                    intro.as_ref(),
                ),


                run_initiator(
                    &retry_conn,
                    endpoint.node_id(),
                    subnet_name,
                    code,
                    &mut trust,
                    &mut subnets,
                    now_secs(),
                    1,
                ),

                run_initiator(
                    &retry_conn,
                    endpoint.node_id(),
                    subnet_name,
                    code,
                    &mut trust,
                    &mut subnets,
                    now_secs(),
                    1,
                    intro.as_ref(),
                ),

&mut subnets,
now_secs(),
1,
+                    intro.as_ref(),
),
)
.await


Responder-side eviction — add to pairhost + registryhost:


    match tokio::time::timeout(
        CEREMONY_TIMEOUT,
        run_responder(
            &conn,
            local_pub,
            &subnets,
            &mut rate,
            &mut trust,
            now_secs(),
        ),
    )
    .await
    {
        Ok(Ok(outcome)) => match trust.save() {
            Ok(()) => eprintln!("PAIRED: joiner pinned ({outcome:?})"),
            Err(e) => eprintln!("PAIRING_TRUST_SAVE_FAIL: {e}"),
        },
        // A failed ceremony is normal traffic (wrong code, impostor probe,
        // vanished peer) — logged, never fatal; the limiter charged it.
        Ok(Err(e)) => eprintln!("PAIRING_ATTEMPT_FAIL: {e}"),
        Err(_) => eprintln!("PAIRING_TIMEOUT: ceremony exceeded {CEREMONY_TIMEOUT_SECS}s"),
    }

    match tokio::time::timeout(
        CEREMONY_TIMEOUT,
        run_responder(
            &conn,
            local_pub,
            &subnets,
            &mut rate,
            &mut trust,
            now_secs(),
        ),
    )
    .await
    {
        Ok(Ok(outcome)) => {
            // Re-pair trust overwrite (M8 decision 13, REQ-SUBNET-7): a
            // COMPLETED ceremony presenting the same label + machine id as
            // an existing row means this MACHINE re-paired under a fresh
            // key — the superseded identity's trust + registry rows go.
            // Fires only here, post-confirm (a gossiped claim never evicts).
            // [impl->REQ-SUBNET-7]
            if let Some(intro) = &outcome.intro {
                crate::registryhost::repair_evict_superseded(
                    &mut trust,
                    &outcome.subnet,
                    &outcome.joiner_hex,
                    intro,
                    &spt_store::perch::identity_dir().join("registry"),
                );
            }
            match trust.save() {
                Ok(()) => eprintln!("PAIRED: joiner pinned ({:?})", outcome.trust),
                Err(e) => eprintln!("PAIRING_TRUST_SAVE_FAIL: {e}"),
            }
        }
        // A failed ceremony is normal traffic (wrong code, impostor probe,
        // vanished peer) — logged, never fatal; the limiter charged it.
        Ok(Err(e)) => eprintln!("PAIRING_ATTEMPT_FAIL: {e}"),
        Err(_) => eprintln!("PAIRING_TIMEOUT: ceremony exceeded {CEREMONY_TIMEOUT_SECS}s"),
    }


    match tokio::time::timeout(
        CEREMONY_TIMEOUT,
        run_responder(
            &conn,
            local_pub,
            &subnets,
            &mut rate,
            &mut trust,
            now_secs(),
        ),
    )
    .await
    {
        Ok(Ok(outcome)) => match trust.save() {
            Ok(()) => eprintln!("PAIRED: joiner pinned ({outcome:?})"),
            Err(e) => eprintln!("PAIRING_TRUST_SAVE_FAIL: {e}"),
        },
        // A failed ceremony is normal traffic (wrong code, impostor probe,
        // vanished peer) — logged, never fatal; the limiter charged it.
        Ok(Err(e)) => eprintln!("PAIRING_ATTEMPT_FAIL: {e}"),
        Err(_) => eprintln!("PAIRING_TIMEOUT: ceremony exceeded {CEREMONY_TIMEOUT_SECS}s"),
    }

    match tokio::time::timeout(
        CEREMONY_TIMEOUT,
        run_responder(
            &conn,
            local_pub,
            &subnets,
            &mut rate,
            &mut trust,
            now_secs(),
        ),
    )
    .await
    {
        Ok(Ok(outcome)) => {
            // Re-pair trust overwrite (M8 decision 13, REQ-SUBNET-7): a
            // COMPLETED ceremony presenting the same label + machine id as
            // an existing row means this MACHINE re-paired under a fresh
            // key — the superseded identity's trust + registry rows go.
            // Fires only here, post-confirm (a gossiped claim never evicts).
            // [impl->REQ-SUBNET-7]
            if let Some(intro) = &outcome.intro {
                crate::registryhost::repair_evict_superseded(
                    &mut trust,
                    &outcome.subnet,
                    &outcome.joiner_hex,
                    intro,
                    &spt_store::perch::identity_dir().join("registry"),
                );
            }
            match trust.save() {
                Ok(()) => eprintln!("PAIRED: joiner pinned ({:?})", outcome.trust),
                Err(e) => eprintln!("PAIRING_TRUST_SAVE_FAIL: {e}"),
            }
        }
        // A failed ceremony is normal traffic (wrong code, impostor probe,
        // vanished peer) — logged, never fatal; the limiter charged it.
        Ok(Err(e)) => eprintln!("PAIRING_ATTEMPT_FAIL: {e}"),
        Err(_) => eprintln!("PAIRING_TIMEOUT: ceremony exceeded {CEREMONY_TIMEOUT_SECS}s"),
    }

-        Ok(Ok(outcome)) => match trust.save() {

-            Ok(()) => eprintln!("PAIRED: joiner pinned ({outcome:?})"),

-            Err(e) => eprintln!("PAIRING_TRUST_SAVE_FAIL: {e}"),

+            // Re-pair trust overwrite (M8 decision 13, REQ-SUBNET-7): a

+            // COMPLETED ceremony presenting the same label + machine id as

+            // an existing row means this MACHINE re-paired under a fresh

+            // key — the superseded identity's trust + registry rows go.

+            // Fires only here, post-confirm (a gossiped claim never evicts).

+            if let Some(intro) = &outcome.intro {

+                crate::registryhost::repair_evict_superseded(

+                    &outcome.joiner_hex,

+                    &spt_store::perch::identity_dir().join("registry"),

+                Ok(()) => eprintln!("PAIRED: joiner pinned ({:?})", outcome.trust),

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


400	        let ttl_ms = ttl.as_millis() as u64;
401	        let heard = self.heard.lock().unwrap();
402	        let silent = |node: &str| {
403	            node != self.node_hex
404	                && heard
405	                    .get(node)
406	                    .is_none_or(|&t| now_ms.saturating_sub(t) > ttl_ms)
407	        };
408	        let mut regs = self.regs.lock().unwrap();
409	        let mut evicted = 0usize;
410	        for (subnet, reg) in regs.iter_mut() {
411	            let n = reg.evict_nodes(silent);
412	            if n > 0 {
413	                eprintln!("REGISTRY_EVICT:{subnet}: {n} row(s) from silent node(s)");
414	            }
415	            evicted += n;
416	        }
417	        drop(regs);
418	        drop(heard);
419	        if evicted > 0 {
420	            self.write_snapshots();
421	        }
422	        evicted
423	    }
424
425	    /// Every instance row for `endpoint_id` in `subnet` (the reconcile
426	    /// election's input; empty when unknown).
427	    pub fn rows(&self, subnet: &str, endpoint_id: &str) -> Vec<Instance> {
428	        self.regs
429	            .lock()
430	            .unwrap()
431	            .get(subnet)
432	            .map(|r| r.instances(endpoint_id).to_vec())
433	            .unwrap_or_default()
434	    }
435
436	    /// A point-in-time clone of one subnet's registry (resolution input).
437	    pub fn snapshot(&self, subnet: &str) -> Option<SubnetRegistry> {
438	        self.regs.lock().unwrap().get(subnet).cloned()
439	    }
440
441	    /// Mirror every subnet's registry to `identity/registry/<subnet>.json`
442	    /// (tmp-write + atomic rename — KNOWN-HAZARDS 5.2) for out-of-process
443	    /// readers. Best-effort: a failed snapshot write must not fail the merge
444	    /// that triggered it (readers tolerate staleness by contract).
445	    fn write_snapshots(&self) {
446	        let regs = self.regs.lock().unwrap();
447	        if regs.is_empty() {
448	            return;
449	        }
450	        if std::fs::create_dir_all(&self.snapshot_dir).is_err() {
451	            return;
452	        }
453	        for (subnet, reg) in regs.iter() {
454	            let Ok(json) = serde_json::to_vec_pretty(reg) else {
455	                continue;
456	            };
457	            let _ = atomic_write(&self.snapshot_dir.join(format!("{subnet}.json")), &json);
458	        }
459	        drop(regs);
460	        // The heard-map mirror (M7 D2 hybrid liveness). `.meta`, not `.json`:
461	        // the snapshot reader globs `<subnet>.json`, so this can never shadow
462	        // a subnet (even one literally named "heard").
463	        let heard = self.heard.lock().unwrap();
464	        if !heard.is_empty() {
465	            if let Ok(json) = serde_json::to_vec_pretty(&*heard) {
466	                let _ = atomic_write(&Self::heard_path(&self.snapshot_dir), &json);
467	            }
468	        }
469	    }
470
471	    /// The snapshot path for one subnet (the CLI reader's contract).
472	    pub fn snapshot_path(dir: &Path, subnet: &str) -> PathBuf {
473	        dir.join(format!("{subnet}.json"))
474	    }
475
476	    /// The heard-map mirror's path (the CLI reader's contract): `node hex →
477	    /// last admitted-feed wall ms`.
478	    // [impl->REQ-SUBNET-1]
479	    pub fn heard_path(dir: &Path) -> PathBuf {
480	        dir.join("heard.meta")
481	    }
482	}
483
484	/// Wall-clock ms (the heard-map stamp; display/recency only — never a merge
485	/// key, the epoch lease owns ordering).
486	fn wall_ms() -> u64 {
487	    std::time::SystemTime::now()
488	        .duration_since(std::time::UNIX_EPOCH)
489	        .map(|d| d.as_millis() as u64)
490	        .unwrap_or(0)
491	}
492
493	/// tmp-write + atomic rename with the 5.2 Windows-EBUSY retry posture.
494	fn atomic_write(path: &Path, bytes: &[u8]) -> io::Result<()> {
495	    let tmp = path.with_extension("json.tmp");
496	    std::fs::write(&tmp, bytes)?;
497	    for attempt in 0..5 {
498	        match std::fs::rename(&tmp, path) {
499	            Ok(()) => return Ok(()),
500	            Err(_) if attempt < 4 => {
501	                std::thread::sleep(std::time::Duration::from_millis(20 << attempt));
502	            }
503	            Err(e) => return Err(e),
504	        }
505	    }
506	    unreachable!("loop returns on every path")
507	}
508
509	#[cfg(test)]
510	mod tests {
511	    use super::*;
512	    use crate::test_home::with_home;
513	    use spt_store::info::{self, InfoJson};
514
515	    fn upd(subnet: &str, id: &str, node: &str, epoch: u64) -> RegistryUpdate {
516	        RegistryUpdate {
517	            subnet: subnet.to_string(),
518	            endpoint_id: id.to_string(),
519	            instance: Instance {
520	                node: node.to_string(),
521	                status: Status::Active,
522	                epoch,
523	                resources: None,
524	                last_active_ms: None,
525	                shell_adapters: Vec::new(),
526	                node_label: None,
527	                machine_id: None,
528	            },
529	        }

        let ttl_ms = ttl.as_millis() as u64;
        let heard = self.heard.lock().unwrap();
        let silent = |node: &str| {
            node != self.node_hex
                && heard
                    .get(node)
                    .is_none_or(|&t| now_ms.saturating_sub(t) > ttl_ms)
        };
        let mut regs = self.regs.lock().unwrap();
        let mut evicted = 0usize;
        for (subnet, reg) in regs.iter_mut() {
            let n = reg.evict_nodes(silent);
            if n > 0 {
                eprintln!("REGISTRY_EVICT:{subnet}: {n} row(s) from silent node(s)");
            }
            evicted += n;
        }
        drop(regs);
        drop(heard);
        if evicted > 0 {
            self.write_snapshots();
        }
        evicted
    }

    /// Every instance row for `endpoint_id` in `subnet` (the reconcile
    /// election's input; empty when unknown).
    pub fn rows(&self, subnet: &str, endpoint_id: &str) -> Vec<Instance> {
        self.regs
            .lock()
            .unwrap()
            .get(subnet)
            .map(|r| r.instances(endpoint_id).to_vec())
            .unwrap_or_default()
    }

    /// A point-in-time clone of one subnet's registry (resolution input).
    pub fn snapshot(&self, subnet: &str) -> Option<SubnetRegistry> {
        self.regs.lock().unwrap().get(subnet).cloned()
    }

    /// Mirror every subnet's registry to `identity/registry/<subnet>.json`
    /// (tmp-write + atomic rename — KNOWN-HAZARDS 5.2) for out-of-process
    /// readers. Best-effort: a failed snapshot write must not fail the merge
    /// that triggered it (readers tolerate staleness by contract).
    fn write_snapshots(&self) {
        let regs = self.regs.lock().unwrap();
        if regs.is_empty() {
            return;
        }
        if std::fs::create_dir_all(&self.snapshot_dir).is_err() {
            return;
        }
        for (subnet, reg) in regs.iter() {
            let Ok(json) = serde_json::to_vec_pretty(reg) else {
                continue;
            };
            let _ = atomic_write(&self.snapshot_dir.join(format!("{subnet}.json")), &json);
        }
        drop(regs);
        // The heard-map mirror (M7 D2 hybrid liveness). `.meta`, not `.json`:
        // the snapshot reader globs `<subnet>.json`, so this can never shadow
        // a subnet (even one literally named "heard").
        let heard = self.heard.lock().unwrap();
        if !heard.is_empty() {
            if let Ok(json) = serde_json::to_vec_pretty(&*heard) {
                let _ = atomic_write(&Self::heard_path(&self.snapshot_dir), &json);
            }
        }
    }

    /// The snapshot path for one subnet (the CLI reader's contract).
    pub fn snapshot_path(dir: &Path, subnet: &str) -> PathBuf {
        dir.join(format!("{subnet}.json"))
    }

    /// The heard-map mirror's path (the CLI reader's contract): `node hex →
    /// last admitted-feed wall ms`.
    // [impl->REQ-SUBNET-1]
    pub fn heard_path(dir: &Path) -> PathBuf {
        dir.join("heard.meta")
    }
}

/// Wall-clock ms (the heard-map stamp; display/recency only — never a merge
/// key, the epoch lease owns ordering).
fn wall_ms() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0)
}

/// tmp-write + atomic rename with the 5.2 Windows-EBUSY retry posture.
fn atomic_write(path: &Path, bytes: &[u8]) -> io::Result<()> {
    let tmp = path.with_extension("json.tmp");
    std::fs::write(&tmp, bytes)?;
    for attempt in 0..5 {
        match std::fs::rename(&tmp, path) {
            Ok(()) => return Ok(()),
            Err(_) if attempt < 4 => {
                std::thread::sleep(std::time::Duration::from_millis(20 << attempt));
            }
            Err(e) => return Err(e),
        }
    }
    unreachable!("loop returns on every path")
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_home::with_home;
    use spt_store::info::{self, InfoJson};

    fn upd(subnet: &str, id: &str, node: &str, epoch: u64) -> RegistryUpdate {
        RegistryUpdate {
            subnet: subnet.to_string(),
            endpoint_id: id.to_string(),
            instance: Instance {
                node: node.to_string(),
                status: Status::Active,
                epoch,
                resources: None,
                last_active_ms: None,
                shell_adapters: Vec::new(),
                node_label: None,
                machine_id: None,
            },
        }


    /// The heard-map mirror's path (the CLI reader's contract): `node hex →
    /// last admitted-feed wall ms`.
    // [impl->REQ-SUBNET-1]
    pub fn heard_path(dir: &Path) -> PathBuf {
        dir.join("heard.meta")
    }
}

    /// The heard-map mirror's path (the CLI reader's contract): `node hex →
    /// last admitted-feed wall ms`.
    // [impl->REQ-SUBNET-1]
    pub fn heard_path(dir: &Path) -> PathBuf {
        dir.join("heard.meta")
    }

    /// The re-pair eviction marker's path (M8 decision 13, REQ-SUBNET-7):
    /// a JSON array of superseded node hexes the pairing host noted —
    /// consumed by the pump's registry tick so the IN-MEMORY rows go too
    /// (a disk-only eviction would be resurrected by the next snapshot
    /// mirror). `.meta` for the same can't-shadow-a-subnet reason as the
    /// heard map.
    pub fn repair_evict_path(dir: &Path) -> PathBuf {
        dir.join("repair-evict.meta")
    }

    /// Consume the re-pair eviction marker (the [`PULL_MARKER_FILE`]
    /// pattern — take is the remove): evict every noted identity's rows
    /// from the in-memory registries + the heard map, and rewrite the
    /// snapshots. No marker = no-op. Returns evicted row count.
    // [impl->REQ-SUBNET-7]
    pub fn consume_repair_evictions(&self) -> usize {
        let path = Self::repair_evict_path(&self.snapshot_dir);
        let Ok(raw) = std::fs::read_to_string(&path) else {
            return 0;
        };
        let _ = std::fs::remove_file(&path);
        let dead: std::collections::BTreeSet<String> =
            serde_json::from_str(&raw).unwrap_or_default();
        if dead.is_empty() {
            return 0;
        }
        {
            let mut heard = self.heard.lock().unwrap();
            for hex in &dead {
                heard.remove(hex);
            }
        }
        let mut regs = self.regs.lock().unwrap();
        let mut evicted = 0usize;
        for (subnet, reg) in regs.iter_mut() {
            let n = reg.evict_nodes(|node| dead.contains(node));
            if n > 0 {
                eprintln!("REPAIR_EVICT:{subnet}: {n} superseded row(s)");
            }
            evicted += n;
        }
        drop(regs);
        if evicted > 0 {
            self.write_snapshots();
        }
        evicted
    }
}

/// The superseded identities a completed ceremony's intro reveals (pure,
/// M8 decision 13 / REQ-SUBNET-7): rows in `reg` advertising the SAME node
/// label AND machine id under a DIFFERENT node key than the re-paired
/// joiner. Both must match — a label alone re-pairs nothing (hostnames
/// collide), a machine id alone is never gossiped without its label.
// [impl->REQ-SUBNET-7]
pub fn superseded_identities(
    reg: &SubnetRegistry,
    intro: &spt_net::net::pairing::wire::NodeIntro,
    joiner_hex: &str,
) -> Vec<String> {
    let mut out = Vec::new();
    for id in reg.endpoint_ids() {
        for inst in reg.instances(id) {
            if inst.node != joiner_hex
                && inst.node_label.as_deref() == Some(intro.label.as_str())
                && inst.machine_id.as_deref() == Some(intro.machine_id.as_str())
                && !out.contains(&inst.node)
            {
                out.push(inst.node.clone());
            }
        }
    }
    out
}

/// The re-pair overwrite's effect leg (M8 decision 13, REQ-SUBNET-7), run
/// by the pairing host AFTER a completed ceremony: resolve the superseded
/// identities from the subnet's snapshot, drop their trust rows (this
/// subnet's scope — the ceremony proved control of THIS subnet's code,
/// nothing about others), rewrite the disk snapshot, and note the hexes in
/// the eviction marker so the pump clears the in-memory rows on its next
/// tick. The caller persists `trust`.
// [impl->REQ-SUBNET-7]
pub fn repair_evict_superseded(
    trust: &mut spt_store::trust::TrustStore,
    subnet: &str,
    joiner_hex: &str,
    intro: &spt_net::net::pairing::wire::NodeIntro,
    snap_dir: &Path,
) {
    let snap_path = RegistryHost::snapshot_path(snap_dir, subnet);
    let Some(mut reg) = std::fs::read_to_string(&snap_path)
        .ok()
        .and_then(|s| serde_json::from_str::<SubnetRegistry>(&s).ok())
    else {
        return; // no gossip yet — nothing to supersede
    };
    let dead = superseded_identities(&reg, intro, joiner_hex);
    if dead.is_empty() {
        return;
    }
    for hex in &dead {
        let dropped = trust.revoke(subnet, hex);
        eprintln!(
            "REPAIR_SUPERSEDED:{} (label '{}' re-paired as {}…; trust row {})",
            hex,
            intro.label,
            spt_net::net::registry::key_prefix(joiner_hex),
            if dropped { "dropped" } else { "absent" },
        );
    }
    reg.evict_nodes(|node| dead.iter().any(|d| d == node));
    if let Ok(json) = serde_json::to_vec_pretty(&reg) {
        let _ = atomic_write(&snap_path, &json);
    }
    // Merge into the marker (several re-pairs can land between pump ticks).
    let marker = RegistryHost::repair_evict_path(snap_dir);
    let mut all: std::collections::BTreeSet<String> = std::fs::read_to_string(&marker)
        .ok()
        .and_then(|s| serde_json::from_str(&s).ok())
        .unwrap_or_default();
    all.extend(dead);
    if let Ok(json) = serde_json::to_vec_pretty(&all) {
        let _ = atomic_write(&marker, &json);
    }
}


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

    /// The heard-map mirror's path (the CLI reader's contract): `node hex →
    /// last admitted-feed wall ms`.
    // [impl->REQ-SUBNET-1]
    pub fn heard_path(dir: &Path) -> PathBuf {
        dir.join("heard.meta")
    }
}

    /// The heard-map mirror's path (the CLI reader's contract): `node hex →
    /// last admitted-feed wall ms`.
    // [impl->REQ-SUBNET-1]
    pub fn heard_path(dir: &Path) -> PathBuf {
        dir.join("heard.meta")
    }

    /// The re-pair eviction marker's path (M8 decision 13, REQ-SUBNET-7):
    /// a JSON array of superseded node hexes the pairing host noted —
    /// consumed by the pump's registry tick so the IN-MEMORY rows go too
    /// (a disk-only eviction would be resurrected by the next snapshot
    /// mirror). `.meta` for the same can't-shadow-a-subnet reason as the
    /// heard map.
    pub fn repair_evict_path(dir: &Path) -> PathBuf {
        dir.join("repair-evict.meta")
    }

    /// Consume the re-pair eviction marker (the [`PULL_MARKER_FILE`]
    /// pattern — take is the remove): evict every noted identity's rows
    /// from the in-memory registries + the heard map, and rewrite the
    /// snapshots. No marker = no-op. Returns evicted row count.
    // [impl->REQ-SUBNET-7]
    pub fn consume_repair_evictions(&self) -> usize {
        let path = Self::repair_evict_path(&self.snapshot_dir);
        let Ok(raw) = std::fs::read_to_string(&path) else {
            return 0;
        };
        let _ = std::fs::remove_file(&path);
        let dead: std::collections::BTreeSet<String> =
            serde_json::from_str(&raw).unwrap_or_default();
        if dead.is_empty() {
            return 0;
        }
        {
            let mut heard = self.heard.lock().unwrap();
            for hex in &dead {
                heard.remove(hex);
            }
        }
        let mut regs = self.regs.lock().unwrap();
        let mut evicted = 0usize;
        for (subnet, reg) in regs.iter_mut() {
            let n = reg.evict_nodes(|node| dead.contains(node));
            if n > 0 {
                eprintln!("REPAIR_EVICT:{subnet}: {n} superseded row(s)");
            }
            evicted += n;
        }
        drop(regs);
        if evicted > 0 {
            self.write_snapshots();
        }
        evicted
    }
}

/// The superseded identities a completed ceremony's intro reveals (pure,
/// M8 decision 13 / REQ-SUBNET-7): rows in `reg` advertising the SAME node
/// label AND machine id under a DIFFERENT node key than the re-paired
/// joiner. Both must match — a label alone re-pairs nothing (hostnames
/// collide), a machine id alone is never gossiped without its label.
// [impl->REQ-SUBNET-7]
pub fn superseded_identities(
    reg: &SubnetRegistry,
    intro: &spt_net::net::pairing::wire::NodeIntro,
    joiner_hex: &str,
) -> Vec<String> {
    let mut out = Vec::new();
    for id in reg.endpoint_ids() {
        for inst in reg.instances(id) {
            if inst.node != joiner_hex
                && inst.node_label.as_deref() == Some(intro.label.as_str())
                && inst.machine_id.as_deref() == Some(intro.machine_id.as_str())
                && !out.contains(&inst.node)
            {
                out.push(inst.node.clone());
            }
        }
    }
    out
}

/// The re-pair overwrite's effect leg (M8 decision 13, REQ-SUBNET-7), run
/// by the pairing host AFTER a completed ceremony: resolve the superseded
/// identities from the subnet's snapshot, drop their trust rows (this
/// subnet's scope — the ceremony proved control of THIS subnet's code,
/// nothing about others), rewrite the disk snapshot, and note the hexes in
/// the eviction marker so the pump clears the in-memory rows on its next
/// tick. The caller persists `trust`.
// [impl->REQ-SUBNET-7]
pub fn repair_evict_superseded(
    trust: &mut spt_store::trust::TrustStore,
    subnet: &str,
    joiner_hex: &str,
    intro: &spt_net::net::pairing::wire::NodeIntro,
    snap_dir: &Path,
) {
    let snap_path = RegistryHost::snapshot_path(snap_dir, subnet);
    let Some(mut reg) = std::fs::read_to_string(&snap_path)
        .ok()
        .and_then(|s| serde_json::from_str::<SubnetRegistry>(&s).ok())
    else {
        return; // no gossip yet — nothing to supersede
    };
    let dead = superseded_identities(&reg, intro, joiner_hex);
    if dead.is_empty() {
        return;
    }
    for hex in &dead {
        let dropped = trust.revoke(subnet, hex);
        eprintln!(
            "REPAIR_SUPERSEDED:{} (label '{}' re-paired as {}…; trust row {})",
            hex,
            intro.label,
            spt_net::net::registry::key_prefix(joiner_hex),
            if dropped { "dropped" } else { "absent" },
        );
    }
    reg.evict_nodes(|node| dead.iter().any(|d| d == node));
    if let Ok(json) = serde_json::to_vec_pretty(&reg) {
        let _ = atomic_write(&snap_path, &json);
    }
    // Merge into the marker (several re-pairs can land between pump ticks).
    let marker = RegistryHost::repair_evict_path(snap_dir);
    let mut all: std::collections::BTreeSet<String> = std::fs::read_to_string(&marker)
        .ok()
        .and_then(|s| serde_json::from_str(&s).ok())
        .unwrap_or_default();
    all.extend(dead);
    if let Ok(json) = serde_json::to_vec_pretty(&all) {
        let _ = atomic_write(&marker, &json);
    }
}

+    /// The re-pair eviction marker's path (M8 decision 13, REQ-SUBNET-7):

+    /// a JSON array of superseded node hexes the pairing host noted —

+    /// consumed by the pump's registry tick so the IN-MEMORY rows go too

+    /// (a disk-only eviction would be resurrected by the next snapshot

+    /// mirror). `.meta` for the same can't-shadow-a-subnet reason as the

+    pub fn repair_evict_path(dir: &Path) -> PathBuf {

+    /// Consume the re-pair eviction marker (the [`PULL_MARKER_FILE`]

+    /// pattern — take is the remove): evict every noted identity's rows

+    /// from the in-memory registries + the heard map, and rewrite the

+    /// snapshots. No marker = no-op. Returns evicted row count.

+    pub fn consume_repair_evictions(&self) -> usize {

+        let path = Self::repair_evict_path(&self.snapshot_dir);

+        let Ok(raw) = std::fs::read_to_string(&path) else {

+        let _ = std::fs::remove_file(&path);

+        let dead: std::collections::BTreeSet<String> =

+            serde_json::from_str(&raw).unwrap_or_default();

+            let mut heard = self.heard.lock().unwrap();

+        let mut regs = self.regs.lock().unwrap();

+        for (subnet, reg) in regs.iter_mut() {

+            let n = reg.evict_nodes(|node| dead.contains(node));

+                eprintln!("REPAIR_EVICT:{subnet}: {n} superseded row(s)");

+/// The superseded identities a completed ceremony's intro reveals (pure,

+/// M8 decision 13 / REQ-SUBNET-7): rows in `reg` advertising the SAME node

+/// label AND machine id under a DIFFERENT node key than the re-paired

+/// joiner. Both must match — a label alone re-pairs nothing (hostnames

+/// collide), a machine id alone is never gossiped without its label.

+    intro: &spt_net::net::pairing::wire::NodeIntro,

+        for inst in reg.instances(id) {

+                && inst.node_label.as_deref() == Some(intro.label.as_str())

+                && inst.machine_id.as_deref() == Some(intro.machine_id.as_str())

+                && !out.contains(&inst.node)

+                out.push(inst.node.clone());

+/// The re-pair overwrite's effect leg (M8 decision 13, REQ-SUBNET-7), run

+/// by the pairing host AFTER a completed ceremony: resolve the superseded

+/// identities from the subnet's snapshot, drop their trust rows (this

+/// subnet's scope — the ceremony proved control of THIS subnet's code,

+/// nothing about others), rewrite the disk snapshot, and note the hexes in

+/// the eviction marker so the pump clears the in-memory rows on its next

+    trust: &mut spt_store::trust::TrustStore,

+    let snap_path = RegistryHost::snapshot_path(snap_dir, subnet);

+    let Some(mut reg) = std::fs::read_to_string(&snap_path)

+        .and_then(|s| serde_json::from_str::<SubnetRegistry>(&s).ok())

+        return; // no gossip yet — nothing to supersede

+    let dead = superseded_identities(&reg, intro, joiner_hex);

+        let dropped = trust.revoke(subnet, hex);

+            "REPAIR_SUPERSEDED:{} (label '{}' re-paired as {}…; trust row {})",

+            spt_net::net::registry::key_prefix(joiner_hex),

+            if dropped { "dropped" } else { "absent" },

+    reg.evict_nodes(|node| dead.iter().any(|d| d == node));

+    if let Ok(json) = serde_json::to_vec_pretty(&reg) {

+        let _ = atomic_write(&snap_path, &json);

+    // Merge into the marker (several re-pairs can land between pump ticks).

+    let marker = RegistryHost::repair_evict_path(snap_dir);

+    let mut all: std::collections::BTreeSet<String> = std::fs::read_to_string(&marker)

+        .and_then(|s| serde_json::from_str(&s).ok())

+    if let Ok(json) = serde_json::to_vec_pretty(&all) {

+        let _ = atomic_write(&marker, &json);

/// Wall-clock ms (the heard-map stamp; display/recency only — never a merge

/// key, the epoch lease owns ordering).


Wire the pump consume + build:


        let adverts = if due_reg {
            // Sweep silent-node ghost rows BEFORE re-advertising (KNOWN-
            // HAZARDS 4.10): a dead node identity's rows decay instead of
            // poisoning bare-id resolution forever; a live peer re-stamps its
            // heard entry with every admitted feed and is never touched.
            // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]
            registry.evict_silent_peers(cadence.registry_evict_after);
            registry.advertise_local(&paths.owlery)

        let adverts = if due_reg {
            // Consume the re-pair eviction marker FIRST (M8 decision 13,
            // REQ-SUBNET-7): a superseded identity's in-memory rows go
            // before this tick's snapshot mirror can resurrect the pairing
            // host's disk-side eviction.
            // [impl->REQ-SUBNET-7]
            registry.consume_repair_evictions();
            // Sweep silent-node ghost rows BEFORE re-advertising (KNOWN-
            // HAZARDS 4.10): a dead node identity's rows decay instead of
            // poisoning bare-id resolution forever; a live peer re-stamps its
            // heard entry with every admitted feed and is never touched.
            // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]
            registry.evict_silent_peers(cadence.registry_evict_after);
            registry.advertise_local(&paths.owlery)


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)

        let adverts = if due_reg {
            // Sweep silent-node ghost rows BEFORE re-advertising (KNOWN-
            // HAZARDS 4.10): a dead node identity's rows decay instead of
            // poisoning bare-id resolution forever; a live peer re-stamps its
            // heard entry with every admitted feed and is never touched.
            // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]
            registry.evict_silent_peers(cadence.registry_evict_after);
            registry.advertise_local(&paths.owlery)

        let adverts = if due_reg {
            // Consume the re-pair eviction marker FIRST (M8 decision 13,
            // REQ-SUBNET-7): a superseded identity's in-memory rows go
            // before this tick's snapshot mirror can resurrect the pairing
            // host's disk-side eviction.
            // [impl->REQ-SUBNET-7]
            registry.consume_repair_evictions();
            // Sweep silent-node ghost rows BEFORE re-advertising (KNOWN-
            // HAZARDS 4.10): a dead node identity's rows decay instead of
            // poisoning bare-id resolution forever; a live peer re-stamps its
            // heard entry with every admitted feed and is never touched.
            // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]
            registry.evict_silent_peers(cadence.registry_evict_after);
            registry.advertise_local(&paths.owlery)

+            // Consume the re-pair eviction marker FIRST (M8 decision 13,

+            // REQ-SUBNET-7): a superseded identity's in-memory rows go

+            // before this tick's snapshot mirror can resurrect the pairing

+            // host's disk-side eviction.

+            registry.consume_repair_evictions();


D3 — install & elevation hardening

D5 — docs + CI + acceptance prep


PowerShell

cargo build -q -p spt -p spt-daemon -p spt-net 2>&1 | Select-Object -First 40

Build after re-pair wiring


(PowerShell completed with no output)


Add re-pair unit tests:


640
641	    fn upd(subnet: &str, id: &str, node: &str, epoch: u64) -> RegistryUpdate {
642	        RegistryUpdate {
643	            subnet: subnet.to_string(),
644	            endpoint_id: id.to_string(),
645	            instance: Instance {
646	                node: node.to_string(),
647	                status: Status::Active,
648	                epoch,
649	                resources: None,
650	                last_active_ms: None,
651	                shell_adapters: Vec::new(),
652	                node_label: None,
653	                machine_id: None,
654	            },
655	        }
656	    }
657
658	    fn policy(subnet: &str, trusted: &str) -> RegistryGatePolicy {
659	        let mut trust = TrustStore::default();
660	        trust.record(subnet, trusted, None, None);
661	        RegistryGatePolicy {
662	            subnets: vec![subnet.to_string()],
663	            trust,
664	        }
665	    }
666
667	    fn host(dir: &Path) -> RegistryHost {
668	        RegistryHost::new_at(
669	            "aa11",
670	            EpochSource::load_from(&dir.join("epoch.json")),
671	            dir.join("registry"),
672	        )
673	    }
674
675	    // [unit->REQ-INST-7] inbound feeds gate fail-closed: non-member subnet
676	    // and untrusted origin write nothing; an admitted row merges and the
677	    // epoch lease still drops a stale follow-up.
678	    #[test]
679	    fn apply_feed_gates_and_merges_under_the_lease() {
680	        let dir = tempfile::tempdir().unwrap();
681	        let h = host(dir.path());
682	        let p = policy("home", "bb22");
683
684	        let v = h.apply_feed("bb22", &[upd("other", "ling", "bb22", 5)], &p);
685	        assert_eq!(v, vec![RegistryApplyVerdict::NotMember]);
686	        let v = h.apply_feed("cc33", &[upd("home", "ling", "cc33", 5)], &p);
687	        assert_eq!(v, vec![RegistryApplyVerdict::UntrustedOrigin]);
688	        assert!(
689	            h.rows("home", "ling").is_empty(),
690	            "refused feeds write nothing"
691	        );
692
693	        let v = h.apply_feed("bb22", &[upd("home", "ling", "bb22", 5)], &p);
694	        assert_eq!(
695	            v,
696	            vec![RegistryApplyVerdict::Merged(MergeOutcome::Inserted)]
697	        );
698	        // A lagging lower-epoch update is dropped by the lease (4.8).
699	        let v = h.apply_feed("bb22", &[upd("home", "ling", "bb22", 4)], &p);
700	        assert_eq!(v, vec![RegistryApplyVerdict::Merged(MergeOutcome::Stale)]);
701	        assert_eq!(h.rows("home", "ling")[0].epoch, 5);
702	    }
703
704	    // [unit->REQ-INST-14] advertisement carries the blurb BOTH-AUTHORED:
705	    // the endpoint's own info.json blurb wins; an endpoint without one
706	    // advertises the node-config seed; a runtime refine (set_resources)
707	    // reaches the registry as an ordinary epoch-bumped update.
708	    #[test]
709	    fn advertisement_carries_both_authored_blurb() {
710	        with_home(|home| {
711	            let owlery = home.join("owlery");
712	            for id in ["authored", "seeded"] {
713	                let p = perch::resolve_perch_path_in(&owlery, id, ParentHint::Infer);
714	                std::fs::create_dir_all(&p).unwrap();
715	                let rec = InfoJson::new(id, "now", std::process::id(), "s", "ready_agent");
716	                info::write_info(&p, &rec).unwrap();
717	            }
718	            let authored = perch::resolve_perch_path_in(&owlery, "authored", ParentHint::Infer);
719	            info::set_resources(&authored, Some("rust builds")).unwrap();

    fn upd(subnet: &str, id: &str, node: &str, epoch: u64) -> RegistryUpdate {
        RegistryUpdate {
            subnet: subnet.to_string(),
            endpoint_id: id.to_string(),
            instance: Instance {
                node: node.to_string(),
                status: Status::Active,
                epoch,
                resources: None,
                last_active_ms: None,
                shell_adapters: Vec::new(),
                node_label: None,
                machine_id: None,
            },
        }
    }

    fn policy(subnet: &str, trusted: &str) -> RegistryGatePolicy {
        let mut trust = TrustStore::default();
        trust.record(subnet, trusted, None, None);
        RegistryGatePolicy {
            subnets: vec![subnet.to_string()],
            trust,
        }
    }

    fn host(dir: &Path) -> RegistryHost {
        RegistryHost::new_at(
            "aa11",
            EpochSource::load_from(&dir.join("epoch.json")),
            dir.join("registry"),
        )
    }

    // [unit->REQ-INST-7] inbound feeds gate fail-closed: non-member subnet
    // and untrusted origin write nothing; an admitted row merges and the
    // epoch lease still drops a stale follow-up.
    #[test]
    fn apply_feed_gates_and_merges_under_the_lease() {
        let dir = tempfile::tempdir().unwrap();
        let h = host(dir.path());
        let p = policy("home", "bb22");

        let v = h.apply_feed("bb22", &[upd("other", "ling", "bb22", 5)], &p);
        assert_eq!(v, vec![RegistryApplyVerdict::NotMember]);
        let v = h.apply_feed("cc33", &[upd("home", "ling", "cc33", 5)], &p);
        assert_eq!(v, vec![RegistryApplyVerdict::UntrustedOrigin]);
        assert!(
            h.rows("home", "ling").is_empty(),
            "refused feeds write nothing"
        );

        let v = h.apply_feed("bb22", &[upd("home", "ling", "bb22", 5)], &p);
        assert_eq!(
            v,
            vec![RegistryApplyVerdict::Merged(MergeOutcome::Inserted)]
        );
        // A lagging lower-epoch update is dropped by the lease (4.8).
        let v = h.apply_feed("bb22", &[upd("home", "ling", "bb22", 4)], &p);
        assert_eq!(v, vec![RegistryApplyVerdict::Merged(MergeOutcome::Stale)]);
        assert_eq!(h.rows("home", "ling")[0].epoch, 5);
    }

    // [unit->REQ-INST-14] advertisement carries the blurb BOTH-AUTHORED:
    // the endpoint's own info.json blurb wins; an endpoint without one
    // advertises the node-config seed; a runtime refine (set_resources)
    // reaches the registry as an ordinary epoch-bumped update.
    #[test]
    fn advertisement_carries_both_authored_blurb() {
        with_home(|home| {
            let owlery = home.join("owlery");
            for id in ["authored", "seeded"] {
                let p = perch::resolve_perch_path_in(&owlery, id, ParentHint::Infer);
                std::fs::create_dir_all(&p).unwrap();
                let rec = InfoJson::new(id, "now", std::process::id(), "s", "ready_agent");
                info::write_info(&p, &rec).unwrap();
            }
            let authored = perch::resolve_perch_path_in(&owlery, "authored", ParentHint::Infer);
            info::set_resources(&authored, Some("rust builds")).unwrap();


Edit

    // [unit->REQ-INST-7] inbound feeds gate fail-closed: non-member subnet
    // and untrusted origin write nothing; an admitted row merges and the
    // epoch lease still drops a stale follow-up.
    #[test]
    fn apply_feed_gates_and_merges_under_the_lease() {

    /// An instance row stamped with a label + machine id (the M8 re-pair
    /// anchor pair).
    fn identified(node: &str, label: &str, mid: &str, epoch: u64) -> Instance {
        Instance {
            node: node.to_string(),
            status: Status::Active,
            epoch,
            resources: None,
            last_active_ms: None,
            shell_adapters: Vec::new(),
            node_label: Some(label.to_string()),
            machine_id: Some(mid.to_string()),
        }
    }

    // [unit->REQ-SUBNET-7] superseded-identity resolution: BOTH label and
    // machine id must match a DIFFERENT key — label alone (hostname
    // collision) and machine id alone never supersede; the joiner's own
    // rows never match.
    #[test]
    fn superseded_requires_label_and_machine_id_under_other_key() {
        let mut reg = SubnetRegistry::new();
        reg.merge_instance("ling", identified("old-key", "BOX", "mid-1", 5));
        reg.merge_instance("oak", identified("label-only", "BOX", "mid-9", 5));
        reg.merge_instance("ivy", identified("mid-only", "OTHER", "mid-1", 5));
        reg.merge_instance("elm", identified("new-key", "BOX", "mid-1", 5));
        let intro = spt_net::net::pairing::wire::NodeIntro {
            label: "BOX".into(),
            machine_id: "mid-1".into(),
        };
        assert_eq!(
            superseded_identities(&reg, &intro, "new-key"),
            vec!["old-key".to_string()],
            "only the same-(label,mid) different-key identity supersedes"
        );
    }

    // [unit->REQ-SUBNET-7] the effect leg: a completed re-pair drops the
    // superseded identity's trust row (this subnet only), rewrites the
    // disk snapshot without its rows, and notes it in the marker; the
    // host's consume then clears the in-memory rows + the heard entry.
    #[test]
    fn repair_evict_drops_trust_snapshot_and_memory() {
        let dir = tempfile::tempdir().unwrap();
        let snap_dir = dir.path().join("registry");
        let h = RegistryHost::new_at(
            "self",
            EpochSource::load_from(&dir.path().join("epoch.json")),
            snap_dir.clone(),
        );
        // Gossip state: the old identity advertises in home + a second subnet.
        let p = policy("home", "old-key");
        h.apply_feed(
            "old-key",
            &[RegistryUpdate {
                subnet: "home".into(),
                endpoint_id: "ling".into(),
                instance: identified("old-key", "BOX", "mid-1", 5),
            }],
            &p,
        );
        // Snapshot mirrored to disk by the merge.
        assert!(RegistryHost::snapshot_path(&snap_dir, "home").exists());

        let mut trust = TrustStore::default();
        trust.record("home", "old-key", None, None);
        trust.record("worknet", "old-key", None, None);
        let intro = spt_net::net::pairing::wire::NodeIntro {
            label: "BOX".into(),
            machine_id: "mid-1".into(),
        };
        repair_evict_superseded(&mut trust, "home", "new-key", &intro, &snap_dir);

        assert!(
            !trust.is_trusted("home", "old-key"),
            "ceremony subnet's trust row dropped"
        );
        assert!(
            trust.is_trusted("worknet", "old-key"),
            "other subnets untouched — the ceremony proved THIS subnet's code only"
        );
        let on_disk: SubnetRegistry = serde_json::from_str(
            &std::fs::read_to_string(RegistryHost::snapshot_path(&snap_dir, "home")).unwrap(),
        )
        .unwrap();
        assert!(
            on_disk.instances("ling").is_empty(),
            "superseded rows gone from the disk snapshot"
        );
        // The marker holds the hex; consume clears the IN-MEMORY rows too
        // (else the next snapshot mirror would resurrect the disk eviction).
        assert!(RegistryHost::repair_evict_path(&snap_dir).exists());
        assert_eq!(h.consume_repair_evictions(), 1, "in-memory row evicted");
        assert!(h.rows("home", "ling").is_empty());
        assert!(
            !RegistryHost::repair_evict_path(&snap_dir).exists(),
            "marker consumed exactly once"
        );
        assert_eq!(h.consume_repair_evictions(), 0, "second consume no-ops");

        // No-match intro: nothing evicts, no marker minted.
        let mut t2 = TrustStore::default();
        t2.record("home", "unrelated", None, None);
        let other = spt_net::net::pairing::wire::NodeIntro {
            label: "ELSEWHERE".into(),
            machine_id: "mid-x".into(),
        };
        repair_evict_superseded(&mut t2, "home", "new-key", &other, &snap_dir);
        assert!(t2.is_trusted("home", "unrelated"));
        assert!(!RegistryHost::repair_evict_path(&snap_dir).exists());
    }

    // [unit->REQ-INST-7] inbound feeds gate fail-closed: non-member subnet
    // and untrusted origin write nothing; an admitted row merges and the
    // epoch lease still drops a stale follow-up.
    #[test]
    fn apply_feed_gates_and_merges_under_the_lease() {


    /// An instance row stamped with a label + machine id (the M8 re-pair
    /// anchor pair).
    fn identified(node: &str, label: &str, mid: &str, epoch: u64) -> Instance {
        Instance {
            node: node.to_string(),
            status: Status::Active,
            epoch,
            resources: None,
            last_active_ms: None,
            shell_adapters: Vec::new(),
            node_label: Some(label.to_string()),
            machine_id: Some(mid.to_string()),
        }
    }

    // [unit->REQ-SUBNET-7] superseded-identity resolution: BOTH label and
    // machine id must match a DIFFERENT key — label alone (hostname
    // collision) and machine id alone never supersede; the joiner's own
    // rows never match.
    #[test]
    fn superseded_requires_label_and_machine_id_under_other_key() {
        let mut reg = SubnetRegistry::new();
        reg.merge_instance("ling", identified("old-key", "BOX", "mid-1", 5));
        reg.merge_instance("oak", identified("label-only", "BOX", "mid-9", 5));
        reg.merge_instance("ivy", identified("mid-only", "OTHER", "mid-1", 5));
        reg.merge_instance("elm", identified("new-key", "BOX", "mid-1", 5));
        let intro = spt_net::net::pairing::wire::NodeIntro {
            label: "BOX".into(),
            machine_id: "mid-1".into(),
        };
        assert_eq!(
            superseded_identities(&reg, &intro, "new-key"),
            vec!["old-key".to_string()],
            "only the same-(label,mid) different-key identity supersedes"
        );
    }

    // [unit->REQ-SUBNET-7] the effect leg: a completed re-pair drops the
    // superseded identity's trust row (this subnet only), rewrites the
    // disk snapshot without its rows, and notes it in the marker; the
    // host's consume then clears the in-memory rows + the heard entry.
    #[test]
    fn repair_evict_drops_trust_snapshot_and_memory() {
        let dir = tempfile::tempdir().unwrap();
        let snap_dir = dir.path().join("registry");
        let h = RegistryHost::new_at(
            "self",
            EpochSource::load_from(&dir.path().join("epoch.json")),
            snap_dir.clone(),
        );
        // Gossip state: the old identity advertises in home + a second subnet.
        let p = policy("home", "old-key");
        h.apply_feed(
            "old-key",
            &[RegistryUpdate {
                subnet: "home".into(),
                endpoint_id: "ling".into(),
                instance: identified("old-key", "BOX", "mid-1", 5),
            }],
            &p,
        );
        // Snapshot mirrored to disk by the merge.
        assert!(RegistryHost::snapshot_path(&snap_dir, "home").exists());

        let mut trust = TrustStore::default();
        trust.record("home", "old-key", None, None);
        trust.record("worknet", "old-key", None, None);
        let intro = spt_net::net::pairing::wire::NodeIntro {
            label: "BOX".into(),
            machine_id: "mid-1".into(),
        };
        repair_evict_superseded(&mut trust, "home", "new-key", &intro, &snap_dir);

        assert!(
            !trust.is_trusted("home", "old-key"),
            "ceremony subnet's trust row dropped"
        );
        assert!(
            trust.is_trusted("worknet", "old-key"),
            "other subnets untouched — the ceremony proved THIS subnet's code only"
        );
        let on_disk: SubnetRegistry = serde_json::from_str(
            &std::fs::read_to_string(RegistryHost::snapshot_path(&snap_dir, "home")).unwrap(),
        )
        .unwrap();
        assert!(
            on_disk.instances("ling").is_empty(),
            "superseded rows gone from the disk snapshot"
        );
        // The marker holds the hex; consume clears the IN-MEMORY rows too
        // (else the next snapshot mirror would resurrect the disk eviction).
        assert!(RegistryHost::repair_evict_path(&snap_dir).exists());
        assert_eq!(h.consume_repair_evictions(), 1, "in-memory row evicted");
        assert!(h.rows("home", "ling").is_empty());
        assert!(
            !RegistryHost::repair_evict_path(&snap_dir).exists(),
            "marker consumed exactly once"
        );
        assert_eq!(h.consume_repair_evictions(), 0, "second consume no-ops");

        // No-match intro: nothing evicts, no marker minted.
        let mut t2 = TrustStore::default();
        t2.record("home", "unrelated", None, None);
        let other = spt_net::net::pairing::wire::NodeIntro {
            label: "ELSEWHERE".into(),
            machine_id: "mid-x".into(),
        };
        repair_evict_superseded(&mut t2, "home", "new-key", &other, &snap_dir);
        assert!(t2.is_trusted("home", "unrelated"));
        assert!(!RegistryHost::repair_evict_path(&snap_dir).exists());
    }

    // [unit->REQ-INST-7] inbound feeds gate fail-closed: non-member subnet
    // and untrusted origin write nothing; an admitted row merges and the
    // epoch lease still drops a stale follow-up.
    #[test]
    fn apply_feed_gates_and_merges_under_the_lease() {

+    /// An instance row stamped with a label + machine id (the M8 re-pair

+    fn identified(node: &str, label: &str, mid: &str, epoch: u64) -> Instance {

+            shell_adapters: Vec::new(),

+            node_label: Some(label.to_string()),

+            machine_id: Some(mid.to_string()),

+    // [unit->REQ-SUBNET-7] superseded-identity resolution: BOTH label and

+    // machine id must match a DIFFERENT key — label alone (hostname

+    // collision) and machine id alone never supersede; the joiner's own

+    fn superseded_requires_label_and_machine_id_under_other_key() {

+        let mut reg = SubnetRegistry::new();

+        reg.merge_instance("ling", identified("old-key", "BOX", "mid-1", 5));

+        reg.merge_instance("oak", identified("label-only", "BOX", "mid-9", 5));

+        reg.merge_instance("ivy", identified("mid-only", "OTHER", "mid-1", 5));

+        reg.merge_instance("elm", identified("new-key", "BOX", "mid-1", 5));

+        let intro = spt_net::net::pairing::wire::NodeIntro {

+            machine_id: "mid-1".into(),

+            superseded_identities(&reg, &intro, "new-key"),

+            vec!["old-key".to_string()],

+            "only the same-(label,mid) different-key identity supersedes"

+    // [unit->REQ-SUBNET-7] the effect leg: a completed re-pair drops the

+    // superseded identity's trust row (this subnet only), rewrites the

+    // disk snapshot without its rows, and notes it in the marker; the

+    // host's consume then clears the in-memory rows + the heard entry.

+    fn repair_evict_drops_trust_snapshot_and_memory() {

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

+        let snap_dir = dir.path().join("registry");

+            EpochSource::load_from(&dir.path().join("epoch.json")),

+        // Gossip state: the old identity advertises in home + a second subnet.

+        let p = policy("home", "old-key");

+                endpoint_id: "ling".into(),

+                instance: identified("old-key", "BOX", "mid-1", 5),

+        // Snapshot mirrored to disk by the merge.

+        assert!(RegistryHost::snapshot_path(&snap_dir, "home").exists());

+        let mut trust = TrustStore::default();

+        trust.record("home", "old-key", None, None);

+        trust.record("worknet", "old-key", None, None);

+            machine_id: "mid-1".into(),

+        repair_evict_superseded(&mut trust, "home", "new-key", &intro, &snap_dir);

+            !trust.is_trusted("home", "old-key"),

+            "ceremony subnet's trust row dropped"

+            trust.is_trusted("worknet", "old-key"),

+            "other subnets untouched — the ceremony proved THIS subnet's code only"

+        let on_disk: SubnetRegistry = serde_json::from_str(

+            &std::fs::read_to_string(RegistryHost::snapshot_path(&snap_dir, "home")).unwrap(),

+            on_disk.instances("ling").is_empty(),

+            "superseded rows gone from the disk snapshot"

+        // The marker holds the hex; consume clears the IN-MEMORY rows too

+        // (else the next snapshot mirror would resurrect the disk eviction).

+        assert!(RegistryHost::repair_evict_path(&snap_dir).exists());

+        assert_eq!(h.consume_repair_evictions(), 1, "in-memory row evicted");

+        assert!(h.rows("home", "ling").is_empty());

+            !RegistryHost::repair_evict_path(&snap_dir).exists(),

+            "marker consumed exactly once"

+        assert_eq!(h.consume_repair_evictions(), 0, "second consume no-ops");

+        // No-match intro: nothing evicts, no marker minted.

+        let mut t2 = TrustStore::default();

+        t2.record("home", "unrelated", None, None);

+        let other = spt_net::net::pairing::wire::NodeIntro {

+            machine_id: "mid-x".into(),

+        repair_evict_superseded(&mut t2, "home", "new-key", &other, &snap_dir);

+        assert!(t2.is_trusted("home", "unrelated"));

+        assert!(!RegistryHost::repair_evict_path(&snap_dir).exists());


PowerShell

cargo test -q -p spt-daemon --lib registryhost 2>&1 | Select-Object -Last 8

Run registryhost tests


running 12 tests
............
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 187 filtered out; finished in 0.13s


895	        );
896	        assert_eq!(
897	            reg.merge_instance("ling", with(Some("OLDHOST"), 1)),
898	            MergeOutcome::Stale
899	        );
900	        assert_eq!(
901	            reg.instances("ling")[0].node_label.as_deref(),
902	            Some("NEWHOST")
903	        );
904
905	        // Serde: roundtrips with the label; a pre-M7 row parses clean.
906	        let json = serde_json::to_string(&reg).unwrap();
907	        let back: SubnetRegistry = serde_json::from_str(&json).unwrap();
908	        assert_eq!(reg, back);
909	        let old: Instance =
910	            serde_json::from_str(r#"{"node":"n1","status":"Active","epoch":1}"#).unwrap();
911	        assert_eq!(old.node_label, None, "absent field parses as no label");
912	    }
913
914	    // [unit->REQ-SUBNET-3] `@node` qualifiers accept a label or key-prefix
915	    // (case-insensitive), not just the full hex; a qualifier matching several
916	    // distinct nodes refuses with paste-able candidate key prefixes —
917	    // machines sharing a hostname are never guessed between.
918	    #[test]
919	    fn node_qualifier_accepts_label_and_prefix_refuses_shared() {
920	        let labeled = |node: &str, label: &str| Instance {
921	            node_label: Some(label.to_string()),
922	            machine_id: None,
923	            ..inst(node, Status::Active, 1)
924	        };
925	        let mut reg = SubnetRegistry::new();
926	        reg.merge_instance("ling", labeled("bcead52b00112233", "HFENDULEAM"));
927	        reg.merge_instance("ling", labeled("77aa11ff00112233", "GRAVITY"));
928
929	        // Label, any case.
930	        let r = reg.resolve(&addr("ling@hfenduleam"), "local");
931	        assert!(
932	            matches!(&r, Resolution::Resolved { instance, .. } if instance.node.starts_with("bcead52b")),
933	            "label resolves case-insensitively: {r:?}"
934	        );
935	        // Key-prefix.
936	        let r = reg.resolve(&addr("ling@77aa11ff"), "local");
937	        assert!(
938	            matches!(&r, Resolution::Resolved { instance, .. } if instance.node.starts_with("77aa11ff")),
939	            "key-prefix resolves: {r:?}"

        );
        assert_eq!(
            reg.merge_instance("ling", with(Some("OLDHOST"), 1)),
            MergeOutcome::Stale
        );
        assert_eq!(
            reg.instances("ling")[0].node_label.as_deref(),
            Some("NEWHOST")
        );

        // Serde: roundtrips with the label; a pre-M7 row parses clean.
        let json = serde_json::to_string(&reg).unwrap();
        let back: SubnetRegistry = serde_json::from_str(&json).unwrap();
        assert_eq!(reg, back);
        let old: Instance =
            serde_json::from_str(r#"{"node":"n1","status":"Active","epoch":1}"#).unwrap();
        assert_eq!(old.node_label, None, "absent field parses as no label");
    }

    // [unit->REQ-SUBNET-3] `@node` qualifiers accept a label or key-prefix
    // (case-insensitive), not just the full hex; a qualifier matching several
    // distinct nodes refuses with paste-able candidate key prefixes —
    // machines sharing a hostname are never guessed between.
    #[test]
    fn node_qualifier_accepts_label_and_prefix_refuses_shared() {
        let labeled = |node: &str, label: &str| Instance {
            node_label: Some(label.to_string()),
            machine_id: None,
            ..inst(node, Status::Active, 1)
        };
        let mut reg = SubnetRegistry::new();
        reg.merge_instance("ling", labeled("bcead52b00112233", "HFENDULEAM"));
        reg.merge_instance("ling", labeled("77aa11ff00112233", "GRAVITY"));

        // Label, any case.
        let r = reg.resolve(&addr("ling@hfenduleam"), "local");
        assert!(
            matches!(&r, Resolution::Resolved { instance, .. } if instance.node.starts_with("bcead52b")),
            "label resolves case-insensitively: {r:?}"
        );
        // Key-prefix.
        let r = reg.resolve(&addr("ling@77aa11ff"), "local");
        assert!(
            matches!(&r, Resolution::Resolved { instance, .. } if instance.node.starts_with("77aa11ff")),
            "key-prefix resolves: {r:?}"


Edit

        let old: Instance =
            serde_json::from_str(r#"{"node":"n1","status":"Active","epoch":1}"#).unwrap();
        assert_eq!(old.node_label, None, "absent field parses as no label");
    }

        let old: Instance =
            serde_json::from_str(r#"{"node":"n1","status":"Active","epoch":1}"#).unwrap();
        assert_eq!(old.node_label, None, "absent field parses as no label");
        // [unit->REQ-SUBNET-7] the machine id is the same additive pattern:
        // a pre-M8 row parses clean as no-machine-id.
        assert_eq!(old.machine_id, None, "absent field parses as no machine id");
    }


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

        let old: Instance =
            serde_json::from_str(r#"{"node":"n1","status":"Active","epoch":1}"#).unwrap();
        assert_eq!(old.node_label, None, "absent field parses as no label");
    }

        let old: Instance =
            serde_json::from_str(r#"{"node":"n1","status":"Active","epoch":1}"#).unwrap();
        assert_eq!(old.node_label, None, "absent field parses as no label");
        // [unit->REQ-SUBNET-7] the machine id is the same additive pattern:
        // a pre-M8 row parses clean as no-machine-id.
        assert_eq!(old.machine_id, None, "absent field parses as no machine id");
    }

+        // [unit->REQ-SUBNET-7] the machine id is the same additive pattern:

+        // a pre-M8 row parses clean as no-machine-id.

+        assert_eq!(old.machine_id, None, "absent field parses as no machine id");

// [unit->REQ-SUBNET-3] `@node` qualifiers accept a label or key-prefix


PowerShell

cargo test -q -p spt-net 2>&1 | Select-Object -Last 6; cargo test -q -p spt-daemon --lib 2>&1 | Select-Object -Last 4

Run spt-net + spt-daemon lib suites


running 0 tests

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

....................................................................................... 174/199
.........................
test result: ok. 199 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 4.30s


cargo test -q -p spt-net --lib 2>&1 | Select-Object -Last 4; cargo test -q -p spt 2>&1 | Select-Object -Last 4; cargo test -q -p spt-daemon --tests 2>&1 | Select-Object -Last 6

Full affected-crate sweeps


i...................................................................................... 87/102
...............
test result: ok. 101 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.20s

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

running 2 tests
..
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.23s


cargo test -q -p spt-daemon 2>&1 | Select-String -Pattern "test result|FAILED|error" | Select-Object -First 25

Full spt-daemon suite


test result: ok. 199 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.75s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.24s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.26s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.41s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.14s
test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.23s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.19s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.11s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.11s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.49s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.24s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.28s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.87s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.20s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.45s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.15s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.25s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.78s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.41s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.29s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


All suites green. Activate REQ-SUBNET-5..8, regen reference, gate, commit.