2750	        if store.find(n).is_none() {
2751	            eprintln!("NO_SUCH_SUBNET:{n}");
2752	            return 2;
2753	        }
2754	    }
2755	    let daemon_running = spt_daemon::is_running();
2756	    let stall = if daemon_running {
2757	        pump_stall_warning(now_ms())
2758	    } else {
2759	        None
2760	    };
2761	    // Blocked-inbound honesty (REQ-INSTALL-7): only worth checking when the
2762	    // daemon serves — a stopped daemon is the bigger headline already.
2763	    let inbound = if daemon_running {
2764	        inbound_block_hint()
2765	    } else {
2766	        None
2767	    };
2768	    let snap_dir = perch::identity_dir().join("registry");
2769	    let regs = crate::wansend::load_snapshots(&snap_dir);
2770	    let trust = spt_store::trust::TrustStore::load();
2771	    let own_hex = spt_store::nodeid::load_or_create()
2772	        .map(|i| i.public_key().to_hex())
2773	        .unwrap_or_default();
2774	    if !nodes {
2775	        let rows = subnet_status_rows(
2776	            &store,
2777	            &regs,
2778	            &trust,
2779	            Some(&own_hex).filter(|h| !h.is_empty()).map(|h| h.as_str()),
2780	            name.as_deref(),
2781	        );
2782	        let mut out = render_subnet_status(&rows, daemon_running, hints);
2783	        if let Some(w) = stall {
2784	            out.push_str(&w);
2785	        }
2786	        if let Some(w) = inbound {
2787	            out.push_str(&w);
2788	        }
2789	        print!("{out}");
2790	        let _ = std::io::stdout().flush();
2791	        return 0;
2792	    }
2793
2794	    // --nodes: per-node rows under the hybrid liveness (decision 6).
2795	    let heard: std::collections::BTreeMap<String, u64> = std::fs::read(
2796	        spt_daemon::registryhost::RegistryHost::heard_path(&snap_dir),
2797	    )
2798	    .ok()
2799	    .and_then(|b| serde_json::from_slice(&b).ok())
2800	    .unwrap_or_default();
2801	    // Stale = silent for 3 advertisement cadences (the daemon re-gossips
2802	    // every pump period; missing a couple is jitter, missing three is news).
2803	    let stale_after_ms = 3 * spt_daemon::DaemonConfig::load()
2804	        .registry_pump_period
2805	        .as_millis() as u64;
2806	    let mut out = String::new();
2807	    // Announce the probe phase once, before any dial — a silent node costs a
2808	    // bounded dial (probe_node) and the user should know what the wait is for.
2809	    let mut announced = false;
2810	    for sub in &store.subnets {
2811	        if name.as_deref().is_some_and(|n| n != sub.name) {
2812	            continue;
2813	        }
2814	        let empty = spt_net::net::registry::SubnetRegistry::new();
2815	        let reg = regs.get(&sub.name).unwrap_or(&empty);
2816	        // Membership seed: self + trust-pinned peers (rowless members still
2817	        // render — fresh boxes and eviction-decayed offline peers alike).
2818	        let mut members: Vec<(String, Option<String>)> = Vec::new();
2819	        if !own_hex.is_empty() {
2820	            members.push((own_hex.clone(), spt_store::hostlabel::os_hostname()));
2821	        }
2822	        for peer in trust.peers_in(&sub.name) {
2823	            members.push((peer.pubkey_hex.clone(), peer.label.clone()));
2824	        }
2825	        let rows = node_status_rows(reg, &members, &heard, &own_hex, now_ms(), stale_after_ms);
2826	        if !announced && rows.iter().any(|r| r.liveness == NodeLiveness::Probe) {
2827	            eprintln!("Checking remote nodes…");
2828	            let _ = std::io::stderr().flush();
2829	            announced = true;
2830	        }
2831	        let settled: Vec<(NodeRow, bool)> = rows
2832	            .into_iter()
2833	            .map(|r| {
2834	                let online = match r.liveness {
2835	                    NodeLiveness::Online => true,
2836	                    NodeLiveness::Offline => false,
2837	                    NodeLiveness::Probe => probe_node(&r.node, &sub.name),
2838	                };
2839	                (r, online)
2840	            })
2841	            .collect();
2842	        out.push_str(&render_node_rows(&sub.name, &settled));
2843	    }
2844	    if out.is_empty() {
2845	        out.push_str(standalone_text(daemon_running));
2846	    }
2847	    if let Some(w) = stall {
2848	        out.push_str(&w);
2849	    }
2850	    if let Some(w) = inbound {
2851	        out.push_str(&w);
2852	    }
2853	    if hints {
2854	        out.push_str(HINT_FOOTER);
2855	    }
2856	    print!("{out}");
2857	    let _ = std::io::stdout().flush();
2858	    0
2859	}
2860
2861	/// The leave/prune elevation verdict (REQ-SUBNET-6): both verbs mutate
2862	/// trust material, the same class as join's machine enrollment — fail-safe,
2863	/// only confirmed elevation proceeds.
2864	// [impl->REQ-SUBNET-6]
2865	fn trust_mutation_refusal(elevation: Elevation, what: &str) -> Option<String> {
2866	    match elevation::gate_for(elevation) {
2867	        Gate::Show => None,
2868	        Gate::Fallback => Some(format!(
2869	            "ELEVATION_REQUIRED: {what} mutates this machine's trust material — re-run \
2870	             elevated (run as administrator / root)"
2871	        )),
2872	    }
2873	}
2874
2875	/// `spt subnet leave <NAME>` — membership exit (M8 decision 6, REQ-SUBNET-6):
2876	/// drop the subnet's seed, its trust rows, its serve-state row, and its
2877	/// registry snapshot on THIS node. Elevation-gated, gate-first (refused =
2878	/// nothing read, nothing removed). The remaining members keep the old seed —
2879	/// revoking this machine from the subnet is their seed rotation
2880	/// (REQ-HAZARD-PAIR-SEED-ROTATION), not something leave can do from here.
2881	// [impl->REQ-SUBNET-6]
2882	fn cmd_subnet_leave(name: &str) -> i32 {
2883	    if let Some(msg) = trust_mutation_refusal(elevation::current(), "leaving a subnet") {
2884	        if let Some(code) = try_auto_elevate() {
2885	            return code;
2886	        }
2887	        eprintln!("{}", with_elevation_hint(msg));
2888	        return EXIT_NOT_ELEVATED;
2889	    }
2890	    let mut subnets = SubnetStore::load();
2891	    if subnets.find(name).is_none() {
2892	        eprintln!("NO_SUCH_SUBNET:{name}");
2893	        return 2;
2894	    }
2895	    // The seed first (the membership fact), then the dependent material —
2896	    // each step is atomic on its own file; a failure mid-way leaves only
2897	    // harmless residue (trust rows without a subnet never dial: the pump
2898	    // iterates subnets, not trust rows).
2899	    subnets.remove(name);
2900	    if let Err(e) = subnets.save() {
2901	        eprintln!("LEAVE_FAIL:{name}: {e}");
2902	        return 1;
2903	    }
2904	    let mut trust = spt_store::trust::TrustStore::load();
2905	    let before = trust.peers.len();
2906	    trust.peers.retain(|p| p.subnet != name);
2907	    let dropped = before - trust.peers.len();
2908	    if let Err(e) = trust.save() {
2909	        eprintln!("LEAVE_TRUST_FAIL:{name}: {e} (membership already removed)");
2910	        return 1;
2911	    }
2912	    let mut att = spt_store::attachment::AttachmentStore::load();
2913	    att.set_detached(name, false);
2914	    let _ = att.save();
2915	    let snap = perch::identity_dir()
2916	        .join("registry")
2917	        .join(format!("{name}.json"));
2918	    let _ = std::fs::remove_file(snap);
2919	    eprintln!(
2920	        "LEFT:{name} (seed removed, {dropped} trust row(s) dropped). The remaining \
2921	         members still hold the old seed — rotate it there if this machine must not rejoin."
2922	    );
2923	    0
2924	}
2925
2926	/// The prune target resolution (pure, REQ-SUBNET-6): distinct trusted
2927	/// pubkeys matching a full hex, a hex prefix, or a node label
2928	/// (case-insensitive) — several distinct matches refuse with candidates,
2929	/// never a guess (the `@node` qualifier stance).
2930	// [impl->REQ-SUBNET-6]
2931	fn prune_candidates(trust: &spt_store::trust::TrustStore, query: &str) -> Vec<String> {
2932	    let q = query.to_ascii_lowercase();
2933	    let mut out: Vec<String> = Vec::new();
2934	    for p in &trust.peers {
2935	        let hit = p.pubkey_hex == q
2936	            || p.pubkey_hex.starts_with(&q)
2937	            || p.label
2938	                .as_deref()
2939	                .is_some_and(|l| l.eq_ignore_ascii_case(query));
2940	        if hit && !out.contains(&p.pubkey_hex) {
2941	            out.push(p.pubkey_hex.clone());
2942	        }
2943	    }
2944	    out
2945	}
2946
2947	/// `spt subnet prune <node>` — remove a dead identity's trust rows + its
2948	/// registry snapshot rows (M8 decision 7, REQ-SUBNET-6). The pump dials
2949	/// trust rows every tick, so a re-paired/retired machine's stale identity
2950	/// costs a failed dial forever until pruned. Elevation-gated, gate-first.
2951	/// Own identity refuses (leave/identity tooling owns that, not prune).
2952	// [impl->REQ-SUBNET-6]
2953	fn cmd_subnet_prune(node: &str) -> i32 {
2954	    if let Some(msg) = trust_mutation_refusal(elevation::current(), "pruning a node's trust rows") {
2955	        if let Some(code) = try_auto_elevate() {
2956	            return code;
2957	        }
2958	        eprintln!("{}", with_elevation_hint(msg));
2959	        return EXIT_NOT_ELEVATED;
2960	    }
2961	    let mut trust = spt_store::trust::TrustStore::load();
2962	    let candidates = prune_candidates(&trust, node);
2963	    let hex = match candidates.as_slice() {
2964	        [] => {
2965	            eprintln!("NO_MATCH:{node} — no trusted identity matches (hex, prefix, or label)");
2966	            return 1;
2967	        }
2968	        [one] => one.clone(),
2969	        several => {
2970	            let pfx: Vec<String> = several
2971	                .iter()
2972	                .map(|h| format!("{}…", spt_net::net::registry::key_prefix(h)))
2973	                .collect();
2974	            eprintln!(
2975	                "AMBIGUOUS:{node} — several identities match: {} (use a longer prefix)",
2976	                pfx.join(", ")
2977	            );
2978	            return 1;
2979	        }
2980	    };
2981	    if let Ok(own) = spt_store::nodeid::load_or_create() {
2982	        if own.public_key().to_hex() == hex {
2983	            eprintln!("OWN_IDENTITY:{node} — prune removes OTHER nodes' rows; use `spt subnet leave` to exit a subnet");
2984	            return 1;
2985	        }
2986	    }
2987	    let before = trust.peers.len();
2988	    let subnets_hit: Vec<String> = trust
2989	        .peers
2990	        .iter()
2991	        .filter(|p| p.pubkey_hex == hex)
2992	        .map(|p| p.subnet.clone())
2993	        .collect();
2994	    trust.peers.retain(|p| p.pubkey_hex != hex);
2995	    let dropped = before - trust.peers.len();
2996	    if let Err(e) = trust.save() {
2997	        eprintln!("PRUNE_FAIL:{node}: {e}");
2998	        return 1;
2999	    }
3000	    // Registry snapshot rows: the dead identity's advertised rows would
3001	    // otherwise linger until the 4.10 eviction window; prune is explicit,
3002	    // so clear them now (every snapshot — the identity is dead everywhere).
3003	    let snap_dir = perch::identity_dir().join("registry");
3004	    let mut evicted = 0usize;
3005	    for (name, mut reg) in crate::wansend::load_snapshots(&snap_dir) {
3006	        // Prune is an EXPLICIT forget: drop the dead identity's routable rows
3007	        // AND its node-level label (silence-decay keeps the label, but a
3008	        // pruned identity is gone for good — REQ-SUBNET-3 / REQ-SUBNET-6).
3009	        let n = reg.evict_nodes(|node| node == hex) + reg.evict_node_labels(|node| node == hex);
3010	        if n > 0 {
3011	            evicted += n;
3012	            if let Ok(json) = serde_json::to_string_pretty(&reg) {
3013	                let _ = spt_store::atomic::atomic_write_string(
3014	                    &snap_dir.join(format!("{name}.json")),
3015	                    &json,
3016	                );
3017	            }
3018	        }
3019	    }

        if store.find(n).is_none() {
            eprintln!("NO_SUCH_SUBNET:{n}");
            return 2;
        }
    }
    let daemon_running = spt_daemon::is_running();
    let stall = if daemon_running {
        pump_stall_warning(now_ms())
    } else {
        None
    };
    // Blocked-inbound honesty (REQ-INSTALL-7): only worth checking when the
    // daemon serves — a stopped daemon is the bigger headline already.
    let inbound = if daemon_running {
        inbound_block_hint()
    } else {
        None
    };
    let snap_dir = perch::identity_dir().join("registry");
    let regs = crate::wansend::load_snapshots(&snap_dir);
    let trust = spt_store::trust::TrustStore::load();
    let own_hex = spt_store::nodeid::load_or_create()
        .map(|i| i.public_key().to_hex())
        .unwrap_or_default();
    if !nodes {
        let rows = subnet_status_rows(
            &store,
            &regs,
            &trust,
            Some(&own_hex).filter(|h| !h.is_empty()).map(|h| h.as_str()),
            name.as_deref(),
        );
        let mut out = render_subnet_status(&rows, daemon_running, hints);
        if let Some(w) = stall {
            out.push_str(&w);
        }
        if let Some(w) = inbound {
            out.push_str(&w);
        }
        print!("{out}");
        let _ = std::io::stdout().flush();
        return 0;
    }

    // --nodes: per-node rows under the hybrid liveness (decision 6).
    let heard: std::collections::BTreeMap<String, u64> = std::fs::read(
        spt_daemon::registryhost::RegistryHost::heard_path(&snap_dir),
    )
    .ok()
    .and_then(|b| serde_json::from_slice(&b).ok())
    .unwrap_or_default();
    // Stale = silent for 3 advertisement cadences (the daemon re-gossips
    // every pump period; missing a couple is jitter, missing three is news).
    let stale_after_ms = 3 * spt_daemon::DaemonConfig::load()
        .registry_pump_period
        .as_millis() as u64;
    let mut out = String::new();
    // Announce the probe phase once, before any dial — a silent node costs a
    // bounded dial (probe_node) and the user should know what the wait is for.
    let mut announced = false;
    for sub in &store.subnets {
        if name.as_deref().is_some_and(|n| n != sub.name) {
            continue;
        }
        let empty = spt_net::net::registry::SubnetRegistry::new();
        let reg = regs.get(&sub.name).unwrap_or(&empty);
        // Membership seed: self + trust-pinned peers (rowless members still
        // render — fresh boxes and eviction-decayed offline peers alike).
        let mut members: Vec<(String, Option<String>)> = Vec::new();
        if !own_hex.is_empty() {
            members.push((own_hex.clone(), spt_store::hostlabel::os_hostname()));
        }
        for peer in trust.peers_in(&sub.name) {
            members.push((peer.pubkey_hex.clone(), peer.label.clone()));
        }
        let rows = node_status_rows(reg, &members, &heard, &own_hex, now_ms(), stale_after_ms);
        if !announced && rows.iter().any(|r| r.liveness == NodeLiveness::Probe) {
            eprintln!("Checking remote nodes…");
            let _ = std::io::stderr().flush();
            announced = true;
        }
        let settled: Vec<(NodeRow, bool)> = rows
            .into_iter()
            .map(|r| {
                let online = match r.liveness {
                    NodeLiveness::Online => true,
                    NodeLiveness::Offline => false,
                    NodeLiveness::Probe => probe_node(&r.node, &sub.name),
                };
                (r, online)
            })
            .collect();
        out.push_str(&render_node_rows(&sub.name, &settled));
    }
    if out.is_empty() {
        out.push_str(standalone_text(daemon_running));
    }
    if let Some(w) = stall {
        out.push_str(&w);
    }
    if let Some(w) = inbound {
        out.push_str(&w);
    }
    if hints {
        out.push_str(HINT_FOOTER);
    }
    print!("{out}");
    let _ = std::io::stdout().flush();
    0
}

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

