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

/// Build the status rows from the subnet store + registry snapshots + the
/// trust store. Pure over its inputs so the view is unit-tested hermetically.
///
/// Node membership is TRUST-derived (self + pinned peers — the durable
/// pairing truth), with registry-row nodes unioned in as enrichment: a
/// member node with zero advertised endpoints (fresh box, empty owlery) is
/// still a member, and an offline peer whose rows the 4.10 eviction decayed
/// must not vanish from its own subnet's view. Endpoint counts stay
/// gossip-derived and stale-tolerant (they catch up as gossip lands).
// [impl->REQ-SUBNET-1]
fn subnet_status_rows(
    store: &SubnetStore,
    regs: &std::collections::BTreeMap<String, spt_net::net::registry::SubnetRegistry>,
    trust: &spt_store::trust::TrustStore,
    own_hex: Option<&str>,
    only: Option<&str>,
) -> Vec<SubnetStatusRow> {
    store
        .subnets
        .iter()
        .filter(|s| only.is_none_or(|n| n == s.name))
        .map(|s| {
            let (mut nodes, mut endpoints) = (std::collections::BTreeSet::new(), 0usize);
            if let Some(own) = own_hex {
                nodes.insert(own.to_string());
            }
            for peer in trust.peers_in(&s.name) {
                nodes.insert(peer.pubkey_hex.clone());
            }

/// Build the status rows from the subnet store + registry snapshots + the
/// member roster. Pure over its inputs so the view is unit-tested hermetically.
///
/// Node membership is ROSTER-derived (self + roster members — the durable mesh
/// membership record, Mesh-D6), with registry-row nodes unioned in as
/// enrichment: a member node with zero advertised endpoints (fresh box, empty
/// owlery) is still a member, and an offline peer whose rows the 4.10 eviction
/// decayed must not vanish from its own subnet's view. Endpoint counts stay
/// gossip-derived and stale-tolerant (they catch up as gossip lands).
// [impl->REQ-SUBNET-1]
fn subnet_status_rows(
    store: &SubnetStore,
    regs: &std::collections::BTreeMap<String, spt_net::net::registry::SubnetRegistry>,
    roster: &spt_store::roster::RosterStore,
    own_hex: Option<&str>,
    only: Option<&str>,
) -> Vec<SubnetStatusRow> {
    store
        .subnets
        .iter()
        .filter(|s| only.is_none_or(|n| n == s.name))
        .map(|s| {
            let (mut nodes, mut endpoints) = (std::collections::BTreeSet::new(), 0usize);
            if let Some(own) = own_hex {
                nodes.insert(own.to_string());
            }
            for member in roster.members_in(&s.name) {
                nodes.insert(member.pubkey_hex.clone());
            }

-/// trust store. Pure over its inputs so the view is unit-tested hermetically.

+/// member roster. Pure over its inputs so the view is unit-tested hermetically.

-/// Node membership is TRUST-derived (self + pinned peers — the durable

-/// pairing truth), with registry-row nodes unioned in as enrichment: a

-/// member node with zero advertised endpoints (fresh box, empty owlery) is

-/// still a member, and an offline peer whose rows the 4.10 eviction decayed

-/// must not vanish from its own subnet's view. Endpoint counts stay

+/// Node membership is ROSTER-derived (self + roster members — the durable mesh

+/// membership record, Mesh-D6), with registry-row nodes unioned in as

+/// enrichment: a member node with zero advertised endpoints (fresh box, empty

+/// owlery) is still a member, and an offline peer whose rows the 4.10 eviction

+/// decayed must not vanish from its own subnet's view. Endpoint counts stay

-    trust: &spt_store::trust::TrustStore,

+    roster: &spt_store::roster::RosterStore,

-            for peer in trust.peers_in(&s.name) {

-                nodes.insert(peer.pubkey_hex.clone());

+            for member in roster.members_in(&s.name) {

+                nodes.insert(member.pubkey_hex.clone());


Now the two call sites (1305, 2770) + the `--nodes` membership seed (2822):


Edit

        let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));
        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();
        println!("subnets:");
        for row in subnet_status_rows(
            &store,
            &regs,
            &trust,
            Some(&own_hex).filter(|h| !h.is_empty()).map(|h| h.as_str()),
            None,
        ) {

        let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));
        let roster = spt_store::roster::RosterStore::load();
        let own_hex = spt_store::nodeid::load_or_create()
            .map(|i| i.public_key().to_hex())
            .unwrap_or_default();
        println!("subnets:");
        for row in subnet_status_rows(
            &store,
            &regs,
            &roster,
            Some(&own_hex).filter(|h| !h.is_empty()).map(|h| h.as_str()),
            None,
        ) {


        let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));
        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();
        println!("subnets:");
        for row in subnet_status_rows(
            &store,
            &regs,
            &trust,
            Some(&own_hex).filter(|h| !h.is_empty()).map(|h| h.as_str()),
            None,
        ) {

        let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));
        let roster = spt_store::roster::RosterStore::load();
        let own_hex = spt_store::nodeid::load_or_create()
            .map(|i| i.public_key().to_hex())
            .unwrap_or_default();
        println!("subnets:");
        for row in subnet_status_rows(
            &store,
            &regs,
            &roster,
            Some(&own_hex).filter(|h| !h.is_empty()).map(|h| h.as_str()),
            None,
        ) {

println!("subnets: (none — this node is standalone)");

-        let trust = spt_store::trust::TrustStore::load();

+        let roster = spt_store::roster::RosterStore::load();


    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 snap_dir = perch::identity_dir().join("registry");
    let regs = crate::wansend::load_snapshots(&snap_dir);
    let roster = spt_store::roster::RosterStore::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,
            &roster,
            Some(&own_hex).filter(|h| !h.is_empty()).map(|h| h.as_str()),
            name.as_deref(),
        );


    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 snap_dir = perch::identity_dir().join("registry");
    let regs = crate::wansend::load_snapshots(&snap_dir);
    let roster = spt_store::roster::RosterStore::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,
            &roster,
            Some(&own_hex).filter(|h| !h.is_empty()).map(|h| h.as_str()),
            name.as_deref(),
        );

-    let trust = spt_store::trust::TrustStore::load();

+    let roster = spt_store::roster::RosterStore::load();


        // 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()));
        }

        // Membership seed: self + roster members (rowless members still
        // render — fresh boxes and eviction-decayed offline peers alike).
        let mut members: Vec<(String, Option<String>)> = Vec::new();
        if !own_hex.is_empty() {
            members.push((own_hex.clone(), spt_store::hostlabel::os_hostname()));
        }
        for member in roster.members_in(&sub.name) {
            let label = (!member.label.is_empty()).then(|| member.label.clone());
            members.push((member.pubkey_hex.clone(), label));
        }


        // 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()));
        }

        // Membership seed: self + roster members (rowless members still
        // render — fresh boxes and eviction-decayed offline peers alike).
        let mut members: Vec<(String, Option<String>)> = Vec::new();
        if !own_hex.is_empty() {
            members.push((own_hex.clone(), spt_store::hostlabel::os_hostname()));
        }
        for member in roster.members_in(&sub.name) {
            let label = (!member.label.is_empty()).then(|| member.label.clone());
            members.push((member.pubkey_hex.clone(), label));
        }

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