/// `spt subnet leave <NAME>` — membership exit (M8 decision 6, REQ-SUBNET-6):
/// drop the subnet's seed, its trust rows, its serve-state row, and its
/// registry snapshot on THIS node. Elevation-gated, gate-first (refused =
/// nothing read, nothing removed). The remaining members keep the old seed —
/// revoking this machine from the subnet is their seed rotation
/// (REQ-HAZARD-PAIR-SEED-ROTATION), not something leave can do from here.
// [impl->REQ-SUBNET-6]
fn cmd_subnet_leave(name: &str) -> i32 {
    if let Some(msg) = trust_mutation_refusal(elevation::current(), "leaving a subnet") {
        if let Some(code) = try_auto_elevate() {
            return code;
        }
        eprintln!("{}", with_elevation_hint(msg));
        return EXIT_NOT_ELEVATED;
    }
    let mut subnets = SubnetStore::load();
    if subnets.find(name).is_none() {
        eprintln!("NO_SUCH_SUBNET:{name}");
        return 2;
    }
    // The seed first (the membership fact), then the dependent material —
    // each step is atomic on its own file; a failure mid-way leaves only
    // harmless residue (trust rows without a subnet never dial: the pump
    // iterates subnets, not trust rows).
    subnets.remove(name);
    if let Err(e) = subnets.save() {
        eprintln!("LEAVE_FAIL:{name}: {e}");
        return 1;
    }
    let mut trust = spt_store::trust::TrustStore::load();
    let before = trust.peers.len();
    trust.peers.retain(|p| p.subnet != name);
    let dropped = before - trust.peers.len();
    if let Err(e) = trust.save() {
        eprintln!("LEAVE_TRUST_FAIL:{name}: {e} (membership already removed)");
        return 1;
    }
    let mut att = spt_store::attachment::AttachmentStore::load();
    att.set_detached(name, false);
    let _ = att.save();
    let snap = perch::identity_dir()
        .join("registry")
        .join(format!("{name}.json"));
    let _ = std::fs::remove_file(snap);
    eprintln!(
        "LEFT:{name} (seed removed, {dropped} trust row(s) dropped). The remaining \
         members still hold the old seed — rotate it there if this machine must not rejoin."
    );
    0
}

/// The prune target resolution (pure, REQ-SUBNET-6): distinct trusted
/// pubkeys matching a full hex, a hex prefix, or a node label
/// (case-insensitive) — several distinct matches refuse with candidates,
/// never a guess (the `@node` qualifier stance).
// [impl->REQ-SUBNET-6]
fn prune_candidates(trust: &spt_store::trust::TrustStore, query: &str) -> Vec<String> {
    let q = query.to_ascii_lowercase();
    let mut out: Vec<String> = Vec::new();
    for p in &trust.peers {
        let hit = p.pubkey_hex == q
            || p.pubkey_hex.starts_with(&q)
            || p.label
                .as_deref()
                .is_some_and(|l| l.eq_ignore_ascii_case(query));
        if hit && !out.contains(&p.pubkey_hex) {
            out.push(p.pubkey_hex.clone());
        }
    }
    out
}

/// `spt subnet prune <node>` — remove a dead identity's trust rows + its
/// registry snapshot rows (M8 decision 7, REQ-SUBNET-6). The pump dials
/// trust rows every tick, so a re-paired/retired machine's stale identity
/// costs a failed dial forever until pruned. Elevation-gated, gate-first.
/// Own identity refuses (leave/identity tooling owns that, not prune).
// [impl->REQ-SUBNET-6]
fn cmd_subnet_prune(node: &str) -> i32 {
    if let Some(msg) = trust_mutation_refusal(elevation::current(), "pruning a node's trust rows") {
        if let Some(code) = try_auto_elevate() {
            return code;
        }
        eprintln!("{}", with_elevation_hint(msg));
        return EXIT_NOT_ELEVATED;
    }
    let mut trust = spt_store::trust::TrustStore::load();
    let candidates = prune_candidates(&trust, node);
    let hex = match candidates.as_slice() {
        [] => {
            eprintln!("NO_MATCH:{node} — no trusted identity matches (hex, prefix, or label)");
            return 1;
        }
        [one] => one.clone(),
        several => {
            let pfx: Vec<String> = several
                .iter()
                .map(|h| format!("{}…", spt_net::net::registry::key_prefix(h)))
                .collect();
            eprintln!(
                "AMBIGUOUS:{node} — several identities match: {} (use a longer prefix)",
                pfx.join(", ")
            );
            return 1;
        }
    };
    if let Ok(own) = spt_store::nodeid::load_or_create() {
        if own.public_key().to_hex() == hex {
            eprintln!("OWN_IDENTITY:{node} — prune removes OTHER nodes' rows; use `spt subnet leave` to exit a subnet");
            return 1;
        }
    }
    let before = trust.peers.len();
    let subnets_hit: Vec<String> = trust
        .peers
        .iter()
        .filter(|p| p.pubkey_hex == hex)
        .map(|p| p.subnet.clone())
        .collect();
    trust.peers.retain(|p| p.pubkey_hex != hex);
    let dropped = before - trust.peers.len();
    if let Err(e) = trust.save() {
        eprintln!("PRUNE_FAIL:{node}: {e}");
        return 1;
    }
    // Registry snapshot rows: the dead identity's advertised rows would
    // otherwise linger until the 4.10 eviction window; prune is explicit,
    // so clear them now (every snapshot — the identity is dead everywhere).
    let snap_dir = perch::identity_dir().join("registry");
    let mut evicted = 0usize;
    for (name, mut reg) in crate::wansend::load_snapshots(&snap_dir) {
        // Prune is an EXPLICIT forget: drop the dead identity's routable rows
        // AND its node-level label (silence-decay keeps the label, but a
        // pruned identity is gone for good — REQ-SUBNET-3 / REQ-SUBNET-6).
        let n = reg.evict_nodes(|node| node == hex) + reg.evict_node_labels(|node| node == hex);
        if n > 0 {
            evicted += n;
            if let Ok(json) = serde_json::to_string_pretty(&reg) {
                let _ = spt_store::atomic::atomic_write_string(
                    &snap_dir.join(format!("{name}.json")),
                    &json,
                );
            }
        }
    }


3019	    }
3020	    eprintln!(
3021	        "PRUNED:{}… ({dropped} trust row(s) [{}], {evicted} registry row(s)). \
3022	         Note: trust-delete alone is not revocation — rotate the seed if the \
3023	         machine itself is untrusted (it still holds the subnet seed).",
3024	        spt_net::net::registry::key_prefix(&hex),
3025	        subnets_hit.join(", "),
3026	    );
3027	    0
3028	}
3029
3030	/// `spt subnet detach|attach <NAME> [--save]` — the per-subnet serve-state
3031	/// flip (M8 decision 6, REQ-SUBNET-5). Ungated: a serve-state toggle
3032	/// mutates no trust material (unlike `leave`/`prune`). The daemon's pump,
3033	/// pairing responder, and meet rotation re-read the live state per round,
3034	/// so the flip takes effect within one cadence without a restart. `--save`
3035	/// also writes the startup default into `daemon.json` (an unsaved flip
3036	/// does not survive a daemon restart — the boot reset re-seeds from
3037	/// config).
3038	// [impl->REQ-SUBNET-5]
3039	fn cmd_subnet_attachment(name: &str, detach: bool, save: bool) -> i32 {
3040	    if SubnetStore::load().find(name).is_none() {
3041	        eprintln!("NO_SUCH_SUBNET:{name}");
3042	        return 2;
3043	    }
3044	    let mut att = spt_store::attachment::AttachmentStore::load();
3045	    let edge = att.set_detached(name, detach);
3046	    if let Err(e) = att.save() {
3047	        eprintln!("ATTACHMENT_SAVE_FAIL:{name}: {e}");
3048	        return 1;
3049	    }
3050	    let verb = if detach { "DETACHED" } else { "ATTACHED" };
3051	    if edge {
3052	        eprintln!("{verb}:{name} (takes effect within one pump cadence)");
3053	    } else {
3054	        eprintln!("NO_EDGE:{name} (already {})", verb.to_lowercase());
3055	    }
3056	    if save {
3057	        let path = spt_daemon::DaemonConfig::default_path();
3058	        let mut cfg = spt_daemon::DaemonConfig::load();

    }
    eprintln!(
        "PRUNED:{}… ({dropped} trust row(s) [{}], {evicted} registry row(s)). \
         Note: trust-delete alone is not revocation — rotate the seed if the \
         machine itself is untrusted (it still holds the subnet seed).",
        spt_net::net::registry::key_prefix(&hex),
        subnets_hit.join(", "),
    );
    0
}

/// `spt subnet detach|attach <NAME> [--save]` — the per-subnet serve-state
/// flip (M8 decision 6, REQ-SUBNET-5). Ungated: a serve-state toggle
/// mutates no trust material (unlike `leave`/`prune`). The daemon's pump,
/// pairing responder, and meet rotation re-read the live state per round,
/// so the flip takes effect within one cadence without a restart. `--save`
/// also writes the startup default into `daemon.json` (an unsaved flip
/// does not survive a daemon restart — the boot reset re-seeds from
/// config).
// [impl->REQ-SUBNET-5]
fn cmd_subnet_attachment(name: &str, detach: bool, save: bool) -> i32 {
    if SubnetStore::load().find(name).is_none() {
        eprintln!("NO_SUCH_SUBNET:{name}");
        return 2;
    }
    let mut att = spt_store::attachment::AttachmentStore::load();
    let edge = att.set_detached(name, detach);
    if let Err(e) = att.save() {
        eprintln!("ATTACHMENT_SAVE_FAIL:{name}: {e}");
        return 1;
    }
    let verb = if detach { "DETACHED" } else { "ATTACHED" };
    if edge {
        eprintln!("{verb}:{name} (takes effect within one pump cadence)");
    } else {
        eprintln!("NO_EDGE:{name} (already {})", verb.to_lowercase());
    }
    if save {
        let path = spt_daemon::DaemonConfig::default_path();
        let mut cfg = spt_daemon::DaemonConfig::load();


6310	            Some(SubnetCmd::Leave { name }) if name == "home"
6311	        ));
6312	        assert!(matches!(
6313	            subnet_cmd(&["spt", "subnet", "prune", "09ef831e"]),
6314	            Some(SubnetCmd::Prune { node }) if node == "09ef831e"
6315	        ));
6316	        assert!(
6317	            parse(&["spt", "subnet", "leave"]).is_err(),
6318	            "leave needs a name"
6319	        );
6320	        assert!(
6321	            parse(&["spt", "subnet", "prune"]).is_err(),
6322	            "prune needs a node"
6323	        );
6324	    }
6325
6326	    // [unit->REQ-SUBNET-6] leave/prune are elevation-gated with the join
6327	    // gate's fail-safe posture: NotElevated and Unknown both refuse.
6328	    #[test]
6329	    fn trust_mutation_gate_refuses_without_confirmed_elevation() {
6330	        assert!(trust_mutation_refusal(Elevation::Elevated, "leaving").is_none());
6331	        assert!(trust_mutation_refusal(Elevation::NotElevated, "leaving").is_some());
6332	        assert!(trust_mutation_refusal(Elevation::Unknown, "leaving").is_some());
6333	    }
6334
6335	    // [unit->REQ-SUBNET-6] prune target resolution: full hex, unambiguous
6336	    // prefix, and label all resolve; a shared prefix yields BOTH candidates
6337	    // (the caller refuses); unknown queries match nothing.
6338	    #[test]
6339	    fn prune_candidates_resolve_never_guess() {
6340	        let mut trust = spt_store::trust::TrustStore::default();
6341	        trust.record("bignet", "09ef831e00112233", None, None);
6342	        trust.record("bignet", "09ef831e00112233", None, None); // idempotent
6343	        trust.record("worknet", "09ef831e00112233", None, None); // same id, 2nd subnet
6344	        trust.record("bignet", "09aa000011223344", Some("GRAVITY"), None);
6345
6346	        // Full hex + unambiguous prefix + label each resolve to one identity.
6347	        assert_eq!(
6348	            prune_candidates(&trust, "09ef831e00112233"),
6349	            vec!["09ef831e00112233".to_string()]
6350	        );
6351	        assert_eq!(
6352	            prune_candidates(&trust, "09ef"),
6353	            vec!["09ef831e00112233".to_string()],
6354	            "cross-subnet rows of ONE identity are one candidate"
6355	        );
6356	        assert_eq!(
6357	            prune_candidates(&trust, "gravity"),
6358	            vec!["09aa000011223344".to_string()],
6359	            "label resolves case-insensitively"
6360	        );
6361	        // A shared prefix surfaces both — the command refuses, never guesses.
6362	        assert_eq!(prune_candidates(&trust, "09").len(), 2);
6363	        // Unknown query matches nothing.
6364	        assert!(prune_candidates(&trust, "ff00").is_empty());
6365	    }
6366
6367	    // [unit->REQ-SUBNET-4] join is elevation-gated with the same fail-safe
6368	    // posture as the seed-reveal paths: only CONFIRMED elevation proceeds;
6369	    // NotElevated and the undeterminable Unknown both refuse (before any
6370	    // prompt or network work — the command checks this gate first).
6371	    #[test]
6372	    fn join_gate_refuses_without_confirmed_elevation() {
6373	        assert!(join_elevation_refusal(Elevation::Elevated).is_none());
6374	        for level in [Elevation::NotElevated, Elevation::Unknown] {
6375	            let msg = join_elevation_refusal(level).expect("refused");
6376	            assert!(msg.contains("ELEVATION_REQUIRED"));
6377	            assert!(msg.contains("administrator"), "actionable fix named: {msg}");
6378	        }
6379	    }
6380
6381	    // [unit->REQ-SUBNET-1] the `pair` namespace is deleted outright (no shim).
6382	    #[test]
6383	    fn pair_namespace_is_gone() {
6384	        assert!(parse(&["spt", "pair", "show-totp"]).is_err());
6385	        assert!(parse(&["spt", "pair"]).is_err());
6386	    }
6387
6388	    // [unit->REQ-SUBNET-1] elevated create prints the full joining material —
6389	    // code, otpauth URI, terminal QR, join hint, expiry (local-time, floored
6390	    // to a minute that is always still true — the ±1 window's end is a
6391	    // :00/:30 boundary) — and requests the persist.
6392	    #[test]
6393	    fn create_prints_joining_material_when_elevated() {
6394	        let mut store = SubnetStore::default();
6395	        let now = 1_111_111_109;
6396	        let r = decide_create(
6397	            resolve(&mut store, None, Some("home".into())),
6398	            now,
6399	            Elevation::Elevated,
6400	        );
6401	        assert_eq!(r.code, 0);
6402	        assert!(r.save, "a freshly minted subnet must be persisted on show");
6403	        let out = r.out.expect("elevated create prints");
6404	        let seed = store.find("home").unwrap().seed_bytes().unwrap();
6405	        assert!(
6406	            out.starts_with(&TotpSeed::from_bytes(seed).code_at(now)),
6407	            "current code first"
6408	        );
6409	        assert!(out.contains("otpauth://"), "provisioning URI present");
6410	        assert!(out.contains('█'), "terminal QR rendered (unicode blocks)");
6411	        assert!(
6412	            out.contains("spt subnet join home"),
6413	            "join hint names the subnet"
6414	        );
6415	        assert!(out.contains("Code expires at "), "expiry line present");
6416	        assert!(out.contains("show-code"), "re-provisioning pointer present");
6417	        assert!(r.msg.contains("SUBNET CREATED: home"));
6418	        // The expiry render is HH:MM (the local-tz wall render of the ±1
6419	        // window's end; the exact value is tz-dependent, the shape is not).
6420	        let hm = code_expiry_local_hm(now);
6421	        assert_eq!(hm.len(), 5);
6422	        assert_eq!(hm.as_bytes()[2], b':');
6423	        assert!(out.contains(&format!("expires at {hm}")));
6424	    }
6425
6426	    // [unit->REQ-PAIR-3] show-code is the re-provisioning surface: the
6427	    // elevated output carries the SAME material as create — code, URI, QR,
6428	    // expiry (ADR-0005 "re-provision the QR from any still-trusted node").
6429	    #[test]
6430	    fn show_code_prints_reprovisioning_material() {
6431	        let mut store = store_with(&["home"]);
6432	        let now = 1_111_111_109;
6433	        let r = decide_show_code(resolve(&mut store, None, None), now, Elevation::Elevated);
6434	        assert_eq!(r.code, 0);
6435	        let out = r.out.expect("elevated show prints");
6436	        assert!(out.contains("otpauth://"));
6437	        assert!(out.contains('█'), "QR present on re-provision: {out}");
6438	        assert!(out.contains("Code expires at "), "expiry line present");
6439	    }
6440
6441	    // [unit->REQ-SUBNET-1] [unit->REQ-SUBNET-4] an unelevated create refuses
6442	    // without minting or leaking (the seed-reveal posture; exit 3 like
6443	    // show-totp) — the create half of the membership-mutation gates.
6444	    #[test]
6445	    fn create_not_elevated_refuses_without_saving() {
6446	        let mut store = SubnetStore::default();
6447	        let r = decide_create(
6448	            resolve(&mut store, None, Some("home".into())),
6449	            0,
6450	            Elevation::NotElevated,
6451	        );
6452	        assert_eq!(r.code, EXIT_NOT_ELEVATED);
6453	        assert!(r.out.is_none(), "no material leaks on the refused path");
6454	        assert!(!r.save, "a refused create persists no mint");
6455	        assert!(r.msg.contains("ELEVATION_REQUIRED"));
6456	    }
6457
6458	    // [unit->REQ-SUBNET-1] creating an existing name is a usage error (the
6459	    // shared resolution core's Exists arm rides through decide_create).
6460	    #[test]
6461	    fn create_existing_name_is_usage_error() {
6462	        let mut store = store_with(&["home"]);
6463	        let r = decide_create(
6464	            resolve(&mut store, None, Some("home".into())),
6465	            0,
6466	            Elevation::Elevated,
6467	        );
6468	        assert_eq!(r.code, 2);
6469	        assert!(r.msg.contains("SUBNET_EXISTS"));
6470	    }
6471
6472	    // [unit->REQ-SUBNET-1] [unit->REQ-SUBNET-8] the status view: zero
6473	    // subnets renders the DAEMON-AWARE standalone explanation (M8 decision
6474	    // 11 — never "working" while the daemon is down); members render
6475	    // name/node/endpoint rows; the hint footer rides ONLY the bare view
6476	    // (M8 decision 12); secrets never appear (M7 decision 5).
6477	    #[test]
6478	    fn subnet_status_renders_rows_and_hints_never_secrets() {
6479	        use spt_net::net::registry::{Instance, Status, SubnetRegistry};
6480
6481	        // Zero subnets, daemon up: standalone + messaging-works blurb.
6482	        let empty = render_subnet_status(&[], true, true);
6483	        assert!(empty.contains("No subnets registered"));
6484	        assert!(empty.contains("Local messaging works"));
6485	        assert!(empty.contains("spt subnet create"));
6486	        assert!(empty.contains("show-code"));
6487	        // Zero subnets, daemon DOWN: says so — never implies messaging works.
6488	        let down = render_subnet_status(&[], false, true);
6489	        assert!(down.contains("No subnets registered"));
6490	        assert!(down.contains("NOT running"));
6491	        assert!(
6492	            !down.contains("Local messaging works"),
6493	            "a down daemon must not read as working"
6494	        );
6495	        // The explicit-status view drops the hint footer (decision 12).
6496	        let no_hints = render_subnet_status(&[], true, false);
6497	        assert!(!no_hints.contains("hint:"), "status view carries no hints");
6498
6499	        // A member subnet with gossip: counts come from the snapshot.
6500	        let store = store_with(&["home"]);
6501	        let mut reg = SubnetRegistry::new();
6502	        for (id, node) in [("lea", "aa"), ("sergey", "bb"), ("lea2", "aa")] {
6503	            reg.merge_instance(
6504	                id,
6505	                Instance {
6506	                    node: node.into(),
6507	                    status: Status::Active,
6508	                    epoch: 1,
6509	                    resources: None,
6510	                    last_active_ms: None,
6511	                    shell_adapters: Vec::new(),
6512	                    node_label: None,
6513	                    machine_id: None,
6514	                },
6515	            );
6516	        }
6517	        let regs = std::collections::BTreeMap::from([("home".to_string(), reg)]);
6518	        let trust = spt_store::trust::TrustStore::default();
6519	        let rows = subnet_status_rows(&store, &regs, &trust, Some("aa"), None);
6520	        assert_eq!(rows.len(), 1);
6521	        assert_eq!((rows[0].nodes, rows[0].endpoints), (2, 3));
6522	        let view = render_subnet_status(&rows, true, true);
6523	        assert!(view.contains("home"));
6524	        assert!(view.contains("ENDPOINTS"));
6525	        assert!(view.contains("hint:"), "bare view keeps the hint footer");
6526
6527	        // MEMBERSHIP is trust-derived, rows enrich (the re-join field bug,
6528	        // 2026-06-06): a member node with ZERO advertised endpoints — fresh
6529	        // box, or an offline peer whose rows the 4.10 eviction decayed —
6530	        // still counts. Self (cc, rowless) + a pinned rowless peer (dd) +
6531	        // the two row-nodes = 4.
6532	        let mut pinned = spt_store::trust::TrustStore::default();
6533	        pinned.record("home", "dd", None, None);
6534	        let rows = subnet_status_rows(&store, &regs, &pinned, Some("cc"), None);
6535	        assert_eq!(
6536	            (rows[0].nodes, rows[0].endpoints),
6537	            (4, 3),
6538	            "rowless self + rowless pinned peer still count as nodes"
6539	        );
6540	        let seed_hex = {
6541	            use std::fmt::Write as _;
6542	            store
6543	                .find("home")
6544	                .unwrap()
6545	                .seed_bytes()
6546	                .unwrap()
6547	                .iter()
6548	                .fold(String::new(), |mut s, b| {
6549	                    let _ = write!(s, "{b:02x}");
6550	                    s
6551	                })
6552	        };
6553	        assert!(
6554	            !view.contains(&seed_hex),
6555	            "the status view never carries seed material"
6556	        );
6557
6558	        // A missing snapshot degrades to this-node-alone, not a panic/zero —
6559	        // even when the identity itself could not load (own_hex None).
6560	        let no_gossip = subnet_status_rows(
6561	            &store,
6562	            &std::collections::BTreeMap::new(),
6563	            &trust,
6564	            None,
6565	            None,
6566	        );
6567	        assert_eq!((no_gossip[0].nodes, no_gossip[0].endpoints), (1, 0));
6568	    }
6569
6570	    // [unit->REQ-SUBNET-1] [unit->REQ-SUBNET-3] the --nodes hybrid liveness
6571	    // verdict is settled by the cheap signals first: own node online, an
6572	    // all-Offline node offline, a recently-heard node online without a
6573	    // probe, and ONLY the silent-but-routable remainder asks for a dial;
6574	    // rows carry the advertised label + [active/total] endpoint counts.
6575	    #[test]
6576	    fn node_status_rows_settle_cheap_signals_probe_only_stale() {
6577	        use spt_net::net::registry::{Instance, Status, SubnetRegistry};
6578	        let inst = |node: &str, status: Status, label: Option<&str>| Instance {
6579	            node: node.into(),
6580	            status,
6581	            epoch: 1,
6582	            resources: None,
6583	            last_active_ms: None,
6584	            shell_adapters: Vec::new(),
6585	            node_label: label.map(str::to_string),
6586	            machine_id: None,
6587	        };
6588	        let mut reg = SubnetRegistry::new();
6589	        reg.merge_instance("a1", inst("self00aa", Status::Active, Some("ME")));
6590	        reg.merge_instance("a2", inst("self00aa", Status::Dormant, Some("ME")));
6591	        reg.merge_instance("b1", inst("heard0bb", Status::Active, Some("FRESH")));
6592	        reg.merge_instance("c1", inst("dead00cc", Status::Offline, None));
6593	        reg.merge_instance("d1", inst("stale0dd", Status::Active, Some("QUIET")));
6594
6595	        let now = 1_000_000u64;
6596	        let heard = std::collections::BTreeMap::from([("heard0bb".to_string(), now - 10_000)]);
6597	        // Membership seed: self (also row-backed) + a ROWLESS pinned peer
6598	        // (the re-join field bug, 2026-06-06 — a fresh/evicted member must
6599	        // still render, [0/0], probed rather than presumed).
6600	        let members = vec![
6601	            ("self00aa".to_string(), Some("ME".to_string())),
6602	            ("rowless0".to_string(), Some("FRESHBOX".to_string())),
6603	        ];
6604	        let rows = node_status_rows(&reg, &members, &heard, "self00aa", now, 33_000);
6605	        let by: std::collections::BTreeMap<&str, &NodeRow> =
6606	            rows.iter().map(|r| (r.node.as_str(), r)).collect();
6607
6608	        assert_eq!(
6609	            by["self00aa"].liveness,
6610	            NodeLiveness::Online,
6611	            "own node never probes"
6612	        );
6613	        assert_eq!((by["self00aa"].active, by["self00aa"].total), (1, 2));
6614	        assert_eq!(
6615	            by["heard0bb"].liveness,
6616	            NodeLiveness::Online,
6617	            "recently heard ⇒ online"
6618	        );
6619	        assert_eq!(
6620	            by["dead00cc"].liveness,
6621	            NodeLiveness::Offline,
6622	            "all-Offline ⇒ offline"
6623	        );
6624	        assert_eq!(
6625	            by["stale0dd"].liveness,
6626	            NodeLiveness::Probe,
6627	            "silent + routable ⇒ probe"
6628	        );
6629	        assert_eq!(by["stale0dd"].label.as_deref(), Some("QUIET"));
6630	        assert_eq!(
6631	            (by["rowless0"].active, by["rowless0"].total),
6632	            (0, 0),
6633	            "rowless member renders [0/0]"
6634	        );
6635	        assert_eq!(
6636	            by["rowless0"].liveness,
6637	            NodeLiveness::Probe,
6638	            "rowless member probes — absence is not evidence of offline"
6639	        );
6640	        assert_eq!(
6641	            by["rowless0"].label.as_deref(),
6642	            Some("FRESHBOX"),
6643	            "trust label renders"
6644	        );
6645
6646	        // Render: a unique label renders CLEAN (no key-prefix parens),
6647	        // bare prefix when unlabeled, online/offline + [active/total];
6648	        // never the full pubkey alone.
6649	        let settled: Vec<(NodeRow, bool)> = rows
6650	            .into_iter()
6651	            .map(|r| {
6652	                let online = r.liveness == NodeLiveness::Online;
6653	                (r, online)
6654	            })
6655	            .collect();
6656	        let view = render_node_rows("home", &settled);
6657	        assert!(view.contains("SUBNET home"));
6658	        assert!(
6659	            view.contains("ME") && !view.contains("ME (self00aa…)"),
6660	            "a unique label renders clean, no prefix parens: {view}"
6661	        );
6662	        assert!(
6663	            view.contains("dead00cc…"),
6664	            "unlabeled node renders the bare prefix"
6665	        );
6666	        assert!(view.contains("[1/2]"), "active/total counts render: {view}");
6667	        assert!(view.contains("online"));
6668	        assert!(view.contains("offline"));
6669
6670	        // Zero rows: an explanatory line, not an empty table.
6671	        let none = render_node_rows("home", &[]);
6672	        assert!(none.contains("no gossip yet"));
6673	    }
6674
6675	    // [unit->REQ-SUBNET-3] the node-level label carrier names an ENDPOINT-LESS
6676	    // peer: a member with no instance rows and an unlabeled trust pin still
6677	    // gets its hostname from reg.node_labels(), WITHOUT gaining a phantom
6678	    // endpoint ([0/0] holds) or having its liveness forced offline. A peer that
6679	    // has an instance-row label keeps it (the map never overrides). A node only
6680	    // in the label map but not a known member is NOT shown (absence from
6681	    // members = not ours to render).
6682	    #[test]
6683	    fn node_level_label_names_an_endpointless_peer() {
6684	        use spt_net::net::registry::SubnetRegistry;
6685	        let mut reg = SubnetRegistry::new();
6686	        // Endpoint-less peer "idle0bb": no instance rows, only a gossiped label.
6687	        reg.merge_node_label("idle0bb", "HFENDULEAM".into(), 1);
6688	        // A stranger in the label map but not a member — must not appear.
6689	        reg.merge_node_label("ghost0cc", "GHOST".into(), 1);
6690
6691	        let now = 1_000_000u64;
6692	        let heard = std::collections::BTreeMap::from([("idle0bb".to_string(), now - 5_000)]);
6693	        // "idle0bb" is a trust-pinned member but its pin is UNLABELED (label None).
6694	        let members = vec![("idle0bb".to_string(), None)];
6695	        let rows = node_status_rows(&reg, &members, &heard, "self00aa", now, 33_000);
6696	        let by: std::collections::BTreeMap<&str, &NodeRow> =
6697	            rows.iter().map(|r| (r.node.as_str(), r)).collect();
6698
6699	        assert_eq!(
6700	            by["idle0bb"].label.as_deref(),
6701	            Some("HFENDULEAM"),
6702	            "the gossiped node-level label names the endpoint-less peer"
6703	        );
6704	        assert_eq!(
6705	            (by["idle0bb"].active, by["idle0bb"].total),
6706	            (0, 0),
6707	            "a node label is not an endpoint — counts stay [0/0]"
6708	        );
6709	        assert_eq!(

            Some(SubnetCmd::Leave { name }) if name == "home"
        ));
        assert!(matches!(
            subnet_cmd(&["spt", "subnet", "prune", "09ef831e"]),
            Some(SubnetCmd::Prune { node }) if node == "09ef831e"
        ));
        assert!(
            parse(&["spt", "subnet", "leave"]).is_err(),
            "leave needs a name"
        );
        assert!(
            parse(&["spt", "subnet", "prune"]).is_err(),
            "prune needs a node"
        );
    }

    // [unit->REQ-SUBNET-6] leave/prune are elevation-gated with the join
    // gate's fail-safe posture: NotElevated and Unknown both refuse.
    #[test]
    fn trust_mutation_gate_refuses_without_confirmed_elevation() {
        assert!(trust_mutation_refusal(Elevation::Elevated, "leaving").is_none());
        assert!(trust_mutation_refusal(Elevation::NotElevated, "leaving").is_some());
        assert!(trust_mutation_refusal(Elevation::Unknown, "leaving").is_some());
    }

    // [unit->REQ-SUBNET-6] prune target resolution: full hex, unambiguous
    // prefix, and label all resolve; a shared prefix yields BOTH candidates
    // (the caller refuses); unknown queries match nothing.
    #[test]
    fn prune_candidates_resolve_never_guess() {
        let mut trust = spt_store::trust::TrustStore::default();
        trust.record("bignet", "09ef831e00112233", None, None);
        trust.record("bignet", "09ef831e00112233", None, None); // idempotent
        trust.record("worknet", "09ef831e00112233", None, None); // same id, 2nd subnet
        trust.record("bignet", "09aa000011223344", Some("GRAVITY"), None);

        // Full hex + unambiguous prefix + label each resolve to one identity.
        assert_eq!(
            prune_candidates(&trust, "09ef831e00112233"),
            vec!["09ef831e00112233".to_string()]
        );
        assert_eq!(
            prune_candidates(&trust, "09ef"),
            vec!["09ef831e00112233".to_string()],
            "cross-subnet rows of ONE identity are one candidate"
        );
        assert_eq!(
            prune_candidates(&trust, "gravity"),
            vec!["09aa000011223344".to_string()],
            "label resolves case-insensitively"
        );
        // A shared prefix surfaces both — the command refuses, never guesses.
        assert_eq!(prune_candidates(&trust, "09").len(), 2);
        // Unknown query matches nothing.
        assert!(prune_candidates(&trust, "ff00").is_empty());
    }

    // [unit->REQ-SUBNET-4] join is elevation-gated with the same fail-safe
    // posture as the seed-reveal paths: only CONFIRMED elevation proceeds;
    // NotElevated and the undeterminable Unknown both refuse (before any
    // prompt or network work — the command checks this gate first).
    #[test]
    fn join_gate_refuses_without_confirmed_elevation() {
        assert!(join_elevation_refusal(Elevation::Elevated).is_none());
        for level in [Elevation::NotElevated, Elevation::Unknown] {
            let msg = join_elevation_refusal(level).expect("refused");
            assert!(msg.contains("ELEVATION_REQUIRED"));
            assert!(msg.contains("administrator"), "actionable fix named: {msg}");
        }
    }

    // [unit->REQ-SUBNET-1] the `pair` namespace is deleted outright (no shim).
    #[test]
    fn pair_namespace_is_gone() {
        assert!(parse(&["spt", "pair", "show-totp"]).is_err());
        assert!(parse(&["spt", "pair"]).is_err());
    }

    // [unit->REQ-SUBNET-1] elevated create prints the full joining material —
    // code, otpauth URI, terminal QR, join hint, expiry (local-time, floored
    // to a minute that is always still true — the ±1 window's end is a
    // :00/:30 boundary) — and requests the persist.
    #[test]
    fn create_prints_joining_material_when_elevated() {
        let mut store = SubnetStore::default();
        let now = 1_111_111_109;
        let r = decide_create(
            resolve(&mut store, None, Some("home".into())),
            now,
            Elevation::Elevated,
        );
        assert_eq!(r.code, 0);
        assert!(r.save, "a freshly minted subnet must be persisted on show");
        let out = r.out.expect("elevated create prints");
        let seed = store.find("home").unwrap().seed_bytes().unwrap();
        assert!(
            out.starts_with(&TotpSeed::from_bytes(seed).code_at(now)),
            "current code first"
        );
        assert!(out.contains("otpauth://"), "provisioning URI present");
        assert!(out.contains('█'), "terminal QR rendered (unicode blocks)");
        assert!(
            out.contains("spt subnet join home"),
            "join hint names the subnet"
        );
        assert!(out.contains("Code expires at "), "expiry line present");
        assert!(out.contains("show-code"), "re-provisioning pointer present");
        assert!(r.msg.contains("SUBNET CREATED: home"));
        // The expiry render is HH:MM (the local-tz wall render of the ±1
        // window's end; the exact value is tz-dependent, the shape is not).
        let hm = code_expiry_local_hm(now);
        assert_eq!(hm.len(), 5);
        assert_eq!(hm.as_bytes()[2], b':');
        assert!(out.contains(&format!("expires at {hm}")));
    }

    // [unit->REQ-PAIR-3] show-code is the re-provisioning surface: the
    // elevated output carries the SAME material as create — code, URI, QR,
    // expiry (ADR-0005 "re-provision the QR from any still-trusted node").
    #[test]
    fn show_code_prints_reprovisioning_material() {
        let mut store = store_with(&["home"]);
        let now = 1_111_111_109;
        let r = decide_show_code(resolve(&mut store, None, None), now, Elevation::Elevated);
        assert_eq!(r.code, 0);
        let out = r.out.expect("elevated show prints");
        assert!(out.contains("otpauth://"));
        assert!(out.contains('█'), "QR present on re-provision: {out}");
        assert!(out.contains("Code expires at "), "expiry line present");
    }

    // [unit->REQ-SUBNET-1] [unit->REQ-SUBNET-4] an unelevated create refuses
    // without minting or leaking (the seed-reveal posture; exit 3 like
    // show-totp) — the create half of the membership-mutation gates.
    #[test]
    fn create_not_elevated_refuses_without_saving() {
        let mut store = SubnetStore::default();
        let r = decide_create(
            resolve(&mut store, None, Some("home".into())),
            0,
            Elevation::NotElevated,
        );
        assert_eq!(r.code, EXIT_NOT_ELEVATED);
        assert!(r.out.is_none(), "no material leaks on the refused path");
        assert!(!r.save, "a refused create persists no mint");
        assert!(r.msg.contains("ELEVATION_REQUIRED"));
    }

    // [unit->REQ-SUBNET-1] creating an existing name is a usage error (the
    // shared resolution core's Exists arm rides through decide_create).
    #[test]
    fn create_existing_name_is_usage_error() {
        let mut store = store_with(&["home"]);
        let r = decide_create(
            resolve(&mut store, None, Some("home".into())),
            0,
            Elevation::Elevated,
        );
        assert_eq!(r.code, 2);
        assert!(r.msg.contains("SUBNET_EXISTS"));
    }

    // [unit->REQ-SUBNET-1] [unit->REQ-SUBNET-8] the status view: zero
    // subnets renders the DAEMON-AWARE standalone explanation (M8 decision
    // 11 — never "working" while the daemon is down); members render
    // name/node/endpoint rows; the hint footer rides ONLY the bare view
    // (M8 decision 12); secrets never appear (M7 decision 5).
    #[test]
    fn subnet_status_renders_rows_and_hints_never_secrets() {
        use spt_net::net::registry::{Instance, Status, SubnetRegistry};

        // Zero subnets, daemon up: standalone + messaging-works blurb.
        let empty = render_subnet_status(&[], true, true);
        assert!(empty.contains("No subnets registered"));
        assert!(empty.contains("Local messaging works"));
        assert!(empty.contains("spt subnet create"));
        assert!(empty.contains("show-code"));
        // Zero subnets, daemon DOWN: says so — never implies messaging works.
        let down = render_subnet_status(&[], false, true);
        assert!(down.contains("No subnets registered"));
        assert!(down.contains("NOT running"));
        assert!(
            !down.contains("Local messaging works"),
            "a down daemon must not read as working"
        );
        // The explicit-status view drops the hint footer (decision 12).
        let no_hints = render_subnet_status(&[], true, false);
        assert!(!no_hints.contains("hint:"), "status view carries no hints");

        // A member subnet with gossip: counts come from the snapshot.
        let store = store_with(&["home"]);
        let mut reg = SubnetRegistry::new();
        for (id, node) in [("lea", "aa"), ("sergey", "bb"), ("lea2", "aa")] {
            reg.merge_instance(
                id,
                Instance {
                    node: node.into(),
                    status: Status::Active,
                    epoch: 1,
                    resources: None,
                    last_active_ms: None,
                    shell_adapters: Vec::new(),
                    node_label: None,
                    machine_id: None,
                },
            );
        }
        let regs = std::collections::BTreeMap::from([("home".to_string(), reg)]);
        let trust = spt_store::trust::TrustStore::default();
        let rows = subnet_status_rows(&store, &regs, &trust, Some("aa"), None);
        assert_eq!(rows.len(), 1);
        assert_eq!((rows[0].nodes, rows[0].endpoints), (2, 3));
        let view = render_subnet_status(&rows, true, true);
        assert!(view.contains("home"));
        assert!(view.contains("ENDPOINTS"));
        assert!(view.contains("hint:"), "bare view keeps the hint footer");

        // MEMBERSHIP is trust-derived, rows enrich (the re-join field bug,
        // 2026-06-06): a member node with ZERO advertised endpoints — fresh
        // box, or an offline peer whose rows the 4.10 eviction decayed —
        // still counts. Self (cc, rowless) + a pinned rowless peer (dd) +
        // the two row-nodes = 4.
        let mut pinned = spt_store::trust::TrustStore::default();
        pinned.record("home", "dd", None, None);
        let rows = subnet_status_rows(&store, &regs, &pinned, Some("cc"), None);
        assert_eq!(
            (rows[0].nodes, rows[0].endpoints),
            (4, 3),
            "rowless self + rowless pinned peer still count as nodes"
        );
        let seed_hex = {
            use std::fmt::Write as _;
            store
                .find("home")
                .unwrap()
                .seed_bytes()
                .unwrap()
                .iter()
                .fold(String::new(), |mut s, b| {
                    let _ = write!(s, "{b:02x}");
                    s
                })
        };
        assert!(
            !view.contains(&seed_hex),
            "the status view never carries seed material"
        );

        // A missing snapshot degrades to this-node-alone, not a panic/zero —
        // even when the identity itself could not load (own_hex None).
        let no_gossip = subnet_status_rows(
            &store,
            &std::collections::BTreeMap::new(),
            &trust,
            None,
            None,
        );
        assert_eq!((no_gossip[0].nodes, no_gossip[0].endpoints), (1, 0));
    }

    // [unit->REQ-SUBNET-1] [unit->REQ-SUBNET-3] the --nodes hybrid liveness
    // verdict is settled by the cheap signals first: own node online, an
    // all-Offline node offline, a recently-heard node online without a
    // probe, and ONLY the silent-but-routable remainder asks for a dial;
    // rows carry the advertised label + [active/total] endpoint counts.
    #[test]
    fn node_status_rows_settle_cheap_signals_probe_only_stale() {
        use spt_net::net::registry::{Instance, Status, SubnetRegistry};
        let inst = |node: &str, status: Status, label: Option<&str>| Instance {
            node: node.into(),
            status,
            epoch: 1,
            resources: None,
            last_active_ms: None,
            shell_adapters: Vec::new(),
            node_label: label.map(str::to_string),
            machine_id: None,
        };
        let mut reg = SubnetRegistry::new();
        reg.merge_instance("a1", inst("self00aa", Status::Active, Some("ME")));
        reg.merge_instance("a2", inst("self00aa", Status::Dormant, Some("ME")));
        reg.merge_instance("b1", inst("heard0bb", Status::Active, Some("FRESH")));
        reg.merge_instance("c1", inst("dead00cc", Status::Offline, None));
        reg.merge_instance("d1", inst("stale0dd", Status::Active, Some("QUIET")));

        let now = 1_000_000u64;
        let heard = std::collections::BTreeMap::from([("heard0bb".to_string(), now - 10_000)]);
        // Membership seed: self (also row-backed) + a ROWLESS pinned peer
        // (the re-join field bug, 2026-06-06 — a fresh/evicted member must
        // still render, [0/0], probed rather than presumed).
        let members = vec![
            ("self00aa".to_string(), Some("ME".to_string())),
            ("rowless0".to_string(), Some("FRESHBOX".to_string())),
        ];
        let rows = node_status_rows(&reg, &members, &heard, "self00aa", now, 33_000);
        let by: std::collections::BTreeMap<&str, &NodeRow> =
            rows.iter().map(|r| (r.node.as_str(), r)).collect();

        assert_eq!(
            by["self00aa"].liveness,
            NodeLiveness::Online,
            "own node never probes"
        );
        assert_eq!((by["self00aa"].active, by["self00aa"].total), (1, 2));
        assert_eq!(
            by["heard0bb"].liveness,
            NodeLiveness::Online,
            "recently heard ⇒ online"
        );
        assert_eq!(
            by["dead00cc"].liveness,
            NodeLiveness::Offline,
            "all-Offline ⇒ offline"
        );
        assert_eq!(
            by["stale0dd"].liveness,
            NodeLiveness::Probe,
            "silent + routable ⇒ probe"
        );
        assert_eq!(by["stale0dd"].label.as_deref(), Some("QUIET"));
        assert_eq!(
            (by["rowless0"].active, by["rowless0"].total),
            (0, 0),
            "rowless member renders [0/0]"
        );
        assert_eq!(
            by["rowless0"].liveness,
            NodeLiveness::Probe,
            "rowless member probes — absence is not evidence of offline"
        );
        assert_eq!(
            by["rowless0"].label.as_deref(),
            Some("FRESHBOX"),
            "trust label renders"
        );

        // Render: a unique label renders CLEAN (no key-prefix parens),
        // bare prefix when unlabeled, online/offline + [active/total];
        // never the full pubkey alone.
        let settled: Vec<(NodeRow, bool)> = rows
            .into_iter()
            .map(|r| {
                let online = r.liveness == NodeLiveness::Online;
                (r, online)
            })
            .collect();
        let view = render_node_rows("home", &settled);
        assert!(view.contains("SUBNET home"));
        assert!(
            view.contains("ME") && !view.contains("ME (self00aa…)"),
            "a unique label renders clean, no prefix parens: {view}"
        );
        assert!(
            view.contains("dead00cc…"),
            "unlabeled node renders the bare prefix"
        );
        assert!(view.contains("[1/2]"), "active/total counts render: {view}");
        assert!(view.contains("online"));
        assert!(view.contains("offline"));

        // Zero rows: an explanatory line, not an empty table.
        let none = render_node_rows("home", &[]);
        assert!(none.contains("no gossip yet"));
    }

    // [unit->REQ-SUBNET-3] the node-level label carrier names an ENDPOINT-LESS
    // peer: a member with no instance rows and an unlabeled trust pin still
    // gets its hostname from reg.node_labels(), WITHOUT gaining a phantom
    // endpoint ([0/0] holds) or having its liveness forced offline. A peer that
    // has an instance-row label keeps it (the map never overrides). A node only
    // in the label map but not a known member is NOT shown (absence from
    // members = not ours to render).
    #[test]
    fn node_level_label_names_an_endpointless_peer() {
        use spt_net::net::registry::SubnetRegistry;
        let mut reg = SubnetRegistry::new();
        // Endpoint-less peer "idle0bb": no instance rows, only a gossiped label.
        reg.merge_node_label("idle0bb", "HFENDULEAM".into(), 1);
        // A stranger in the label map but not a member — must not appear.
        reg.merge_node_label("ghost0cc", "GHOST".into(), 1);

        let now = 1_000_000u64;
        let heard = std::collections::BTreeMap::from([("idle0bb".to_string(), now - 5_000)]);
        // "idle0bb" is a trust-pinned member but its pin is UNLABELED (label None).
        let members = vec![("idle0bb".to_string(), None)];
        let rows = node_status_rows(&reg, &members, &heard, "self00aa", now, 33_000);
        let by: std::collections::BTreeMap<&str, &NodeRow> =
            rows.iter().map(|r| (r.node.as_str(), r)).collect();

        assert_eq!(
            by["idle0bb"].label.as_deref(),
            Some("HFENDULEAM"),
            "the gossiped node-level label names the endpoint-less peer"
        );
        assert_eq!(
            (by["idle0bb"].active, by["idle0bb"].total),
            (0, 0),
            "a node label is not an endpoint — counts stay [0/0]"
        );
        assert_eq!(


Enough recon. Executing bottom-up. Start: delete trust.rs, drop mod + trust_file.


Bash

rm crates/spt-store/src/trust.rs && echo deleted

Delete trust.rs


deleted


spt-store: delete trust.rs, drop mod + trust_file

Delete crates/spt-store/src/trust.rs, remove `pub mod trust` from lib.rs, remove perch::trust_file (keep trust_dir for access/grants).

spt-net wire.rs: strip trust from ceremony

Remove trust param + TrustOutcome from run_responder/run_initiator/responder_admitted; drop ResponderOutcome.trust field; migrate wire.rs tests to roster membership.

pairhost.rs: drop trust write

Remove TrustStore load/save/args in both legs; update run_responder/run_initiator calls; demote warn-on-change to machine_id notif at repair_evict_superseded point.

propagate.rs serve gate trust->roster

serve_update signature trust:&TrustStore -> roster:&RosterStore; both is_member-any checks; dispatch.rs:326 caller.

peerloop.rs THE WIDEN

push target trust.peers_in -> roster.members_in; reuse roster loaded for sync gate; drop PumpPaths.trust + TrustStore load.

registryhost.rs: repair_evict drop trust

repair_evict_superseded drop &mut TrustStore param (roster-only now); migrate its unit tests.

CLI cli.rs repoint to roster

subnet status/list/prune trust->roster (prune=local tombstone); 1305 load; render unit tests.

xtask + misc comment refs

xtask main.rs:897 trust dump -> roster; nethost/notifsync/sync/access doc comment refs.

Migrate daemon tests trust->roster

twohost.rs, propagate.rs, peerloop.rs, dispatch.rs tests: TrustStore -> RosterStore membership.

mesh.rs: un-ignore + regression assert

Un-ignore D0 staggered+star harness; migrate trust seeding to roster; add no-third-party-row origin==author assert.

Activate REQ-MESH-3 + complete REQ-MESH-5

traceable-reqs.toml: REQ-MESH-3 [impl,unit,int], REQ-MESH-5 [impl,unit]; run traceable-reqs check; clippy; daemon suite.


Edit

pub mod syncmerge;
pub mod trust;
pub mod visibility;


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

//! `spt-store` — the persistence commodity layer for spt-core.
//!
//! Sits directly above `spt-proto` (R-ARCH-1 layering): it serializes proto
//! types to disk (spool, registry, info.json) through a single atomic-write
//! primitive. Stable on-disk formats are copied verbatim from the sister
//! project (ADR-0001); the EBUSY-retry hardening on the write path is the
//! clean-room addition demanded by KNOWN-HAZARDS 5.2.
//!
// Second layer of the acyclically-layered crate stack (R-ARCH-1): spt-store
// depends only on spt-proto (one layer down), never the reverse — the
// dependency edge is one-way, so the workspace graph stays acyclic.
// [impl->REQ-ARCH-1]

pub mod access;
pub mod atomic;
pub mod attachment;
pub mod branchstore;
pub mod contextmark;
pub mod contextstore;
mod db;
pub mod epoch;
pub mod gitrun;
pub mod grants;
pub mod history;
pub mod home;
pub mod hostlabel;
pub mod info;
pub mod liveness;
pub mod nodeid;
pub mod notif;
pub mod peeraddrs;
pub mod perch;
pub mod proc;
pub mod project;
pub mod registry;
pub mod rename;
pub mod roster;
pub mod seed;
pub mod shellinfo;
pub mod spool;
pub mod subnet;
pub mod syncmerge;
pub mod trust;
pub mod visibility;
pub mod xfer;

pub mod spool;
pub mod subnet;
pub mod syncmerge;
-pub mod trust;
pub mod visibility;
pub mod xfer;


/// Node-local **durable** identity/security material: `<spt_home>/identity/`
/// (`docs/STORAGE.md`). Holds `node.key`, the [`trust_dir`] store, and
/// [`subnet_file`]. **Never git-tracked, never synced** — node-bound secret
/// material (the node keypair + subnet TOTP seeds). Pure path composition.

/// Node-local **durable** identity/security material: `<spt_home>/identity/`
/// (`docs/STORAGE.md`). Holds `node.key`, the [`trust_dir`] access/grants
/// store, and [`subnet_file`]. **Never git-tracked, never synced** — node-bound
/// secret material (the node keypair + subnet TOTP seeds). Pure path composition.


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

/// Node-local **durable** identity/security material: `<spt_home>/identity/`
/// (`docs/STORAGE.md`). Holds `node.key`, the [`trust_dir`] store, and
/// [`subnet_file`]. **Never git-tracked, never synced** — node-bound secret
/// material (the node keypair + subnet TOTP seeds). Pure path composition.

/// Node-local **durable** identity/security material: `<spt_home>/identity/`
/// (`docs/STORAGE.md`). Holds `node.key`, the [`trust_dir`] access/grants
/// store, and [`subnet_file`]. **Never git-tracked, never synced** — node-bound
/// secret material (the node keypair + subnet TOTP seeds). Pure path composition.

-/// (`docs/STORAGE.md`). Holds `node.key`, the [`trust_dir`] store, and

-/// [`subnet_file`]. **Never git-tracked, never synced** — node-bound secret

-/// material (the node keypair + subnet TOTP seeds). Pure path composition.

+/// (`docs/STORAGE.md`). Holds `node.key`, the [`trust_dir`] access/grants

+/// store, and [`subnet_file`]. **Never git-tracked, never synced** — node-bound

+/// secret material (the node keypair + subnet TOTP seeds). Pure path composition.


/// The trust store directory: `<spt_home>/identity/trust/` — authorized peer
/// node pubkeys (TOFU, `docs/STORAGE.md`). Node-local, never synced.
// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]
pub fn trust_dir() -> PathBuf {
    identity_dir().join("trust")
}

/// The trusted-peers file: `<spt_home>/identity/trust/peers.json` — the TOFU store
/// of authorized peer node pubkeys (R-PAIR-2). Node-local, never git-tracked/synced.
// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]
pub fn trust_file() -> PathBuf {
    trust_dir().join("peers.json")
}

/// The endpoint access whitelist: `<spt_home>/identity/trust/access.json`

/// The local security-material directory: `<spt_home>/identity/trust/` — homes
/// the endpoint [`access_file`] whitelist, the [`recent_outbound_file`]
/// reply-correlation log, and the [`grants_file`] consent store
/// (`docs/STORAGE.md`). Node-local, never synced. (The Mesh-D6 cutover removed
/// the TOFU `peers.json` trust store that once lived here — membership is now
/// the [`roster_file`]; the dir name is incidental.)
// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]
pub fn trust_dir() -> PathBuf {
    identity_dir().join("trust")
}

/// The endpoint access whitelist: `<spt_home>/identity/trust/access.json`


/// The trust store directory: `<spt_home>/identity/trust/` — authorized peer
/// node pubkeys (TOFU, `docs/STORAGE.md`). Node-local, never synced.
// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]
pub fn trust_dir() -> PathBuf {
    identity_dir().join("trust")
}

/// The trusted-peers file: `<spt_home>/identity/trust/peers.json` — the TOFU store
/// of authorized peer node pubkeys (R-PAIR-2). Node-local, never git-tracked/synced.
// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]
pub fn trust_file() -> PathBuf {
    trust_dir().join("peers.json")
}

/// The endpoint access whitelist: `<spt_home>/identity/trust/access.json`

/// The local security-material directory: `<spt_home>/identity/trust/` — homes
/// the endpoint [`access_file`] whitelist, the [`recent_outbound_file`]
/// reply-correlation log, and the [`grants_file`] consent store
/// (`docs/STORAGE.md`). Node-local, never synced. (The Mesh-D6 cutover removed
/// the TOFU `peers.json` trust store that once lived here — membership is now
/// the [`roster_file`]; the dir name is incidental.)
// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]
pub fn trust_dir() -> PathBuf {
    identity_dir().join("trust")
}

/// The endpoint access whitelist: `<spt_home>/identity/trust/access.json`

-/// The trust store directory: `<spt_home>/identity/trust/` — authorized peer

-/// node pubkeys (TOFU, `docs/STORAGE.md`). Node-local, never synced.

+/// The local security-material directory: `<spt_home>/identity/trust/` — homes

+/// the endpoint [`access_file`] whitelist, the [`recent_outbound_file`]

+/// reply-correlation log, and the [`grants_file`] consent store

+/// (`docs/STORAGE.md`). Node-local, never synced. (The Mesh-D6 cutover removed

+/// the TOFU `peers.json` trust store that once lived here — membership is now

+/// the [`roster_file`]; the dir name is incidental.)

-/// The trusted-peers file: `<spt_home>/identity/trust/peers.json` — the TOFU store

-/// of authorized peer node pubkeys (R-PAIR-2). Node-local, never git-tracked/synced.

/// (ADR-0009, REQ-SEC-1). Security material next to the trust store —

/// node-local, never git-tracked/synced.


Now fix doc-link refs in access.rs and roster.rs that point to the deleted `crate::trust`.


70	        Self::load_from(&access_file())
71	    }
72
73	    /// Load from an explicit path (the testable core). Absent or corrupt ⇒
74	    /// empty, same degrade stance as [`TrustStore`](crate::trust::TrustStore):
75	    /// a damaged file must not brick the node. Note the failure direction is
76	    /// **open** (ADR-0009's default), not closed — the whitelist is a
77	    /// same-subnet convenience boundary layered *inside* pairing trust, not
78	    /// the system's outer wall.
79	    pub fn load_from(path: &Path) -> AccessStore {

        Self::load_from(&access_file())
    }

    /// Load from an explicit path (the testable core). Absent or corrupt ⇒
    /// empty, same degrade stance as [`TrustStore`](crate::trust::TrustStore):
    /// a damaged file must not brick the node. Note the failure direction is
    /// **open** (ADR-0009's default), not closed — the whitelist is a
    /// same-subnet convenience boundary layered *inside* pairing trust, not
    /// the system's outer wall.
    pub fn load_from(path: &Path) -> AccessStore {


    /// Load from an explicit path (the testable core). Absent or corrupt ⇒
    /// empty, same degrade stance as [`TrustStore`](crate::trust::TrustStore):
    /// a damaged file must not brick the node. Note the failure direction is
    /// **open** (ADR-0009's default), not closed — the whitelist is a
    /// same-subnet convenience boundary layered *inside* pairing trust, not
    /// the system's outer wall.

    /// Load from an explicit path (the testable core). Absent or corrupt ⇒
    /// empty, same degrade stance as [`RosterStore`](crate::roster::RosterStore):
    /// a damaged file must not brick the node. Note the failure direction is
    /// **open** (ADR-0009's default), not closed — the whitelist is a
    /// same-subnet convenience boundary layered *inside* subnet membership, not
    /// the system's outer wall.


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

    /// Load from an explicit path (the testable core). Absent or corrupt ⇒
    /// empty, same degrade stance as [`TrustStore`](crate::trust::TrustStore):
    /// a damaged file must not brick the node. Note the failure direction is
    /// **open** (ADR-0009's default), not closed — the whitelist is a
    /// same-subnet convenience boundary layered *inside* pairing trust, not
    /// the system's outer wall.

    /// Load from an explicit path (the testable core). Absent or corrupt ⇒
    /// empty, same degrade stance as [`RosterStore`](crate::roster::RosterStore):
    /// a damaged file must not brick the node. Note the failure direction is
    /// **open** (ADR-0009's default), not closed — the whitelist is a
    /// same-subnet convenience boundary layered *inside* subnet membership, not
    /// the system's outer wall.

-    /// empty, same degrade stance as [`TrustStore`](crate::trust::TrustStore):

+    /// empty, same degrade stance as [`RosterStore`](crate::roster::RosterStore):

-    /// same-subnet convenience boundary layered *inside* pairing trust, not

+    /// same-subnet convenience boundary layered *inside* subnet membership, not

pub fn load_from(path: &Path) -> AccessStore {


//! over seed-proof'd member connections (D4). Node-local, durable, replicated only
//! over the pairing/P2P channel, **never git-tracked or context-synced** —
//! alongside [`subnet.json`](crate::subnet) and [`peers.json`](crate::trust)
//! under [`identity/`](crate::perch::identity_dir).

//! over seed-proof'd member connections (D4). Node-local, durable, replicated only
//! over the pairing/P2P channel, **never git-tracked or context-synced** —
//! alongside [`subnet.json`](crate::subnet) under
//! [`identity/`](crate::perch::identity_dir). (Mesh-D6 made this the sole
//! membership record — the former `peers.json` TOFU trust store is gone.)


<tool_use_error>String to replace not found in file.
String: //! over seed-proof'd member connections (D4). Node-local, durable, replicated only
//! over the pairing/P2P channel, **never git-tracked or context-synced** —
//! alongside [`subnet.json`](crate::subnet) and [`peers.json`](crate::trust)
//! under [`identity/`](crate::perch::identity_dir).
(note: Edit also tried swapping \uXXXX escapes and their characters; neither form matched, so the mismatch is likely elsewhere in old_string. Re-read the file and copy the exact surrounding text.)</tool_use_error>

Error: String to replace not found in file.
String: //! over seed-proof'd member connections (D4). Node-local, durable, replicated only
//! over the pairing/P2P channel, **never git-tracked or context-synced** —
//! alongside [`subnet.json`](crate::subnet) and [`peers.json`](crate::trust)
//! under [`identity/`](crate::perch::identity_dir).
(note: Edit also tried swapping \uXXXX escapes and their characters; neither form matched, so the mismatch is likely elsewhere in old_string. Re-read the file and copy the exact surrounding text.)


16	//! Seeded **in full at pairing** — the seed-holder hands the joiner its whole
17	//! current roster (incl. offline members), folding in the deferred pairing-time
18	//! hostname capture + post-join address seeding. Thereafter it merges over
19	//! seed-proof'd member connections (D4). Node-local, durable, replicated only
20	//! over the pairing/P2P channel, **never git-tracked or context-synced** —
21	//! alongside [`subnet.json`](crate::subnet) and [`peers.json`](crate::trust)
22	//! under [`identity/`](crate::perch::identity_dir).
23	//!

//! Seeded **in full at pairing** — the seed-holder hands the joiner its whole
//! current roster (incl. offline members), folding in the deferred pairing-time
//! hostname capture + post-join address seeding. Thereafter it merges over
//! seed-proof'd member connections (D4). Node-local, durable, replicated only
//! over the pairing/P2P channel, **never git-tracked or context-synced** —
//! alongside [`subnet.json`](crate::subnet) and [`peers.json`](crate::trust)
//! under [`identity/`](crate::perch::identity_dir).
//!


//! over the pairing/P2P channel, **never git-tracked or context-synced** —
//! alongside [`subnet.json`](crate::subnet) and [`peers.json`](crate::trust)
//! under [`identity/`](crate::perch::identity_dir).

//! over the pairing/P2P channel, **never git-tracked or context-synced** —
//! alongside [`subnet.json`](crate::subnet) under
//! [`identity/`](crate::perch::identity_dir). (Mesh-D6 made this the sole
//! membership record — the former `peers.json` TOFU trust store is gone.)


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

//! over the pairing/P2P channel, **never git-tracked or context-synced** —
//! alongside [`subnet.json`](crate::subnet) and [`peers.json`](crate::trust)
//! under [`identity/`](crate::perch::identity_dir).

//! over the pairing/P2P channel, **never git-tracked or context-synced** —
//! alongside [`subnet.json`](crate::subnet) under
//! [`identity/`](crate::perch::identity_dir). (Mesh-D6 made this the sole
//! membership record — the former `peers.json` TOFU trust store is gone.)

//! hostname capture + post-join address seeding. Thereafter it merges over

//! seed-proof'd member connections (D4). Node-local, durable, replicated only

-//! alongside [`subnet.json`](crate::subnet) and [`peers.json`](crate::trust)

+//! alongside [`subnet.json`](crate::subnet) under

+//! [`identity/`](crate::perch::identity_dir). (Mesh-D6 made this the sole

+//! membership record — the former `peers.json` TOFU trust store is gone.)


    /// unreadable or corrupt file also degrades to empty rather than panicking —
    /// a damaged roster must not brick the node (re-pair/merge rebuilds it), the
    /// same stance as [`SubnetStore`](crate::subnet)/[`TrustStore`](crate::trust).

    /// unreadable or corrupt file also degrades to empty rather than panicking —
    /// a damaged roster must not brick the node (re-pair/merge rebuilds it), the
    /// same stance as [`SubnetStore`](crate::subnet).


    /// unreadable or corrupt file also degrades to empty rather than panicking —
    /// a damaged roster must not brick the node (re-pair/merge rebuilds it), the
    /// same stance as [`SubnetStore`](crate::subnet)/[`TrustStore`](crate::trust).

    /// unreadable or corrupt file also degrades to empty rather than panicking —
    /// a damaged roster must not brick the node (re-pair/merge rebuilds it), the
    /// same stance as [`SubnetStore`](crate::subnet).

/// Load from an explicit path (the testable core). Absent ⇒ empty; an

-    /// same stance as [`SubnetStore`](crate::subnet)/[`TrustStore`](crate::trust).

+    /// same stance as [`SubnetStore`](crate::subnet).

pub fn load_from(path: &Path) -> RosterStore {


pairhost.rs: drop trust write

propagate.rs serve gate trust->roster

peerloop.rs THE WIDEN

CLI cli.rs repoint to roster

xtask + misc comment refs

Migrate daemon tests trust->roster

mesh.rs: un-ignore + regression assert


spt-store layer done. Build it alone to verify before moving up.


Bash

cargo build -p spt-store 2>&1 | tail -20; echo "===EXIT:${PIPESTATUS[0]}==="

Build spt-store


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


spt-net wire.rs — strip trust from ceremony. Production code first.


Edit

//! This is the transport glue that runs the TOTP-SPAKE2 ceremony (the crypto core of
//! [`spake`](super::spake) + [`transcript`](super::transcript)) over a real iroh bidi
//! stream on a dedicated pre-trust **pairing ALPN** ([`SPT_PAIR_ALPN`]), gated by the
//! subnet-global [rate limiter](super::ratelimit), and writes the confirmed peer into
//! each side's [`TrustStore`]. It turns "two machines pair via one TOTP code" from a
//! same-process unit test into an over-the-wire exchange.

//! This is the transport glue that runs the TOTP-SPAKE2 ceremony (the crypto core of
//! [`spake`](super::spake) + [`transcript`](super::transcript)) over a real iroh bidi
//! stream on a dedicated pre-trust **pairing ALPN** ([`SPT_PAIR_ALPN`]), gated by the
//! subnet-global [rate limiter](super::ratelimit). It turns "two machines pair via one
//! TOTP code" from a same-process unit test into an over-the-wire exchange.
//!
//! Membership is recorded by the daemon's pairing host into the
//! [`RosterStore`](spt_store::roster) (Mesh-D6): this wire confirms the peer's
//! identity and transfers the seed + roster; the former per-side `peers.json` TOFU
//! trust write is gone.


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)

//! This is the transport glue that runs the TOTP-SPAKE2 ceremony (the crypto core of
//! [`spake`](super::spake) + [`transcript`](super::transcript)) over a real iroh bidi
//! stream on a dedicated pre-trust **pairing ALPN** ([`SPT_PAIR_ALPN`]), gated by the
//! subnet-global [rate limiter](super::ratelimit), and writes the confirmed peer into
//! each side's [`TrustStore`]. It turns "two machines pair via one TOTP code" from a
//! same-process unit test into an over-the-wire exchange.

//! This is the transport glue that runs the TOTP-SPAKE2 ceremony (the crypto core of
//! [`spake`](super::spake) + [`transcript`](super::transcript)) over a real iroh bidi
//! stream on a dedicated pre-trust **pairing ALPN** ([`SPT_PAIR_ALPN`]), gated by the
//! subnet-global [rate limiter](super::ratelimit). It turns "two machines pair via one
//! TOTP code" from a same-process unit test into an over-the-wire exchange.
//!
//! Membership is recorded by the daemon's pairing host into the
//! [`RosterStore`](spt_store::roster) (Mesh-D6): this wire confirms the peer's
//! identity and transfers the seed + roster; the former per-side `peers.json` TOFU
//! trust write is gone.

-//! subnet-global [rate limiter](super::ratelimit), and writes the confirmed peer into

-//! each side's [`TrustStore`]. It turns "two machines pair via one TOTP code" from a

-//! same-process unit test into an over-the-wire exchange.

+//! subnet-global [rate limiter](super::ratelimit). It turns "two machines pair via one

+//! TOTP code" from a same-process unit test into an over-the-wire exchange.

+//! Membership is recorded by the daemon's pairing host into the

+//! [`RosterStore`](spt_store::roster) (Mesh-D6): this wire confirms the peer's

+//! identity and transfers the seed + roster; the former per-side `peers.json` TOFU

//! ## Why a responder `Announce{epoch}` precedes the SPAKE messages

//! [`Initiator::start`](super::spake::Initiator::start) bakes the **full transcript**


use iroh::endpoint::{Connection, RecvStream, SendStream};
use spt_proto::identity::PublicKey;
use spt_store::trust::{TrustOutcome, TrustStore};

use spt_store::roster::{RosterEntry, RosterStore, Tombstone};

use iroh::endpoint::{Connection, RecvStream, SendStream};
use spt_proto::identity::PublicKey;

use spt_store::roster::{RosterEntry, RosterStore, Tombstone};


use iroh::endpoint::{Connection, RecvStream, SendStream};
use spt_proto::identity::PublicKey;
use spt_store::trust::{TrustOutcome, TrustStore};

use spt_store::roster::{RosterEntry, RosterStore, Tombstone};

use iroh::endpoint::{Connection, RecvStream, SendStream};
use spt_proto::identity::PublicKey;

use spt_store::roster::{RosterEntry, RosterStore, Tombstone};

use spt_store::subnet::{SubnetError, SubnetStore, TOTP_SEED_LEN};


/// What a completed responder ceremony yields (M8 decision 13): the TOFU
/// verdict plus the identity facts the re-pair overwrite consumes — the
/// caller (the daemon's pairing host) decides whether a superseded identity
/// must be evicted; this layer only reports what the ceremony proved.
#[derive(Debug)]
pub struct ResponderOutcome {
    /// The TOFU verdict for the joiner.
    pub trust: TrustOutcome,
    /// The joiner's iroh-authenticated node pubkey, lowercase hex.
    pub joiner_hex: String,

/// What a completed responder ceremony yields (M8 decision 13): the identity
/// facts the re-pair overwrite + roster write consume — the caller (the
/// daemon's pairing host) records membership and decides whether a superseded
/// identity must be evicted; this layer only reports what the ceremony proved.
#[derive(Debug)]
pub struct ResponderOutcome {
    /// The joiner's iroh-authenticated node pubkey, lowercase hex.
    pub joiner_hex: String,


/// What a completed responder ceremony yields (M8 decision 13): the TOFU
/// verdict plus the identity facts the re-pair overwrite consumes — the
/// caller (the daemon's pairing host) decides whether a superseded identity
/// must be evicted; this layer only reports what the ceremony proved.
#[derive(Debug)]
pub struct ResponderOutcome {
    /// The TOFU verdict for the joiner.
    pub trust: TrustOutcome,
    /// The joiner's iroh-authenticated node pubkey, lowercase hex.
    pub joiner_hex: String,

/// What a completed responder ceremony yields (M8 decision 13): the identity
/// facts the re-pair overwrite + roster write consume — the caller (the
/// daemon's pairing host) records membership and decides whether a superseded
/// identity must be evicted; this layer only reports what the ceremony proved.
#[derive(Debug)]
pub struct ResponderOutcome {
    /// The joiner's iroh-authenticated node pubkey, lowercase hex.
    pub joiner_hex: String,

-/// What a completed responder ceremony yields (M8 decision 13): the TOFU

-/// verdict plus the identity facts the re-pair overwrite consumes — the

-/// caller (the daemon's pairing host) decides whether a superseded identity

-/// must be evicted; this layer only reports what the ceremony proved.

+/// What a completed responder ceremony yields (M8 decision 13): the identity

+/// facts the re-pair overwrite + roster write consume — the caller (the

+/// daemon's pairing host) records membership and decides whether a superseded

+/// identity must be evicted; this layer only reports what the ceremony proved.

-    /// The TOFU verdict for the joiner.


/// `local_pub` is this node's own pubkey (the iroh endpoint id); `subnets` supplies the
/// seed + epoch for the named subnet; `rate` is the subnet-global admission gate; `trust`
/// receives the peer on success. `now` (unix seconds) is injected for deterministic
/// tests, mirroring [`ratelimit`](super::ratelimit) / [`totp`](super::totp).
///
/// On `Ok` the peer was confirmed and pinned (the returned [`TrustOutcome`]); the rate
/// limiter slot is released as a success. Every error path releases the slot too — as a
/// failure when a ceremony was admitted (charging backoff), or not at all when admission
/// itself was refused.
// [impl->REQ-PAIR-1]
#[allow(clippy::too_many_arguments)]
pub async fn run_responder(
    conn: &Connection,
    local_pub: PublicKey,
    subnets: &SubnetStore,
    roster: &RosterStore,
    rate: &mut PairingRateLimiter,
    trust: &mut TrustStore,
    now: u64,
) -> Result<ResponderOutcome, PairWireError> {

/// `local_pub` is this node's own pubkey (the iroh endpoint id); `subnets` supplies the
/// seed + epoch for the named subnet; `rate` is the subnet-global admission gate. `now`
/// (unix seconds) is injected for deterministic tests, mirroring
/// [`ratelimit`](super::ratelimit) / [`totp`](super::totp).
///
/// On `Ok` the peer was confirmed (the [`ResponderOutcome`] carries the proven
/// identity for the caller's roster write); the rate limiter slot is released as a
/// success. Every error path releases the slot too — as a failure when a ceremony was
/// admitted (charging backoff), or not at all when admission itself was refused.
// [impl->REQ-PAIR-1]
#[allow(clippy::too_many_arguments)]
pub async fn run_responder(
    conn: &Connection,
    local_pub: PublicKey,
    subnets: &SubnetStore,
    roster: &RosterStore,
    rate: &mut PairingRateLimiter,
    now: u64,
) -> Result<ResponderOutcome, PairWireError> {


/// `local_pub` is this node's own pubkey (the iroh endpoint id); `subnets` supplies the
/// seed + epoch for the named subnet; `rate` is the subnet-global admission gate; `trust`
/// receives the peer on success. `now` (unix seconds) is injected for deterministic
/// tests, mirroring [`ratelimit`](super::ratelimit) / [`totp`](super::totp).
///
/// On `Ok` the peer was confirmed and pinned (the returned [`TrustOutcome`]); the rate
/// limiter slot is released as a success. Every error path releases the slot too — as a
/// failure when a ceremony was admitted (charging backoff), or not at all when admission
/// itself was refused.
// [impl->REQ-PAIR-1]
#[allow(clippy::too_many_arguments)]
pub async fn run_responder(
    conn: &Connection,
    local_pub: PublicKey,
    subnets: &SubnetStore,
    roster: &RosterStore,
    rate: &mut PairingRateLimiter,
    trust: &mut TrustStore,
    now: u64,
) -> Result<ResponderOutcome, PairWireError> {

/// `local_pub` is this node's own pubkey (the iroh endpoint id); `subnets` supplies the
/// seed + epoch for the named subnet; `rate` is the subnet-global admission gate. `now`
/// (unix seconds) is injected for deterministic tests, mirroring
/// [`ratelimit`](super::ratelimit) / [`totp`](super::totp).
///
/// On `Ok` the peer was confirmed (the [`ResponderOutcome`] carries the proven
/// identity for the caller's roster write); the rate limiter slot is released as a
/// success. Every error path releases the slot too — as a failure when a ceremony was
/// admitted (charging backoff), or not at all when admission itself was refused.
// [impl->REQ-PAIR-1]
#[allow(clippy::too_many_arguments)]
pub async fn run_responder(
    conn: &Connection,
    local_pub: PublicKey,
    subnets: &SubnetStore,
    roster: &RosterStore,
    rate: &mut PairingRateLimiter,
    now: u64,
) -> Result<ResponderOutcome, PairWireError> {

/// Run the **responder** (seed-holder) side of the ceremony over an accepted connection.

-/// seed + epoch for the named subnet; `rate` is the subnet-global admission gate; `trust`

-/// receives the peer on success. `now` (unix seconds) is injected for deterministic

-/// tests, mirroring [`ratelimit`](super::ratelimit) / [`totp`](super::totp).

+/// seed + epoch for the named subnet; `rate` is the subnet-global admission gate. `now`

+/// (unix seconds) is injected for deterministic tests, mirroring

+/// [`ratelimit`](super::ratelimit) / [`totp`](super::totp).

-/// On `Ok` the peer was confirmed and pinned (the returned [`TrustOutcome`]); the rate

-/// limiter slot is released as a success. Every error path releases the slot too — as a

-/// failure when a ceremony was admitted (charging backoff), or not at all when admission

+/// On `Ok` the peer was confirmed (the [`ResponderOutcome`] carries the proven

+/// identity for the caller's roster write); the rate limiter slot is released as a

+/// success. Every error path releases the slot too — as a failure when a ceremony was

+/// admitted (charging backoff), or not at all when admission itself was refused.


    let result = responder_admitted(&mut send, &mut recv, &params, trust).await;
    match &result {
        Ok(_) => rate.record_success(&subnet_name),
        Err(_) => rate.record_failure(&subnet_name, now),
    }
    result.map(|trust| ResponderOutcome {
        trust,
        joiner_hex: initiator_pub.to_hex(),
        subnet: subnet_name,
        intro,
    })
}

    let result = responder_admitted(&mut send, &mut recv, &params).await;
    match &result {
        Ok(_) => rate.record_success(&subnet_name),
        Err(_) => rate.record_failure(&subnet_name, now),
    }
    result.map(|()| ResponderOutcome {
        joiner_hex: initiator_pub.to_hex(),
        subnet: subnet_name,
        intro,
    })
}


    let result = responder_admitted(&mut send, &mut recv, &params, trust).await;
    match &result {
        Ok(_) => rate.record_success(&subnet_name),
        Err(_) => rate.record_failure(&subnet_name, now),
    }
    result.map(|trust| ResponderOutcome {
        trust,
        joiner_hex: initiator_pub.to_hex(),
        subnet: subnet_name,
        intro,
    })
}

    let result = responder_admitted(&mut send, &mut recv, &params).await;
    match &result {
        Ok(_) => rate.record_success(&subnet_name),
        Err(_) => rate.record_failure(&subnet_name, now),
    }
    result.map(|()| ResponderOutcome {
        joiner_hex: initiator_pub.to_hex(),
        subnet: subnet_name,
        intro,
    })
}

-    let result = responder_admitted(&mut send, &mut recv, &params, trust).await;

+    let result = responder_admitted(&mut send, &mut recv, &params).await;

-    result.map(|trust| ResponderOutcome {


/// The admitted-ceremony parameters [`run_responder`] hands to [`responder_admitted`]
/// once a subnet seed is found and the rate-limit slot is held. Bundled so the inner
/// driver stays a tidy four-argument call (the streams, this, and the trust store).
struct Admitted<'a> {

/// The admitted-ceremony parameters [`run_responder`] hands to [`responder_admitted`]
/// once a subnet seed is found and the rate-limit slot is held. Bundled so the inner
/// driver stays a tidy three-argument call (the two streams and this).
struct Admitted<'a> {


/// The admitted-ceremony parameters [`run_responder`] hands to [`responder_admitted`]
/// once a subnet seed is found and the rate-limit slot is held. Bundled so the inner
/// driver stays a tidy four-argument call (the streams, this, and the trust store).
struct Admitted<'a> {

/// The admitted-ceremony parameters [`run_responder`] hands to [`responder_admitted`]
/// once a subnet seed is found and the rate-limit slot is held. Bundled so the inner
/// driver stays a tidy three-argument call (the two streams and this).
struct Admitted<'a> {

/// instead of waiting out the ceremony timeout.

-/// driver stays a tidy four-argument call (the streams, this, and the trust store).

+/// driver stays a tidy three-argument call (the two streams and this).

/// The dialer-announced TOTP step (already known to exist; ±1-checked inside).


async fn responder_admitted(
    send: &mut SendStream,
    recv: &mut RecvStream,
    p: &Admitted<'_>,
    trust: &mut TrustStore,
) -> Result<TrustOutcome, PairWireError> {

async fn responder_admitted(
    send: &mut SendStream,
    recv: &mut RecvStream,
    p: &Admitted<'_>,
) -> Result<(), PairWireError> {


async fn responder_admitted(
    send: &mut SendStream,
    recv: &mut RecvStream,
    p: &Admitted<'_>,
    trust: &mut TrustStore,
) -> Result<TrustOutcome, PairWireError> {

async fn responder_admitted(
    send: &mut SendStream,
    recv: &mut RecvStream,
    p: &Admitted<'_>,
) -> Result<(), PairWireError> {

-) -> Result<TrustOutcome, PairWireError> {

// ±1 clock-skew window on the dialer-announced step.

if !accepts_step(time_step(p.now), p.peer_step) {


    match responder.confirm(&initiator_tag) {
        Ok(_paired) => {
            // Confirmed live: pin the peer (TOFU). The peer id is the iroh-authenticated
            // remote id; label/user None (v1).
            let outcome = trust.record(p.subnet_name, &p.initiator_pub.to_hex(), None, None);
            write_frame(send, &enc_done(true)).await?;
            // F7: transfer the subnet seed so the new node becomes a full member /
            // future seed-holder ("every trusted node holds the seed", CONTEXT.md). Sent
            // ONLY on confirm-success, over the iroh-authenticated TLS stream to the
            // now-trust-pinned peer. `seed` is Some here (a None seed aborted upstream).
            let seed = p.seed.expect("seed present on the confirm-success path");
            write_frame(
                send,
                &enc_seed(&seed, p.epoch, p.roster_members, p.roster_tombstones),
            )
            .await?;
            let _ = send.finish();
            Ok(outcome)
        }
        Err(e) => {

    match responder.confirm(&initiator_tag) {
        Ok(_paired) => {
            // Confirmed live. Membership is recorded by the caller (the daemon's
            // pairing host) into the roster — this layer no longer writes trust
            // (Mesh-D6 deleted the TOFU `peers.json` store).
            write_frame(send, &enc_done(true)).await?;
            // F7: transfer the subnet seed so the new node becomes a full member /
            // future seed-holder ("every member node holds the seed", CONTEXT.md). Sent
            // ONLY on confirm-success, over the iroh-authenticated TLS stream to the
            // now-confirmed peer. `seed` is Some here (a None seed aborted upstream).
            let seed = p.seed.expect("seed present on the confirm-success path");
            write_frame(
                send,
                &enc_seed(&seed, p.epoch, p.roster_members, p.roster_tombstones),
            )
            .await?;
            let _ = send.finish();
            Ok(())
        }
        Err(e) => {