+        // Membership seed: self + roster members (rowless members still

-        for peer in trust.peers_in(&sub.name) {

-            members.push((peer.pubkey_hex.clone(), peer.label.clone()));

+        for member in roster.members_in(&sub.name) {

+            let label = (!member.label.is_empty()).then(|| member.label.clone());

+            members.push((member.pubkey_hex.clone(), label));

let rows = node_status_rows(reg, &members, &heard, &own_hex, now_ms(), stale_after_ms);

if !announced && rows.iter().any(|r| r.liveness == NodeLiveness::Probe) {


Now `cmd_subnet_leave` — drop the subnet's roster rows instead of trust rows:


    // 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();

    // 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 (roster rows without a subnet never dial: the pump
    // iterates subnets, not roster rows).
    subnets.remove(name);
    if let Err(e) = subnets.save() {
        eprintln!("LEAVE_FAIL:{name}: {e}");
        return 1;
    }
    // Drop this subnet's roster (members + tombstones) — a local wipe, not a
    // grow-set tombstone: we are removing the whole subnet here, and with no
    // seed left the pump never re-merges it (Mesh-D6 — the roster is membership).
    let mut roster = spt_store::roster::RosterStore::load();
    let before = roster.members.len();
    roster.members.retain(|m| m.subnet != name);
    roster.tombstones.retain(|t| t.subnet != name);
    let dropped = before - roster.members.len();
    if let Err(e) = roster.save() {
        eprintln!("LEAVE_ROSTER_FAIL:{name}: {e} (membership already removed)");
        return 1;
    }
    let mut att = spt_store::attachment::AttachmentStore::load();


    // 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();

    // 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 (roster rows without a subnet never dial: the pump
    // iterates subnets, not roster rows).
    subnets.remove(name);
    if let Err(e) = subnets.save() {
        eprintln!("LEAVE_FAIL:{name}: {e}");
        return 1;
    }
    // Drop this subnet's roster (members + tombstones) — a local wipe, not a
    // grow-set tombstone: we are removing the whole subnet here, and with no
    // seed left the pump never re-merges it (Mesh-D6 — the roster is membership).
    let mut roster = spt_store::roster::RosterStore::load();
    let before = roster.members.len();
    roster.members.retain(|m| m.subnet != name);
    roster.tombstones.retain(|t| t.subnet != name);
    let dropped = before - roster.members.len();
    if let Err(e) = roster.save() {
        eprintln!("LEAVE_ROSTER_FAIL:{name}: {e} (membership already removed)");
        return 1;
    }
    let mut att = spt_store::attachment::AttachmentStore::load();

-    // harmless residue (trust rows without a subnet never dial: the pump

-    // iterates subnets, not trust rows).

+    // harmless residue (roster rows without a subnet never dial: the pump

+    // iterates subnets, not roster rows).

-    let mut trust = spt_store::trust::TrustStore::load();

-    trust.peers.retain(|p| p.subnet != name);

-    let dropped = before - trust.peers.len();

-        eprintln!("LEAVE_TRUST_FAIL:{name}: {e} (membership already removed)");

+    // Drop this subnet's roster (members + tombstones) — a local wipe, not a

+    // grow-set tombstone: we are removing the whole subnet here, and with no

+    // seed left the pump never re-merges it (Mesh-D6 — the roster is membership).

+    let mut roster = spt_store::roster::RosterStore::load();

+    roster.members.retain(|m| m.subnet != name);

+    roster.tombstones.retain(|t| t.subnet != name);

+    let dropped = before - roster.members.len();

+        eprintln!("LEAVE_ROSTER_FAIL:{name}: {e} (membership already removed)");


    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
}

    eprintln!(
        "LEFT:{name} (seed removed, {dropped} roster member(s) dropped). The remaining \
         members still hold the old seed — rotate it there if this machine must not rejoin."
    );
    0
}


    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
}

    eprintln!(
        "LEFT:{name} (seed removed, {dropped} roster member(s) dropped). The remaining \
         members still hold the old seed — rotate it there if this machine must not rejoin."
    );
    0
}

-        "LEFT:{name} (seed removed, {dropped} trust row(s) dropped). The remaining \

+        "LEFT:{name} (seed removed, {dropped} roster member(s) dropped). The remaining \


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.


/// 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
}

/// The prune target resolution (pure, REQ-SUBNET-6): distinct member
/// 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(roster: &spt_store::roster::RosterStore, query: &str) -> Vec<String> {
    let q = query.to_ascii_lowercase();
    let mut out: Vec<String> = Vec::new();
    for m in &roster.members {
        let hit = m.pubkey_hex == q
            || m.pubkey_hex.starts_with(&q)
            || (!m.label.is_empty() && m.label.eq_ignore_ascii_case(query));
        if hit && !out.contains(&m.pubkey_hex) {
            out.push(m.pubkey_hex.clone());
        }
    }
    out
}


/// 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
}

/// The prune target resolution (pure, REQ-SUBNET-6): distinct member
/// 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(roster: &spt_store::roster::RosterStore, query: &str) -> Vec<String> {
    let q = query.to_ascii_lowercase();
    let mut out: Vec<String> = Vec::new();
    for m in &roster.members {
        let hit = m.pubkey_hex == q
            || m.pubkey_hex.starts_with(&q)
            || (!m.label.is_empty() && m.label.eq_ignore_ascii_case(query));
        if hit && !out.contains(&m.pubkey_hex) {
            out.push(m.pubkey_hex.clone());
        }
    }
    out
}

-/// The prune target resolution (pure, REQ-SUBNET-6): distinct trusted

+/// The prune target resolution (pure, REQ-SUBNET-6): distinct member

-fn prune_candidates(trust: &spt_store::trust::TrustStore, query: &str) -> Vec<String> {

+fn prune_candidates(roster: &spt_store::roster::RosterStore, query: &str) -> Vec<String> {

-            || p.pubkey_hex.starts_with(&q)

-                .is_some_and(|l| l.eq_ignore_ascii_case(query));

-        if hit && !out.contains(&p.pubkey_hex) {

-            out.push(p.pubkey_hex.clone());

+            || m.pubkey_hex.starts_with(&q)

+            || (!m.label.is_empty() && m.label.eq_ignore_ascii_case(query));

+        if hit && !out.contains(&m.pubkey_hex) {

+            out.push(m.pubkey_hex.clone());


2952	/// trust rows every tick, so a re-paired/retired machine's stale identity
2953	/// costs a failed dial forever until pruned. Elevation-gated, gate-first.
2954	/// Own identity refuses (leave/identity tooling owns that, not prune).
2955	// [impl->REQ-SUBNET-6]
2956	fn cmd_subnet_prune(node: &str) -> i32 {
2957	    if let Some(msg) = trust_mutation_refusal(elevation::current(), "pruning a node's trust rows") {
2958	        if let Some(code) = try_auto_elevate() {
2959	            return code;
2960	        }
2961	        eprintln!("{}", with_elevation_hint(msg));
2962	        return EXIT_NOT_ELEVATED;
2963	    }
2964	    let mut trust = spt_store::trust::TrustStore::load();
2965	    let candidates = prune_candidates(&trust, node);
2966	    let hex = match candidates.as_slice() {
2967	        [] => {
2968	            eprintln!("NO_MATCH:{node} — no trusted identity matches (hex, prefix, or label)");
2969	            return 1;
2970	        }
2971	        [one] => one.clone(),
2972	        several => {
2973	            let pfx: Vec<String> = several
2974	                .iter()
2975	                .map(|h| format!("{}…", spt_net::net::registry::key_prefix(h)))
2976	                .collect();
2977	            eprintln!(
2978	                "AMBIGUOUS:{node} — several identities match: {} (use a longer prefix)",
2979	                pfx.join(", ")
2980	            );
2981	            return 1;
2982	        }
2983	    };
2984	    if let Ok(own) = spt_store::nodeid::load_or_create() {
2985	        if own.public_key().to_hex() == hex {
2986	            eprintln!("OWN_IDENTITY:{node} — prune removes OTHER nodes' rows; use `spt subnet leave` to exit a subnet");
2987	            return 1;
2988	        }
2989	    }
2990	    let before = trust.peers.len();
2991	    let subnets_hit: Vec<String> = trust
2992	        .peers
2993	        .iter()
2994	        .filter(|p| p.pubkey_hex == hex)
2995	        .map(|p| p.subnet.clone())
2996	        .collect();
2997	    trust.peers.retain(|p| p.pubkey_hex != hex);
2998	    let dropped = before - trust.peers.len();
2999	    if let Err(e) = trust.save() {

/// 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() {


2940	        let hit = m.pubkey_hex == q
2941	            || m.pubkey_hex.starts_with(&q)
2942	            || (!m.label.is_empty() && m.label.eq_ignore_ascii_case(query));
2943	        if hit && !out.contains(&m.pubkey_hex) {
2944	            out.push(m.pubkey_hex.clone());
2945	        }
2946	    }
2947	    out
2948	}
2949
2950	/// `spt subnet prune <node>` — remove a dead identity's trust rows + its
2951	/// registry snapshot rows (M8 decision 7, REQ-SUBNET-6). The pump dials

        let hit = m.pubkey_hex == q
            || m.pubkey_hex.starts_with(&q)
            || (!m.label.is_empty() && m.label.eq_ignore_ascii_case(query));
        if hit && !out.contains(&m.pubkey_hex) {
            out.push(m.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


2999	    if let Err(e) = trust.save() {
3000	        eprintln!("PRUNE_FAIL:{node}: {e}");
3001	        return 1;
3002	    }
3003	    // Registry snapshot rows: the dead identity's advertised rows would
3004	    // otherwise linger until the 4.10 eviction window; prune is explicit,
3005	    // so clear them now (every snapshot — the identity is dead everywhere).
3006	    let snap_dir = perch::identity_dir().join("registry");
3007	    let mut evicted = 0usize;
3008	    for (name, mut reg) in crate::wansend::load_snapshots(&snap_dir) {
3009	        // Prune is an EXPLICIT forget: drop the dead identity's routable rows
3010	        // AND its node-level label (silence-decay keeps the label, but a
3011	        // pruned identity is gone for good — REQ-SUBNET-3 / REQ-SUBNET-6).
3012	        let n = reg.evict_nodes(|node| node == hex) + reg.evict_node_labels(|node| node == hex);
3013	        if n > 0 {
3014	            evicted += n;
3015	            if let Ok(json) = serde_json::to_string_pretty(&reg) {
3016	                let _ = spt_store::atomic::atomic_write_string(
3017	                    &snap_dir.join(format!("{name}.json")),
3018	                    &json,
3019	                );
3020	            }
3021	        }
3022	    }
3023	    eprintln!(
3024	        "PRUNED:{}… ({dropped} trust row(s) [{}], {evicted} registry row(s)). \
3025	         Note: trust-delete alone is not revocation — rotate the seed if the \
3026	         machine itself is untrusted (it still holds the subnet seed).",
3027	        spt_net::net::registry::key_prefix(&hex),
3028	        subnets_hit.join(", "),
3029	    );
3030	    0

    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,
                );
            }
        }
    }
    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


Now rewrite the prune doc + body to roster tombstones:


/// `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;
    }

/// `spt subnet prune <node>` — locally tombstone a dead identity's roster
/// membership + clear its registry snapshot rows (M8 decision 7, REQ-SUBNET-6).
/// The pump dials roster members every tick, so a re-paired/retired machine's
/// stale identity costs a failed dial forever until pruned. The tombstone is
/// LOCAL (Mesh-D6): it drops the `is_member` gate and a later merge cannot
/// re-admit it here — but it does NOT propagate or rotate the seed (the
/// fleet-wide, seed-rotating revoke is `spt subnet revoke`, Mesh-D7).
/// Elevation-gated, gate-first. Own identity refuses (leave owns that).
// [impl->REQ-SUBNET-6]
fn cmd_subnet_prune(node: &str) -> i32 {
    if let Some(msg) = trust_mutation_refusal(elevation::current(), "pruning a node's roster rows") {
        if let Some(code) = try_auto_elevate() {
            return code;
        }
        eprintln!("{}", with_elevation_hint(msg));
        return EXIT_NOT_ELEVATED;
    }
    let mut roster = spt_store::roster::RosterStore::load();
    let candidates = prune_candidates(&roster, node);
    let hex = match candidates.as_slice() {
        [] => {
            eprintln!("NO_MATCH:{node} — no member 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;
        }
    }
    // Tombstone (not delete) every subnet the identity is a member of — a bare
    // remove would re-merge from any peer that still lists the key; the
    // tombstone dominates + suppresses reinsert (a fresh re-pair clears it).
    let subnets_hit: Vec<String> = roster
        .members
        .iter()
        .filter(|m| m.pubkey_hex == hex)
        .map(|m| m.subnet.clone())
        .collect();
    let stamp = (now_ms() / 1000).to_string();
    for sub in &subnets_hit {
        roster.tombstone(sub, &hex, &stamp);
    }
    let dropped = subnets_hit.len();
    if let Err(e) = roster.save() {
        eprintln!("PRUNE_FAIL:{node}: {e}");
        return 1;
    }


/// `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;
    }

/// `spt subnet prune <node>` — locally tombstone a dead identity's roster
/// membership + clear its registry snapshot rows (M8 decision 7, REQ-SUBNET-6).
/// The pump dials roster members every tick, so a re-paired/retired machine's
/// stale identity costs a failed dial forever until pruned. The tombstone is
/// LOCAL (Mesh-D6): it drops the `is_member` gate and a later merge cannot
/// re-admit it here — but it does NOT propagate or rotate the seed (the
/// fleet-wide, seed-rotating revoke is `spt subnet revoke`, Mesh-D7).
/// Elevation-gated, gate-first. Own identity refuses (leave owns that).
// [impl->REQ-SUBNET-6]
fn cmd_subnet_prune(node: &str) -> i32 {
    if let Some(msg) = trust_mutation_refusal(elevation::current(), "pruning a node's roster rows") {
        if let Some(code) = try_auto_elevate() {
            return code;
        }
        eprintln!("{}", with_elevation_hint(msg));
        return EXIT_NOT_ELEVATED;
    }
    let mut roster = spt_store::roster::RosterStore::load();
    let candidates = prune_candidates(&roster, node);
    let hex = match candidates.as_slice() {
        [] => {
            eprintln!("NO_MATCH:{node} — no member 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;
        }
    }
    // Tombstone (not delete) every subnet the identity is a member of — a bare
    // remove would re-merge from any peer that still lists the key; the
    // tombstone dominates + suppresses reinsert (a fresh re-pair clears it).
    let subnets_hit: Vec<String> = roster
        .members
        .iter()
        .filter(|m| m.pubkey_hex == hex)
        .map(|m| m.subnet.clone())
        .collect();
    let stamp = (now_ms() / 1000).to_string();
    for sub in &subnets_hit {
        roster.tombstone(sub, &hex, &stamp);
    }
    let dropped = subnets_hit.len();
    if let Err(e) = roster.save() {
        eprintln!("PRUNE_FAIL:{node}: {e}");
        return 1;
    }

-/// `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).

+/// `spt subnet prune <node>` — locally tombstone a dead identity's roster

+/// membership + clear its registry snapshot rows (M8 decision 7, REQ-SUBNET-6).

+/// The pump dials roster members every tick, so a re-paired/retired machine's

+/// stale identity costs a failed dial forever until pruned. The tombstone is

+/// LOCAL (Mesh-D6): it drops the `is_member` gate and a later merge cannot

+/// re-admit it here — but it does NOT propagate or rotate the seed (the

+/// fleet-wide, seed-rotating revoke is `spt subnet revoke`, Mesh-D7).

+/// Elevation-gated, gate-first. Own identity refuses (leave owns that).

-    if let Some(msg) = trust_mutation_refusal(elevation::current(), "pruning a node's trust rows") {

+    if let Some(msg) = trust_mutation_refusal(elevation::current(), "pruning a node's roster rows") {

-    let mut trust = spt_store::trust::TrustStore::load();

-    let candidates = prune_candidates(&trust, node);

+    let mut roster = spt_store::roster::RosterStore::load();

+    let candidates = prune_candidates(&roster, node);

-            eprintln!("NO_MATCH:{node} — no trusted identity matches (hex, prefix, or label)");

+            eprintln!("NO_MATCH:{node} — no member identity matches (hex, prefix, or label)");

-    let subnets_hit: Vec<String> = trust

+    // Tombstone (not delete) every subnet the identity is a member of — a bare

+    // remove would re-merge from any peer that still lists the key; the

+    // tombstone dominates + suppresses reinsert (a fresh re-pair clears it).

+    let subnets_hit: Vec<String> = roster

-        .filter(|p| p.pubkey_hex == hex)

+        .filter(|m| m.pubkey_hex == hex)

-    trust.peers.retain(|p| p.pubkey_hex != hex);

-    let dropped = before - trust.peers.len();

+    let stamp = (now_ms() / 1000).to_string();

+        roster.tombstone(sub, &hex, &stamp);


    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

    eprintln!(
        "PRUNED:{}… ({dropped} roster tombstone(s) [{}], {evicted} registry row(s)). \
         Note: a local tombstone is not revocation — it does not propagate or \
         rotate the seed (the machine still holds it). Use `spt subnet revoke` \
         to revoke fleet-wide and rotate the seed.",
        spt_net::net::registry::key_prefix(&hex),
        subnets_hit.join(", "),
    );
    0


    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

    eprintln!(
        "PRUNED:{}… ({dropped} roster tombstone(s) [{}], {evicted} registry row(s)). \
         Note: a local tombstone is not revocation — it does not propagate or \
         rotate the seed (the machine still holds it). Use `spt subnet revoke` \
         to revoke fleet-wide and rotate the seed.",
        spt_net::net::registry::key_prefix(&hex),
        subnets_hit.join(", "),
    );
    0

-        "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).",

+        "PRUNED:{}… ({dropped} roster tombstone(s) [{}], {evicted} registry row(s)). \

+         Note: a local tombstone is not revocation — it does not propagate or \

+         rotate the seed (the machine still holds it). Use `spt subnet revoke` \

+         to revoke fleet-wide and rotate the seed.",


CLI source done. Now migrate the two CLI unit tests still on trust.


Grep

trust::TrustStore|&trust|trust\.record|let trust|let mut trust|let mut pinned|pinned\.record|&pinned


6352:        let mut trust = spt_store::trust::TrustStore::default();
6353:        trust.record("bignet", "09ef831e00112233", None, None);
6354:        trust.record("bignet", "09ef831e00112233", None, None); // idempotent
6355:        trust.record("worknet", "09ef831e00112233", None, None); // same id, 2nd subnet
6356:        trust.record("bignet", "09aa000011223344", Some("GRAVITY"), None);
6360:            prune_candidates(&trust, "09ef831e00112233"),
6364:            prune_candidates(&trust, "09ef"),
6369:            prune_candidates(&trust, "gravity"),
6374:        assert_eq!(prune_candidates(&trust, "09").len(), 2);
6376:        assert!(prune_candidates(&trust, "ff00").is_empty());
6530:        let trust = spt_store::trust::TrustStore::default();
6531:        let rows = subnet_status_rows(&store, &regs, &trust, Some("aa"), None);
6544:        let mut pinned = spt_store::trust::TrustStore::default();
6545:        pinned.record("home", "dd", None, None);
6546:        let rows = subnet_status_rows(&store, &regs, &pinned, Some("cc"), None);
6575:            &trust,


6346
6347	    // [unit->REQ-SUBNET-6] prune target resolution: full hex, unambiguous
6348	    // prefix, and label all resolve; a shared prefix yields BOTH candidates
6349	    // (the caller refuses); unknown queries match nothing.
6350	    #[test]
6351	    fn prune_candidates_resolve_never_guess() {
6352	        let mut trust = spt_store::trust::TrustStore::default();
6353	        trust.record("bignet", "09ef831e00112233", None, None);
6354	        trust.record("bignet", "09ef831e00112233", None, None); // idempotent
6355	        trust.record("worknet", "09ef831e00112233", None, None); // same id, 2nd subnet
6356	        trust.record("bignet", "09aa000011223344", Some("GRAVITY"), None);
6357
6358	        // Full hex + unambiguous prefix + label each resolve to one identity.
6359	        assert_eq!(
6360	            prune_candidates(&trust, "09ef831e00112233"),
6361	            vec!["09ef831e00112233".to_string()]
6362	        );
6363	        assert_eq!(
6364	            prune_candidates(&trust, "09ef"),
6365	            vec!["09ef831e00112233".to_string()],
6366	            "cross-subnet rows of ONE identity are one candidate"
6367	        );
6368	        assert_eq!(
6369	            prune_candidates(&trust, "gravity"),
6370	            vec!["09aa000011223344".to_string()],
6371	            "label resolves case-insensitively"
6372	        );
6373	        // A shared prefix surfaces both — the command refuses, never guesses.
6374	        assert_eq!(prune_candidates(&trust, "09").len(), 2);
6375	        // Unknown query matches nothing.
6376	        assert!(prune_candidates(&trust, "ff00").is_empty());
6377	    }
6378
6379	    // [unit->REQ-SUBNET-4] join is elevation-gated with the same fail-safe
6380	    // posture as the seed-reveal paths: only CONFIRMED elevation proceeds;

    // [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;


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


Edit

        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());
    }

        let mut roster = spt_store::roster::RosterStore::default();
        let mut member = |subnet: &str, pk: &str, label: &str| {
            roster.merge_entry(spt_store::roster::RosterEntry {
                pubkey_hex: pk.to_string(),
                subnet: subnet.to_string(),
                label: label.to_string(),
                machine_id: String::new(),
                address: None,
                last_seen: "0".to_string(),
                lease_epoch: 1,
            });
        };
        member("bignet", "09ef831e00112233", "");
        member("bignet", "09ef831e00112233", ""); // idempotent (same key,subnet)
        member("worknet", "09ef831e00112233", ""); // same id, 2nd subnet
        member("bignet", "09aa000011223344", "GRAVITY");

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


        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());
    }

        let mut roster = spt_store::roster::RosterStore::default();
        let mut member = |subnet: &str, pk: &str, label: &str| {
            roster.merge_entry(spt_store::roster::RosterEntry {
                pubkey_hex: pk.to_string(),
                subnet: subnet.to_string(),
                label: label.to_string(),
                machine_id: String::new(),
                address: None,
                last_seen: "0".to_string(),
                lease_epoch: 1,
            });
        };
        member("bignet", "09ef831e00112233", "");
        member("bignet", "09ef831e00112233", ""); // idempotent (same key,subnet)
        member("worknet", "09ef831e00112233", ""); // same id, 2nd subnet
        member("bignet", "09aa000011223344", "GRAVITY");

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

// (the caller refuses); unknown queries match nothing.

-        let mut trust = spt_store::trust::TrustStore::default();

-        trust.record("bignet", "09ef831e00112233", None, None); // idempotent

-        trust.record("worknet", "09ef831e00112233", None, None); // same id, 2nd subnet

-        trust.record("bignet", "09aa000011223344", Some("GRAVITY"), None);

+        let mut roster = spt_store::roster::RosterStore::default();

+        let mut member = |subnet: &str, pk: &str, label: &str| {

+            roster.merge_entry(spt_store::roster::RosterEntry {

+                pubkey_hex: pk.to_string(),

+                subnet: subnet.to_string(),

+                label: label.to_string(),

+                machine_id: String::new(),

+                last_seen: "0".to_string(),

+        member("bignet", "09ef831e00112233", ""); // idempotent (same key,subnet)

+        member("worknet", "09ef831e00112233", ""); // same id, 2nd subnet

-            prune_candidates(&trust, "09ef831e00112233"),

+            prune_candidates(&roster, "09ef831e00112233"),

-            prune_candidates(&trust, "09ef"),

+            prune_candidates(&roster, "09ef"),

-            prune_candidates(&trust, "gravity"),

+            prune_candidates(&roster, "gravity"),

-        assert_eq!(prune_candidates(&trust, "09").len(), 2);

+        assert_eq!(prune_candidates(&roster, "09").len(), 2);

-        assert!(prune_candidates(&trust, "ff00").is_empty());

+        assert!(prune_candidates(&roster, "ff00").is_empty());

// [unit->REQ-SUBNET-4] join is elevation-gated with the same fail-safe


Now the subnet_status test region:


6526	            reg.merge_instance(
6527	                id,
6528	                Instance {
6529	                    node: node.into(),
6530	                    status: Status::Active,
6531	                    epoch: 1,
6532	                    resources: None,
6533	                    last_active_ms: None,
6534	                    shell_adapters: Vec::new(),
6535	                    node_label: None,
6536	                    machine_id: None,
6537	                },
6538	            );
6539	        }
6540	        let regs = std::collections::BTreeMap::from([("home".to_string(), reg)]);
6541	        let trust = spt_store::trust::TrustStore::default();
6542	        let rows = subnet_status_rows(&store, &regs, &trust, Some("aa"), None);
6543	        assert_eq!(rows.len(), 1);
6544	        assert_eq!((rows[0].nodes, rows[0].endpoints), (2, 3));
6545	        let view = render_subnet_status(&rows, true, true);
6546	        assert!(view.contains("home"));
6547	        assert!(view.contains("ENDPOINTS"));
6548	        assert!(view.contains("hint:"), "bare view keeps the hint footer");
6549
6550	        // MEMBERSHIP is trust-derived, rows enrich (the re-join field bug,
6551	        // 2026-06-06): a member node with ZERO advertised endpoints — fresh
6552	        // box, or an offline peer whose rows the 4.10 eviction decayed —
6553	        // still counts. Self (cc, rowless) + a pinned rowless peer (dd) +
6554	        // the two row-nodes = 4.
6555	        let mut pinned = spt_store::trust::TrustStore::default();
6556	        pinned.record("home", "dd", None, None);
6557	        let rows = subnet_status_rows(&store, &regs, &pinned, Some("cc"), None);
6558	        assert_eq!(
6559	            (rows[0].nodes, rows[0].endpoints),
6560	            (4, 3),
6561	            "rowless self + rowless pinned peer still count as nodes"
6562	        );
6563	        let seed_hex = {
6564	            use std::fmt::Write as _;
6565	            store
6566	                .find("home")
6567	                .unwrap()
6568	                .seed_bytes()
6569	                .unwrap()
6570	                .iter()
6571	                .fold(String::new(), |mut s, b| {
6572	                    let _ = write!(s, "{b:02x}");
6573	                    s
6574	                })
6575	        };
6576	        assert!(
6577	            !view.contains(&seed_hex),
6578	            "the status view never carries seed material"
6579	        );
6580
6581	        // A missing snapshot degrades to this-node-alone, not a panic/zero —
6582	        // even when the identity itself could not load (own_hex None).
6583	        let no_gossip = subnet_status_rows(
6584	            &store,
6585	            &std::collections::BTreeMap::new(),

            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(),


        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 regs = std::collections::BTreeMap::from([("home".to_string(), reg)]);
        let roster = spt_store::roster::RosterStore::default();
        let rows = subnet_status_rows(&store, &regs, &roster, 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 roster-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 rowless roster member (dd) +
        // the two row-nodes = 4.
        let mut member_roster = spt_store::roster::RosterStore::default();
        member_roster.upsert_self("home", "dd", "", "", None, "0", 1);
        let rows = subnet_status_rows(&store, &regs, &member_roster, Some("cc"), None);
        assert_eq!(
            (rows[0].nodes, rows[0].endpoints),
            (4, 3),
            "rowless self + rowless roster member still count as nodes"
        );


        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 regs = std::collections::BTreeMap::from([("home".to_string(), reg)]);
        let roster = spt_store::roster::RosterStore::default();
        let rows = subnet_status_rows(&store, &regs, &roster, 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 roster-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 rowless roster member (dd) +
        // the two row-nodes = 4.
        let mut member_roster = spt_store::roster::RosterStore::default();
        member_roster.upsert_self("home", "dd", "", "", None, "0", 1);
        let rows = subnet_status_rows(&store, &regs, &member_roster, Some("cc"), None);
        assert_eq!(
            (rows[0].nodes, rows[0].endpoints),
            (4, 3),
            "rowless self + rowless roster member still count as nodes"
        );

-        let trust = spt_store::trust::TrustStore::default();

-        let rows = subnet_status_rows(&store, &regs, &trust, Some("aa"), None);

+        let roster = spt_store::roster::RosterStore::default();

+        let rows = subnet_status_rows(&store, &regs, &roster, Some("aa"), None);

-        // MEMBERSHIP is trust-derived, rows enrich (the re-join field bug,

+        // MEMBERSHIP is roster-derived, rows enrich (the re-join field bug,

-        // still counts. Self (cc, rowless) + a pinned rowless peer (dd) +

+        // still counts. Self (cc, rowless) + a rowless roster member (dd) +

-        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);

+        let mut member_roster = spt_store::roster::RosterStore::default();

+        member_roster.upsert_self("home", "dd", "", "", None, "0", 1);

+        let rows = subnet_status_rows(&store, &regs, &member_roster, Some("cc"), None);

-            "rowless self + rowless pinned peer still count as nodes"

+            "rowless self + rowless roster member still count as nodes"


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

        // 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));
    }


        let no_gossip = subnet_status_rows(
            &store,
            &std::collections::BTreeMap::new(),
            &trust,
            None,
            None,
        );

        let no_gossip = subnet_status_rows(
            &store,
            &std::collections::BTreeMap::new(),
            &roster,
            None,
            None,
        );


        let no_gossip = subnet_status_rows(
            &store,
            &std::collections::BTreeMap::new(),
            &trust,
            None,
            None,
        );

        let no_gossip = subnet_status_rows(
            &store,
            &std::collections::BTreeMap::new(),
            &roster,
            None,
            None,
        );


Now xtask + comment refs (task 8). Read xtask dump.


885	    };
886
887	    // Expected-node derivation: explicit --nodes wins; else the coordinator's
888	    // trust-derived membership (self excluded — the coordinator's own state
889	    // is local, not a wire question). A freshly-joined node is a trust row,
890	    // so it shows up as an expected row, never a silent omission.
891	    let own_hex = spt_store::nodeid::load_or_create()
892	        .map(|i| i.public_key().to_hex())
893	        .unwrap_or_default();
894	    let expected: Vec<String> = if !nodes.is_empty() {
895	        nodes
896	    } else {
897	        let trust = spt_store::trust::TrustStore::load();
898	        let mut set = std::collections::BTreeSet::new();
899	        for p in &trust.peers {
900	            if subnet.as_deref().is_none_or(|s| p.subnet == s) && p.pubkey_hex != own_hex {
901	                set.insert(p.pubkey_hex.clone());
902	            }
903	        }
904	        set.into_iter().collect()
905	    };
906	    if expected.is_empty() {
907	        eprintln!(
908	            "no expected nodes (no trust rows{}; use --nodes)",
909	            subnet
910	                .as_deref()
911	                .map(|s| format!(" in subnet '{s}'"))
912	                .unwrap_or_default()
913	        );
914	        std::process::exit(2);

    };

    // Expected-node derivation: explicit --nodes wins; else the coordinator's
    // trust-derived membership (self excluded — the coordinator's own state
    // is local, not a wire question). A freshly-joined node is a trust row,
    // so it shows up as an expected row, never a silent omission.
    let own_hex = spt_store::nodeid::load_or_create()
        .map(|i| i.public_key().to_hex())
        .unwrap_or_default();
    let expected: Vec<String> = if !nodes.is_empty() {
        nodes
    } else {
        let trust = spt_store::trust::TrustStore::load();
        let mut set = std::collections::BTreeSet::new();
        for p in &trust.peers {
            if subnet.as_deref().is_none_or(|s| p.subnet == s) && p.pubkey_hex != own_hex {
                set.insert(p.pubkey_hex.clone());
            }
        }
        set.into_iter().collect()
    };
    if expected.is_empty() {
        eprintln!(
            "no expected nodes (no trust rows{}; use --nodes)",
            subnet
                .as_deref()
                .map(|s| format!(" in subnet '{s}'"))
                .unwrap_or_default()
        );
        std::process::exit(2);


    // Expected-node derivation: explicit --nodes wins; else the coordinator's
    // trust-derived membership (self excluded — the coordinator's own state
    // is local, not a wire question). A freshly-joined node is a trust row,
    // so it shows up as an expected row, never a silent omission.
    let own_hex = spt_store::nodeid::load_or_create()
        .map(|i| i.public_key().to_hex())
        .unwrap_or_default();
    let expected: Vec<String> = if !nodes.is_empty() {
        nodes
    } else {
        let trust = spt_store::trust::TrustStore::load();
        let mut set = std::collections::BTreeSet::new();
        for p in &trust.peers {
            if subnet.as_deref().is_none_or(|s| p.subnet == s) && p.pubkey_hex != own_hex {
                set.insert(p.pubkey_hex.clone());
            }
        }
        set.into_iter().collect()
    };
    if expected.is_empty() {
        eprintln!(
            "no expected nodes (no trust rows{}; use --nodes)",

    // Expected-node derivation: explicit --nodes wins; else the coordinator's
    // roster-derived membership (self excluded — the coordinator's own state
    // is local, not a wire question). A freshly-joined node is a roster member,
    // so it shows up as an expected row, never a silent omission.
    let own_hex = spt_store::nodeid::load_or_create()
        .map(|i| i.public_key().to_hex())
        .unwrap_or_default();
    let expected: Vec<String> = if !nodes.is_empty() {
        nodes
    } else {
        let roster = spt_store::roster::RosterStore::load();
        let mut set = std::collections::BTreeSet::new();
        for m in &roster.members {
            if subnet.as_deref().is_none_or(|s| m.subnet == s)
                && m.pubkey_hex != own_hex
                && !roster.is_tombstoned(&m.subnet, &m.pubkey_hex)
            {
                set.insert(m.pubkey_hex.clone());
            }
        }
        set.into_iter().collect()
    };
    if expected.is_empty() {
        eprintln!(
            "no expected nodes (no roster members{}; use --nodes)",


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

    // Expected-node derivation: explicit --nodes wins; else the coordinator's
    // trust-derived membership (self excluded — the coordinator's own state
    // is local, not a wire question). A freshly-joined node is a trust row,
    // so it shows up as an expected row, never a silent omission.
    let own_hex = spt_store::nodeid::load_or_create()
        .map(|i| i.public_key().to_hex())
        .unwrap_or_default();
    let expected: Vec<String> = if !nodes.is_empty() {
        nodes
    } else {
        let trust = spt_store::trust::TrustStore::load();
        let mut set = std::collections::BTreeSet::new();
        for p in &trust.peers {
            if subnet.as_deref().is_none_or(|s| p.subnet == s) && p.pubkey_hex != own_hex {
                set.insert(p.pubkey_hex.clone());
            }
        }
        set.into_iter().collect()
    };
    if expected.is_empty() {
        eprintln!(
            "no expected nodes (no trust rows{}; use --nodes)",

    // Expected-node derivation: explicit --nodes wins; else the coordinator's
    // roster-derived membership (self excluded — the coordinator's own state
    // is local, not a wire question). A freshly-joined node is a roster member,
    // so it shows up as an expected row, never a silent omission.
    let own_hex = spt_store::nodeid::load_or_create()
        .map(|i| i.public_key().to_hex())
        .unwrap_or_default();
    let expected: Vec<String> = if !nodes.is_empty() {
        nodes
    } else {
        let roster = spt_store::roster::RosterStore::load();
        let mut set = std::collections::BTreeSet::new();
        for m in &roster.members {
            if subnet.as_deref().is_none_or(|s| m.subnet == s)
                && m.pubkey_hex != own_hex
                && !roster.is_tombstoned(&m.subnet, &m.pubkey_hex)
            {
                set.insert(m.pubkey_hex.clone());
            }
        }
        set.into_iter().collect()
    };
    if expected.is_empty() {
        eprintln!(
            "no expected nodes (no roster members{}; use --nodes)",

-    // trust-derived membership (self excluded — the coordinator's own state

-    // is local, not a wire question). A freshly-joined node is a trust row,

+    // roster-derived membership (self excluded — the coordinator's own state

+    // is local, not a wire question). A freshly-joined node is a roster member,

-        let trust = spt_store::trust::TrustStore::load();

+        let roster = spt_store::roster::RosterStore::load();

-            if subnet.as_deref().is_none_or(|s| p.subnet == s) && p.pubkey_hex != own_hex {

-                set.insert(p.pubkey_hex.clone());

+            if subnet.as_deref().is_none_or(|s| m.subnet == s)

+                && m.pubkey_hex != own_hex

+                && !roster.is_tombstoned(&m.subnet, &m.pubkey_hex)

+                set.insert(m.pubkey_hex.clone());

-            "no expected nodes (no trust rows{}; use --nodes)",

+            "no expected nodes (no roster members{}; use --nodes)",


74	    pub roster: RosterStore,
75	    pub vis: VisibilityStore,
76	}
77
78	impl SyncPolicy {
79	    /// Load from the node's canonical stores (`subnet.json`, `trust/peers.json`,
80	    /// `visibility.json`). Re-read per pull — config edits take effect on the
81	    /// next request without a daemon restart (the access-gate stance).
82	    pub fn load() -> SyncPolicy {
83	        let subnets = spt_store::subnet::SubnetStore::load()
84	            .subnets
85	            .iter()
86	            .map(|s| (s.name.clone(), s.hide_new_endpoints))
87	            .collect();
88	        SyncPolicy {
89	            subnets,
90	            roster: RosterStore::load(),
91	            vis: VisibilityStore::load(),
92	        }
93	    }
94
95	    /// Whether `branch` may sync with `origin_node` (handshake-proven). The

    pub roster: RosterStore,
    pub vis: VisibilityStore,
}

impl SyncPolicy {
    /// Load from the node's canonical stores (`subnet.json`, `trust/peers.json`,
    /// `visibility.json`). Re-read per pull — config edits take effect on the
    /// next request without a daemon restart (the access-gate stance).
    pub fn load() -> SyncPolicy {
        let subnets = spt_store::subnet::SubnetStore::load()
            .subnets
            .iter()
            .map(|s| (s.name.clone(), s.hide_new_endpoints))
            .collect();
        SyncPolicy {
            subnets,
            roster: RosterStore::load(),
            vis: VisibilityStore::load(),
        }
    }

    /// Whether `branch` may sync with `origin_node` (handshake-proven). The


    /// Load from the node's canonical stores (`subnet.json`, `trust/peers.json`,
    /// `visibility.json`). Re-read per pull — config edits take effect on the
    /// next request without a daemon restart (the access-gate stance).

    /// Load from the node's canonical stores (`subnet.json`, `roster.json`,
    /// `visibility.json`). Re-read per pull — config edits take effect on the
    /// next request without a daemon restart (the access-gate stance).


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

    /// Load from the node's canonical stores (`subnet.json`, `trust/peers.json`,
    /// `visibility.json`). Re-read per pull — config edits take effect on the
    /// next request without a daemon restart (the access-gate stance).

    /// Load from the node's canonical stores (`subnet.json`, `roster.json`,
    /// `visibility.json`). Re-read per pull — config edits take effect on the
    /// next request without a daemon restart (the access-gate stance).

-    /// Load from the node's canonical stores (`subnet.json`, `trust/peers.json`,

+    /// Load from the node's canonical stores (`subnet.json`, `roster.json`,


40	    pub roster: RosterStore,
41	}
42
43	impl NotifPolicy {
44	    /// Load from the node's canonical stores (`subnet.json`, `trust/peers.json`).
45	    pub fn load() -> NotifPolicy {
46	        let subnets = spt_store::subnet::SubnetStore::load()
47	            .subnets
48	            .iter()
49	            .map(|s| s.name.clone())
50	            .collect();
51	        NotifPolicy {

    pub roster: RosterStore,
}

impl NotifPolicy {
    /// Load from the node's canonical stores (`subnet.json`, `trust/peers.json`).
    pub fn load() -> NotifPolicy {
        let subnets = spt_store::subnet::SubnetStore::load()
            .subnets
            .iter()
            .map(|s| s.name.clone())
            .collect();
        NotifPolicy {


    /// Load from the node's canonical stores (`subnet.json`, `trust/peers.json`).
    pub fn load() -> NotifPolicy {

    /// Load from the node's canonical stores (`subnet.json`, `roster.json`).
    pub fn load() -> NotifPolicy {


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

    /// Load from the node's canonical stores (`subnet.json`, `trust/peers.json`).
    pub fn load() -> NotifPolicy {

    /// Load from the node's canonical stores (`subnet.json`, `roster.json`).
    pub fn load() -> NotifPolicy {

-    /// Load from the node's canonical stores (`subnet.json`, `trust/peers.json`).

+    /// Load from the node's canonical stores (`subnet.json`, `roster.json`).

let subnets = spt_store::subnet::SubnetStore::load()


90	    /// the QUIC connection. Cloned (cheap handle) for stream opens.
91	    conn: Connection,
92	    /// The remote node's stable name (hex pubkey), proven by the QUIC handshake.
93	    remote_id_hex: String,
94	    /// The subnets the peer proved current-epoch seed-knowledge of at connect
95	    /// (Mesh-D2, REQ-MESH-1). Empty when seed-proof is disabled (`membership:
96	    /// None`). The D5 gate swap reads this in place of `is_trusted(subnet, …)`;
97	    /// at D2 it is cached but not yet consumed (the `is_trusted` gates still run
98	    /// in parallel — belt + suspenders, no regression).
99	    proven_subnets: HashSet<String>,
100	}
101

    /// the QUIC connection. Cloned (cheap handle) for stream opens.
    conn: Connection,
    /// The remote node's stable name (hex pubkey), proven by the QUIC handshake.
    remote_id_hex: String,
    /// The subnets the peer proved current-epoch seed-knowledge of at connect
    /// (Mesh-D2, REQ-MESH-1). Empty when seed-proof is disabled (`membership:
    /// None`). The D5 gate swap reads this in place of `is_trusted(subnet, …)`;
    /// at D2 it is cached but not yet consumed (the `is_trusted` gates still run
    /// in parallel — belt + suspenders, no regression).
    proven_subnets: HashSet<String>,
}


Build workspace (libs + bins) to catch CLI/xtask errors before tests.


Bash

cargo build --workspace 2>&1 | tail -30; echo "===EXIT:${PIPESTATUS[0]}==="

Build full workspace


   Compiling spt-msg v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-msg)
   Compiling spt-runtime v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime)
   Compiling spt-net v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
   Compiling spt-term v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-term)
   Compiling spt-live v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-live)
   Compiling spt-daemon v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
   Compiling spt v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
   Compiling xtask v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\xtask)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 14.25s
===EXIT:0===


Workspace lib+bin builds. Now the daemon test files. Read the relevant regions.


120	        .net_status()
121	        .expect("status")
122	        .node_id_hex
123	        .expect("net");
124	    let b_hex = b_probe
125	        .net_status()
126	        .expect("status")
127	        .node_id_hex
128	        .expect("net");
129	    let b_addr = b_probe.net_status().expect("status").addr;
130
131	    // ── B (canonical home): gates + the mind A will bootstrap-pull. ──────
132	    let mut subnets = SubnetStore::load();
133	    subnets.create_subnet(subnet).expect("create subnet");
134	    subnets.save().expect("save subnets");
135	    let mut trust = TrustStore::load();
136	    trust.record(subnet, &a_hex, None, None);
137	    trust.save().expect("save trust");
138	    // Mesh-D5: the inbound gates now authorize on roster membership, so B must
139	    // roster A (the push target stays the trust set — see the pump). Seed the
140	    // canonical roster to mirror the trust pin.
141	    let mut roster = spt_store::roster::RosterStore::load();
142	    roster.merge_entry(spt_store::roster::RosterEntry {
143	        pubkey_hex: a_hex.clone(),
144	        subnet: subnet.to_string(),
145	        label: String::new(),
146	        machine_id: String::new(),
147	        address: None,
148	        last_seen: String::new(),
149	        lease_epoch: 1,
150	    });
151	    roster.save().expect("save roster");
152	    let mut vis = VisibilityStore::load();
153	    vis.set_sync_subnets("ling-pumped", vec![subnet.to_string()]);
154	    vis.save().expect("save visibility");
155
156	    let cs_b = ContextStore::open_or_init().expect("B context store");
157	    let p = cs_b.live_context_path("ling-pumped").unwrap();
158	    std::fs::write(&p, stamped("a mind worth following")).unwrap();
159	    cs_b.commit_live("ling-pumped", "seed").unwrap();
160
161	    // B stages a v6 release (the canonical releases dir B's dispatcher serves
162	    // from); A's keys file will trust the signer.
163	    use ed25519_dalek::{Signer, SigningKey};
164	    let sk = SigningKey::from_bytes(&[5u8; 32]);
165	    let artifact = b"pump-propagated binary".to_vec();
166	    let meta = ReleaseMetadata {
167	        version: 6,
168	        channel: "stable".to_string(),
169	        expires_at_ms: u64::MAX,
170	        key_id: "k".to_string(),
171	        artifact_sha256: sha256_hex(&artifact),
172	        brain_ipc_version: spt_daemon::IPC_PROTOCOL_VERSION,
173	        broker_resource_abi: BROKER_RESOURCE_ABI,
174	    };
175	    let metadata_json = serde_json::to_string(&meta).unwrap();
176	    let sig = sk.sign(metadata_json.as_bytes());
177	    let signed = SignedRelease {
178	        metadata_json,
179	        signature_hex: sig.to_bytes().iter().map(|b| format!("{b:02x}")).collect(),

        .net_status()
        .expect("status")
        .node_id_hex
        .expect("net");
    let b_hex = b_probe
        .net_status()
        .expect("status")
        .node_id_hex
        .expect("net");
    let b_addr = b_probe.net_status().expect("status").addr;

    // ── B (canonical home): gates + the mind A will bootstrap-pull. ──────
    let mut subnets = SubnetStore::load();
    subnets.create_subnet(subnet).expect("create subnet");
    subnets.save().expect("save subnets");
    let mut trust = TrustStore::load();
    trust.record(subnet, &a_hex, None, None);
    trust.save().expect("save trust");
    // Mesh-D5: the inbound gates now authorize on roster membership, so B must
    // roster A (the push target stays the trust set — see the pump). Seed the
    // canonical roster to mirror the trust pin.
    let mut roster = spt_store::roster::RosterStore::load();
    roster.merge_entry(spt_store::roster::RosterEntry {
        pubkey_hex: a_hex.clone(),
        subnet: subnet.to_string(),
        label: String::new(),
        machine_id: String::new(),
        address: None,
        last_seen: String::new(),
        lease_epoch: 1,
    });
    roster.save().expect("save roster");
    let mut vis = VisibilityStore::load();
    vis.set_sync_subnets("ling-pumped", vec![subnet.to_string()]);
    vis.save().expect("save visibility");

    let cs_b = ContextStore::open_or_init().expect("B context store");
    let p = cs_b.live_context_path("ling-pumped").unwrap();
    std::fs::write(&p, stamped("a mind worth following")).unwrap();
    cs_b.commit_live("ling-pumped", "seed").unwrap();

    // B stages a v6 release (the canonical releases dir B's dispatcher serves
    // from); A's keys file will trust the signer.
    use ed25519_dalek::{Signer, SigningKey};
    let sk = SigningKey::from_bytes(&[5u8; 32]);
    let artifact = b"pump-propagated binary".to_vec();
    let meta = ReleaseMetadata {
        version: 6,
        channel: "stable".to_string(),
        expires_at_ms: u64::MAX,
        key_id: "k".to_string(),
        artifact_sha256: sha256_hex(&artifact),
        brain_ipc_version: spt_daemon::IPC_PROTOCOL_VERSION,
        broker_resource_abi: BROKER_RESOURCE_ABI,
    };
    let metadata_json = serde_json::to_string(&meta).unwrap();
    let sig = sk.sign(metadata_json.as_bytes());
    let signed = SignedRelease {
        metadata_json,
        signature_hex: sig.to_bytes().iter().map(|b| format!("{b:02x}")).collect(),