1620	}
1621
1622	/// Decide `subnet create`'s rendered outcome from a resolution + clock + elevation.
1623	/// Same gate-last shape as [`decide_show_code`] — a refused mint persists nothing and
1624	/// leaks nothing — but the elevated output is the full joining material (M7 plan
1625	/// decision 11): code, `otpauth://` URI, terminal QR, and the join hint. The gate is
1626	/// the create half of the membership-mutation elevation requirement (a mint REVEALS
1627	/// the subnet's joining secret).
1628	// [impl->REQ-SUBNET-1] [impl->REQ-SUBNET-4]
1629	fn decide_create(resolved: Resolved, now: u64, elevation: Elevation) -> ShowResult {
1630	    match resolved {
1631	        Resolved::Subnet { name, seed, minted } => match elevation::gate_for(elevation) {
1632	            Gate::Show => {
1633	                let totp = TotpSeed::from_bytes(seed);
1634	                let uri = totp.otpauth_uri(ISSUER, &name);
1635	                let mut out = format!("{}\n{}\n", totp.code_at(now), uri);
1636	                // Terminal QR of the provisioning URI — scan it into an
1637	                // authenticator app from the other machine's side of the desk.
1638	                if let Some(qr) = qr_unicode(&uri) {
1639	                    out.push_str(&qr);
1640	                    out.push('\n');
1641	                }
1642	                out.push_str(&format!(
1643	                    "Run `spt subnet join {name}` on the other machine to pair it in.\n"
1644	                ));
1645	                ShowResult {
1646	                    code: 0,
1647	                    out: Some(out),
1648	                    msg: format!("SUBNET_CREATED:{name} (this node is the sole seed-holder)"),
1649	                    save: minted,
1650	                }
1651	            }
1652	            // Refused mint: nothing persists, nothing leaks (the seed-reveal
1653	            // path needs confirmed privilege — same posture as show-code).
1654	            Gate::Fallback => ShowResult {
1655	                code: EXIT_NOT_ELEVATED,
1656	                out: None,
1657	                msg: format!(
1658	                    "ELEVATION_REQUIRED: creating subnet '{name}' reveals its joining secret — \
1659	                     re-run elevated (run as administrator / root)"
1660	                ),
1661	                save: false,
1662	            },
1663	        },
1664	        other => decide_show_code(other, now, elevation),
1665	    }
1666	}
1667
1668	/// Render the provisioning URI as a terminal QR (unicode half-blocks). `None` when
1669	/// encoding fails (URI too long for the QR alphabet) — the URI text above it is the
1670	/// fallback, so create degrades gracefully instead of refusing.
1671	fn qr_unicode(uri: &str) -> Option<String> {
1672	    let code = qrcode::QrCode::new(uri.as_bytes()).ok()?;
1673	    Some(code.render::<qrcode::render::unicode::Dense1x2>().quiet_zone(true).build())
1674	}
1675
1676	/// Persist a freshly-minted subnet + fire first-join home adoption (REQ-INST-15):
1677	/// a fresh subnet on a previously-unpaired node homes every local-only Self perch.
1678	fn save_minted(store: &SubnetStore) -> i32 {
1679	    if let Err(e) = store.save() {
1680	        eprintln!("SAVE_FAIL: freshly minted subnet not persisted: {e}");
1681	        return 1;
1682	    }
1683	    let mut vis = spt_store::visibility::VisibilityStore::load();
1684	    let adopted = spt_store::home::adopt_for_unset(&perch::owlery_dir(), store, &mut vis);
1685	    if !adopted.is_empty() {
1686	        let _ = vis.save();
1687	        eprintln!("HOME_ADOPTED:{}", adopted.join(","));
1688	    }
1689	    0
1690	}
1691
1692	/// Print a [`ShowResult`]: stdout block (if any), stderr status, exit code.
1693	fn finish_show(result: ShowResult, store: &SubnetStore) -> i32 {
1694	    if result.save {
1695	        let code = save_minted(store);
1696	        if code != 0 {
1697	            return code;
1698	        }
1699	    }
1700	    if let Some(out) = result.out {
1701	        print!("{out}");
1702	        let _ = std::io::stdout().flush();
1703	    }
1704	    eprintln!("{}", result.msg);
1705	    result.code
1706	}
1707
1708	/// `spt subnet show-code [NAME]` — surface a subnet's current code off this node,
1709	/// behind the OS-elevation gate (REQ-PAIR-3/6; ported off `spt pair show-totp` (renamed show-code), M7
1710	/// plan decision 3). Wires the real store, clock, and elevation probe into the
1711	/// unit-tested [`resolve`]/[`decide_show_code`] core.
1712	// [impl->REQ-PAIR-3] [impl->REQ-PAIR-6] [impl->REQ-SUBNET-1]
1713	fn cmd_subnet_show_code(name: Option<String>) -> i32 {
1714	    let mut store = SubnetStore::load();
1715	    let resolved = resolve(&mut store, name, None);
1716	    let result = decide_show_code(resolved, now_unix(), elevation::current());
1717	    finish_show(result, &store)
1718	}
1719
1720	/// `spt subnet create <NAME>` — mint a fresh subnet (this node becomes the sole
1721	/// seed-holder) and print its joining material (REQ-SUBNET-1; the verb-as-flag
1722	/// `show-totp --create-new` un-buried (now show-code), M7 plan decisions 3 + 11). Elevation-gated:
1723	/// the output reveals the subnet's joining secret.
1724	// [impl->REQ-SUBNET-1]
1725	fn cmd_subnet_create(name: String) -> i32 {
1726	    let mut store = SubnetStore::load();
1727	    let resolved = resolve(&mut store, None, Some(name));
1728	    let result = decide_create(resolved, now_unix(), elevation::current());
1729	    if result.save {
1730	        let code = save_minted(&store);
1731	        if code != 0 {
1732	            return code;
1733	        }
1734	    }
1735	    // Membership implies reachability (CONTEXT §subnet attachment): the
1736	    // sole seed-holder must be answering joins by the time the join hint
1737	    // below prints. After the persist (a refused create starts nothing),
1738	    // before the payload (the banner concludes with its divider first).
1739	    // [impl->REQ-SUBNET-2]
1740	    if result.code == 0 {
1741	        if let Err(code) = ensure_daemon_announced() {
1742	            return code;
1743	        }
1744	    }
1745	    if let Some(out) = result.out {
1746	        print!("{out}");
1747	        let _ = std::io::stdout().flush();
1748	    }
1749	    eprintln!("{}", result.msg);
1750	    result.code
1751	}
1752
1753	/// One subnet's status-view row: membership shape only — never seeds, epochs,
1754	/// or codes (M7 plan decision 5).
1755	struct SubnetStatusRow {
1756	    name: String,
1757	    /// Paired nodes known for the subnet (this node included).
1758	    nodes: usize,
1759	    /// Endpoints visible in the subnet's registry snapshot.
1760	    endpoints: usize,
1761	}
1762
1763	/// Build the status rows from the subnet store + registry snapshots. Pure over
1764	/// its inputs so the view is unit-tested hermetically. Counts are
1765	/// gossip-derived and stale-tolerant: a missing snapshot renders as this node
1766	/// alone with no advertised endpoints (counts catch up as gossip lands).
1767	// [impl->REQ-SUBNET-1]
1768	fn subnet_status_rows(
1769	    store: &SubnetStore,
1770	    regs: &std::collections::BTreeMap<String, spt_net::net::registry::SubnetRegistry>,
1771	    only: Option<&str>,
1772	) -> Vec<SubnetStatusRow> {
1773	    store
1774	        .subnets
1775	        .iter()
1776	        .filter(|s| only.is_none_or(|n| n == s.name))
1777	        .map(|s| {
1778	            let (mut nodes, mut endpoints) = (std::collections::BTreeSet::new(), 0usize);
1779	            if let Some(reg) = regs.get(&s.name) {
1780	                for id in reg.endpoint_ids() {
1781	                    endpoints += 1;
1782	                    for inst in reg.instances(id) {
1783	                        nodes.insert(inst.node.clone());
1784	                    }
1785	                }
1786	            }
1787	            // This node is a member even before its first row gossips in.
1788	            SubnetStatusRow { name: s.name.clone(), nodes: nodes.len().max(1), endpoints }
1789	        })

}

/// Decide `subnet create`'s rendered outcome from a resolution + clock + elevation.
/// Same gate-last shape as [`decide_show_code`] — a refused mint persists nothing and
/// leaks nothing — but the elevated output is the full joining material (M7 plan
/// decision 11): code, `otpauth://` URI, terminal QR, and the join hint. The gate is
/// the create half of the membership-mutation elevation requirement (a mint REVEALS
/// the subnet's joining secret).
// [impl->REQ-SUBNET-1] [impl->REQ-SUBNET-4]
fn decide_create(resolved: Resolved, now: u64, elevation: Elevation) -> ShowResult {
    match resolved {
        Resolved::Subnet { name, seed, minted } => match elevation::gate_for(elevation) {
            Gate::Show => {
                let totp = TotpSeed::from_bytes(seed);
                let uri = totp.otpauth_uri(ISSUER, &name);
                let mut out = format!("{}\n{}\n", totp.code_at(now), uri);
                // Terminal QR of the provisioning URI — scan it into an
                // authenticator app from the other machine's side of the desk.
                if let Some(qr) = qr_unicode(&uri) {
                    out.push_str(&qr);
                    out.push('\n');
                }
                out.push_str(&format!(
                    "Run `spt subnet join {name}` on the other machine to pair it in.\n"
                ));
                ShowResult {
                    code: 0,
                    out: Some(out),
                    msg: format!("SUBNET_CREATED:{name} (this node is the sole seed-holder)"),
                    save: minted,
                }
            }
            // Refused mint: nothing persists, nothing leaks (the seed-reveal
            // path needs confirmed privilege — same posture as show-code).
            Gate::Fallback => ShowResult {
                code: EXIT_NOT_ELEVATED,
                out: None,
                msg: format!(
                    "ELEVATION_REQUIRED: creating subnet '{name}' reveals its joining secret — \
                     re-run elevated (run as administrator / root)"
                ),
                save: false,
            },
        },
        other => decide_show_code(other, now, elevation),
    }
}

/// Render the provisioning URI as a terminal QR (unicode half-blocks). `None` when
/// encoding fails (URI too long for the QR alphabet) — the URI text above it is the
/// fallback, so create degrades gracefully instead of refusing.
fn qr_unicode(uri: &str) -> Option<String> {
    let code = qrcode::QrCode::new(uri.as_bytes()).ok()?;
    Some(code.render::<qrcode::render::unicode::Dense1x2>().quiet_zone(true).build())
}

/// Persist a freshly-minted subnet + fire first-join home adoption (REQ-INST-15):
/// a fresh subnet on a previously-unpaired node homes every local-only Self perch.
fn save_minted(store: &SubnetStore) -> i32 {
    if let Err(e) = store.save() {
        eprintln!("SAVE_FAIL: freshly minted subnet not persisted: {e}");
        return 1;
    }
    let mut vis = spt_store::visibility::VisibilityStore::load();
    let adopted = spt_store::home::adopt_for_unset(&perch::owlery_dir(), store, &mut vis);
    if !adopted.is_empty() {
        let _ = vis.save();
        eprintln!("HOME_ADOPTED:{}", adopted.join(","));
    }
    0
}

/// Print a [`ShowResult`]: stdout block (if any), stderr status, exit code.
fn finish_show(result: ShowResult, store: &SubnetStore) -> i32 {
    if result.save {
        let code = save_minted(store);
        if code != 0 {
            return code;
        }
    }
    if let Some(out) = result.out {
        print!("{out}");
        let _ = std::io::stdout().flush();
    }
    eprintln!("{}", result.msg);
    result.code
}

/// `spt subnet show-code [NAME]` — surface a subnet's current code off this node,
/// behind the OS-elevation gate (REQ-PAIR-3/6; ported off `spt pair show-totp` (renamed show-code), M7
/// plan decision 3). Wires the real store, clock, and elevation probe into the
/// unit-tested [`resolve`]/[`decide_show_code`] core.
// [impl->REQ-PAIR-3] [impl->REQ-PAIR-6] [impl->REQ-SUBNET-1]
fn cmd_subnet_show_code(name: Option<String>) -> i32 {
    let mut store = SubnetStore::load();
    let resolved = resolve(&mut store, name, None);
    let result = decide_show_code(resolved, now_unix(), elevation::current());
    finish_show(result, &store)
}

/// `spt subnet create <NAME>` — mint a fresh subnet (this node becomes the sole
/// seed-holder) and print its joining material (REQ-SUBNET-1; the verb-as-flag
/// `show-totp --create-new` un-buried (now show-code), M7 plan decisions 3 + 11). Elevation-gated:
/// the output reveals the subnet's joining secret.
// [impl->REQ-SUBNET-1]
fn cmd_subnet_create(name: String) -> i32 {
    let mut store = SubnetStore::load();
    let resolved = resolve(&mut store, None, Some(name));
    let result = decide_create(resolved, now_unix(), elevation::current());
    if result.save {
        let code = save_minted(&store);
        if code != 0 {
            return code;
        }
    }
    // Membership implies reachability (CONTEXT §subnet attachment): the
    // sole seed-holder must be answering joins by the time the join hint
    // below prints. After the persist (a refused create starts nothing),
    // before the payload (the banner concludes with its divider first).
    // [impl->REQ-SUBNET-2]
    if result.code == 0 {
        if let Err(code) = ensure_daemon_announced() {
            return code;
        }
    }
    if let Some(out) = result.out {
        print!("{out}");
        let _ = std::io::stdout().flush();
    }
    eprintln!("{}", result.msg);
    result.code
}

/// One subnet's status-view row: membership shape only — never seeds, epochs,
/// or codes (M7 plan decision 5).
struct SubnetStatusRow {
    name: String,
    /// Paired nodes known for the subnet (this node included).
    nodes: usize,
    /// Endpoints visible in the subnet's registry snapshot.
    endpoints: usize,
}

/// Build the status rows from the subnet store + registry snapshots. Pure over
/// its inputs so the view is unit-tested hermetically. Counts are
/// gossip-derived and stale-tolerant: a missing snapshot renders as this node
/// alone with no advertised endpoints (counts 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>,
    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(reg) = regs.get(&s.name) {
                for id in reg.endpoint_ids() {
                    endpoints += 1;
                    for inst in reg.instances(id) {
                        nodes.insert(inst.node.clone());
                    }
                }
            }
            // This node is a member even before its first row gossips in.
            SubnetStatusRow { name: s.name.clone(), nodes: nodes.len().max(1), endpoints }
        })


1. agree with rec 2. agree 3. also humanize join's ceremony line. anything barred behind admin is unlikely to be grepped anyway. 4. accept --- proceed


Edit

                out.push_str(&format!(
                    "Run `spt subnet join {name}` on the other machine to pair it in.\n"
                ));
                ShowResult {
                    code: 0,
                    out: Some(out),
                    msg: format!("SUBNET_CREATED:{name} (this node is the sole seed-holder)"),
                    save: minted,
                }

                out.push_str(&format!(
                    "Run `spt subnet join {name}` on other machines to have them join.\n\
                     Code expires at {}. To pair again, scan the QR with your auth app \
                     or run `spt subnet show-code`.\n",
                    code_expiry_local_hm(now)
                ));
                ShowResult {
                    code: 0,
                    out: Some(out),
                    msg: format!("SUBNET CREATED: {name} (this node is the sole seed-holder)"),
                    save: minted,
                }


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)

                out.push_str(&format!(
                    "Run `spt subnet join {name}` on the other machine to pair it in.\n"
                ));
                ShowResult {
                    code: 0,
                    out: Some(out),
                    msg: format!("SUBNET_CREATED:{name} (this node is the sole seed-holder)"),
                    save: minted,
                }

                out.push_str(&format!(
                    "Run `spt subnet join {name}` on other machines to have them join.\n\
                     Code expires at {}. To pair again, scan the QR with your auth app \
                     or run `spt subnet show-code`.\n",
                    code_expiry_local_hm(now)
                ));
                ShowResult {
                    code: 0,
                    out: Some(out),
                    msg: format!("SUBNET CREATED: {name} (this node is the sole seed-holder)"),
                    save: minted,
                }

-                    "Run `spt subnet join {name}` on the other machine to pair it in.\n"

+                    "Run `spt subnet join {name}` on other machines to have them join.\n\

+                     Code expires at {}. To pair again, scan the QR with your auth app \

+                     or run `spt subnet show-code`.\n",

+                    code_expiry_local_hm(now)

-                    msg: format!("SUBNET_CREATED:{name} (this node is the sole seed-holder)"),

+                    msg: format!("SUBNET CREATED: {name} (this node is the sole seed-holder)"),


/// Persist a freshly-minted subnet + fire first-join home adoption (REQ-INST-15):
/// a fresh subnet on a previously-unpaired node homes every local-only Self perch.
fn save_minted(store: &SubnetStore) -> i32 {
    if let Err(e) = store.save() {
        eprintln!("SAVE_FAIL: freshly minted subnet not persisted: {e}");
        return 1;
    }
    let mut vis = spt_store::visibility::VisibilityStore::load();
    let adopted = spt_store::home::adopt_for_unset(&perch::owlery_dir(), store, &mut vis);
    if !adopted.is_empty() {
        let _ = vis.save();
        eprintln!("HOME_ADOPTED:{}", adopted.join(","));
    }
    0
}

/// Persist a freshly-minted subnet + fire first-join home adoption
/// (REQ-INST-15): a fresh subnet on a previously-unpaired node homes every
/// local-only Self perch. Returns the adopted ids — the CALLER prints the
/// sync-home line, because it must land AFTER the created/joined ceremony
/// line (grill 2026-06-06).
fn save_minted(store: &SubnetStore) -> Result<Vec<String>, i32> {
    if let Err(e) = store.save() {
        eprintln!("SAVE_FAIL: freshly minted subnet not persisted: {e}");
        return Err(1);
    }
    let mut vis = spt_store::visibility::VisibilityStore::load();
    let adopted = spt_store::home::adopt_for_unset(&perch::owlery_dir(), store, &mut vis);
    if !adopted.is_empty() {
        let _ = vis.save();
    }
    Ok(adopted)
}

/// The sync-home adoption ceremony line (printed after created/joined).
fn announce_sync_home(adopted: &[String]) {
    if !adopted.is_empty() {
        eprintln!("NOW THE SYNC HOME FOR: {}", adopted.join(", "));
    }
}


/// Persist a freshly-minted subnet + fire first-join home adoption (REQ-INST-15):
/// a fresh subnet on a previously-unpaired node homes every local-only Self perch.
fn save_minted(store: &SubnetStore) -> i32 {
    if let Err(e) = store.save() {
        eprintln!("SAVE_FAIL: freshly minted subnet not persisted: {e}");
        return 1;
    }
    let mut vis = spt_store::visibility::VisibilityStore::load();
    let adopted = spt_store::home::adopt_for_unset(&perch::owlery_dir(), store, &mut vis);
    if !adopted.is_empty() {
        let _ = vis.save();
        eprintln!("HOME_ADOPTED:{}", adopted.join(","));
    }
    0
}

/// Persist a freshly-minted subnet + fire first-join home adoption
/// (REQ-INST-15): a fresh subnet on a previously-unpaired node homes every
/// local-only Self perch. Returns the adopted ids — the CALLER prints the
/// sync-home line, because it must land AFTER the created/joined ceremony
/// line (grill 2026-06-06).
fn save_minted(store: &SubnetStore) -> Result<Vec<String>, i32> {
    if let Err(e) = store.save() {
        eprintln!("SAVE_FAIL: freshly minted subnet not persisted: {e}");
        return Err(1);
    }
    let mut vis = spt_store::visibility::VisibilityStore::load();
    let adopted = spt_store::home::adopt_for_unset(&perch::owlery_dir(), store, &mut vis);
    if !adopted.is_empty() {
        let _ = vis.save();
    }
    Ok(adopted)
}

/// The sync-home adoption ceremony line (printed after created/joined).
fn announce_sync_home(adopted: &[String]) {
    if !adopted.is_empty() {
        eprintln!("NOW THE SYNC HOME FOR: {}", adopted.join(", "));
    }
}

-/// Persist a freshly-minted subnet + fire first-join home adoption (REQ-INST-15):

-/// a fresh subnet on a previously-unpaired node homes every local-only Self perch.

-fn save_minted(store: &SubnetStore) -> i32 {

+/// Persist a freshly-minted subnet + fire first-join home adoption

+/// (REQ-INST-15): a fresh subnet on a previously-unpaired node homes every

+/// local-only Self perch. Returns the adopted ids — the CALLER prints the

+/// sync-home line, because it must land AFTER the created/joined ceremony

+fn save_minted(store: &SubnetStore) -> Result<Vec<String>, i32> {

-        eprintln!("HOME_ADOPTED:{}", adopted.join(","));

+/// The sync-home adoption ceremony line (printed after created/joined).

+fn announce_sync_home(adopted: &[String]) {

+        eprintln!("NOW THE SYNC HOME FOR: {}", adopted.join(", "));

/// Print a [`ShowResult`]: stdout block (if any), stderr status, exit code.

fn finish_show(result: ShowResult, store: &SubnetStore) -> i32 {


/// Print a [`ShowResult`]: stdout block (if any), stderr status, exit code.
fn finish_show(result: ShowResult, store: &SubnetStore) -> i32 {
    if result.save {
        let code = save_minted(store);
        if code != 0 {
            return code;
        }
    }
    if let Some(out) = result.out {
        print!("{out}");
        let _ = std::io::stdout().flush();
    }
    eprintln!("{}", result.msg);
    result.code
}

/// Print a [`ShowResult`]: stdout block (if any), stderr status, exit code.
fn finish_show(result: ShowResult, store: &SubnetStore) -> i32 {
    let adopted = if result.save {
        match save_minted(store) {
            Ok(a) => a,
            Err(code) => return code,
        }
    } else {
        Vec::new()
    };
    if let Some(out) = result.out {
        print!("{out}");
        let _ = std::io::stdout().flush();
    }
    eprintln!("{}", result.msg);
    announce_sync_home(&adopted);
    result.code
}


/// Print a [`ShowResult`]: stdout block (if any), stderr status, exit code.
fn finish_show(result: ShowResult, store: &SubnetStore) -> i32 {
    if result.save {
        let code = save_minted(store);
        if code != 0 {
            return code;
        }
    }
    if let Some(out) = result.out {
        print!("{out}");
        let _ = std::io::stdout().flush();
    }
    eprintln!("{}", result.msg);
    result.code
}

/// Print a [`ShowResult`]: stdout block (if any), stderr status, exit code.
fn finish_show(result: ShowResult, store: &SubnetStore) -> i32 {
    let adopted = if result.save {
        match save_minted(store) {
            Ok(a) => a,
            Err(code) => return code,
        }
    } else {
        Vec::new()
    };
    if let Some(out) = result.out {
        print!("{out}");
        let _ = std::io::stdout().flush();
    }
    eprintln!("{}", result.msg);
    announce_sync_home(&adopted);
    result.code
}


    let result = decide_create(resolved, now_unix(), elevation::current());
    if result.save {
        let code = save_minted(&store);
        if code != 0 {
            return code;
        }
    }
    // Membership implies reachability (CONTEXT §subnet attachment): the
    // sole seed-holder must be answering joins by the time the join hint
    // below prints. After the persist (a refused create starts nothing),
    // before the payload (the banner concludes with its divider first).
    // [impl->REQ-SUBNET-2]
    if result.code == 0 {
        if let Err(code) = ensure_daemon_announced() {
            return code;
        }
    }
    if let Some(out) = result.out {
        print!("{out}");
        let _ = std::io::stdout().flush();
    }
    eprintln!("{}", result.msg);
    result.code
}

    let result = decide_create(resolved, now_unix(), elevation::current());
    let adopted = if result.save {
        match save_minted(&store) {
            Ok(a) => a,
            Err(code) => return code,
        }
    } else {
        Vec::new()
    };
    // Membership implies reachability (CONTEXT §subnet attachment): the
    // sole seed-holder must be answering joins by the time the join hint
    // below prints. After the persist (a refused create starts nothing),
    // before the payload (the banner concludes with its divider first).
    // [impl->REQ-SUBNET-2]
    if result.code == 0 {
        if let Err(code) = ensure_daemon_announced() {
            return code;
        }
    }
    if let Some(out) = result.out {
        print!("{out}");
        let _ = std::io::stdout().flush();
    }
    eprintln!("{}", result.msg);
    announce_sync_home(&adopted);
    result.code
}


    let result = decide_create(resolved, now_unix(), elevation::current());
    if result.save {
        let code = save_minted(&store);
        if code != 0 {
            return code;
        }
    }
    // Membership implies reachability (CONTEXT §subnet attachment): the
    // sole seed-holder must be answering joins by the time the join hint
    // below prints. After the persist (a refused create starts nothing),
    // before the payload (the banner concludes with its divider first).
    // [impl->REQ-SUBNET-2]
    if result.code == 0 {
        if let Err(code) = ensure_daemon_announced() {
            return code;
        }
    }
    if let Some(out) = result.out {
        print!("{out}");
        let _ = std::io::stdout().flush();
    }
    eprintln!("{}", result.msg);
    result.code
}

    let result = decide_create(resolved, now_unix(), elevation::current());
    let adopted = if result.save {
        match save_minted(&store) {
            Ok(a) => a,
            Err(code) => return code,
        }
    } else {
        Vec::new()
    };
    // Membership implies reachability (CONTEXT §subnet attachment): the
    // sole seed-holder must be answering joins by the time the join hint
    // below prints. After the persist (a refused create starts nothing),
    // before the payload (the banner concludes with its divider first).
    // [impl->REQ-SUBNET-2]
    if result.code == 0 {
        if let Err(code) = ensure_daemon_announced() {
            return code;
        }
    }
    if let Some(out) = result.out {
        print!("{out}");
        let _ = std::io::stdout().flush();
    }
    eprintln!("{}", result.msg);
    announce_sync_home(&adopted);
    result.code
}

let resolved = resolve(&mut store, None, Some(name));

-        let code = save_minted(&store);


Join's ceremony lines:


        if reply.ok {
            println!("JOINED:{name}");
            // First-join home adoption (REQ-INST-15): fires on join exactly
            // as on create — the daemon saved the seed; local-only Self
            // perches adopt the fresh sole subnet now rather than waiting
            // one advertisement cadence.
            let store = SubnetStore::load();
            let mut vis = spt_store::visibility::VisibilityStore::load();
            let adopted = spt_store::home::adopt_for_unset(&perch::owlery_dir(), &store, &mut vis);
            if !adopted.is_empty() {
                let _ = vis.save();
                eprintln!("HOME_ADOPTED:{}", adopted.join(","));
            }
            eprintln!(
                "Joined \"{name}\" ({}). `spt subnet status --nodes` shows the fleet \
                 as gossip lands.",
                reply.detail
            );
            return 0;
        }

        if reply.ok {
            println!("JOINED SUBNET: {name}");
            // First-join home adoption (REQ-INST-15): fires on join exactly
            // as on create — the daemon saved the seed; local-only Self
            // perches adopt the fresh sole subnet now rather than waiting
            // one advertisement cadence.
            let store = SubnetStore::load();
            let mut vis = spt_store::visibility::VisibilityStore::load();
            let adopted = spt_store::home::adopt_for_unset(&perch::owlery_dir(), &store, &mut vis);
            if !adopted.is_empty() {
                let _ = vis.save();
            }
            announce_sync_home(&adopted);
            eprintln!(
                "Joined \"{name}\" ({}). `spt subnet status --nodes` shows the fleet \
                 as gossip lands.",
                reply.detail
            );
            return 0;
        }


        if reply.ok {
            println!("JOINED:{name}");
            // First-join home adoption (REQ-INST-15): fires on join exactly
            // as on create — the daemon saved the seed; local-only Self
            // perches adopt the fresh sole subnet now rather than waiting
            // one advertisement cadence.
            let store = SubnetStore::load();
            let mut vis = spt_store::visibility::VisibilityStore::load();
            let adopted = spt_store::home::adopt_for_unset(&perch::owlery_dir(), &store, &mut vis);
            if !adopted.is_empty() {
                let _ = vis.save();
                eprintln!("HOME_ADOPTED:{}", adopted.join(","));
            }
            eprintln!(
                "Joined \"{name}\" ({}). `spt subnet status --nodes` shows the fleet \
                 as gossip lands.",
                reply.detail
            );
            return 0;
        }

        if reply.ok {
            println!("JOINED SUBNET: {name}");
            // First-join home adoption (REQ-INST-15): fires on join exactly
            // as on create — the daemon saved the seed; local-only Self
            // perches adopt the fresh sole subnet now rather than waiting
            // one advertisement cadence.
            let store = SubnetStore::load();
            let mut vis = spt_store::visibility::VisibilityStore::load();
            let adopted = spt_store::home::adopt_for_unset(&perch::owlery_dir(), &store, &mut vis);
            if !adopted.is_empty() {
                let _ = vis.save();
            }
            announce_sync_home(&adopted);
            eprintln!(
                "Joined \"{name}\" ({}). `spt subnet status --nodes` shows the fleet \
                 as gossip lands.",
                reply.detail
            );
            return 0;
        }

+            println!("JOINED SUBNET: {name}");

-                eprintln!("HOME_ADOPTED:{}", adopted.join(","));

+            announce_sync_home(&adopted);


Bash

cargo build -p spt 2>&1 | grep -E "^error" -A5 | head -20; cargo build -p spt 2>&1 | tail -1

Build with renames


Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.46s


cargo test -p spt --bin spt 2>&1 | grep -E "test result|FAILED|panicked at|assertion" | head -10

Run unit tests, find stale assertions


test cli::tests::create_prints_joining_material_when_elevated ... FAILED
thread 'cli::tests::create_prints_joining_material_when_elevated' (51272) panicked at crates\spt\src\cli.rs:4665:9:
assertion failed: r.msg.contains("SUBNET_CREATED:home")
test result: FAILED. 87 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 28.72s


4646	        assert!(parse(&["spt", "pair", "show-totp"]).is_err());
4647	        assert!(parse(&["spt", "pair"]).is_err());
4648	    }
4649
4650	    // [unit->REQ-SUBNET-1] elevated create prints the full joining material —
4651	    // code, otpauth URI, terminal QR, join hint — and requests the persist.
4652	    #[test]
4653	    fn create_prints_joining_material_when_elevated() {
4654	        let mut store = SubnetStore::default();
4655	        let now = 1_111_111_109;
4656	        let r = decide_create(resolve(&mut store, None, Some("home".into())), now, Elevation::Elevated);
4657	        assert_eq!(r.code, 0);
4658	        assert!(r.save, "a freshly minted subnet must be persisted on show");
4659	        let out = r.out.expect("elevated create prints");
4660	        let seed = store.find("home").unwrap().seed_bytes().unwrap();
4661	        assert!(out.starts_with(&TotpSeed::from_bytes(seed).code_at(now)), "current code first");
4662	        assert!(out.contains("otpauth://"), "provisioning URI present");
4663	        assert!(out.contains('█'), "terminal QR rendered (unicode blocks)");
4664	        assert!(out.contains("spt subnet join home"), "join hint names the subnet");
4665	        assert!(r.msg.contains("SUBNET_CREATED:home"));
4666	    }
4667
4668	    // [unit->REQ-SUBNET-1] [unit->REQ-SUBNET-4] an unelevated create refuses
4669	    // without minting or leaking (the seed-reveal posture; exit 3 like
4670	    // show-totp) — the create half of the membership-mutation gates.

        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 — 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!(r.msg.contains("SUBNET_CREATED:home"));
    }

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


Edit

    // [unit->REQ-SUBNET-1] elevated create prints the full joining material —
    // code, otpauth URI, terminal QR, join hint — 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!(r.msg.contains("SUBNET_CREATED:home"));
    }

    // [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] elevated create prints the full joining material —
    // code, otpauth URI, terminal QR, join hint — 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!(r.msg.contains("SUBNET_CREATED:home"));
    }

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

-    // code, otpauth URI, terminal QR, join hint — and requests the persist.

+    // 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.

-        assert!(r.msg.contains("SUBNET_CREATED:home"));

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

+    fn show_code_prints_reprovisioning_material() {

+        let mut store = store_with(&["home"]);

+        let r = decide_show_code(resolve(&mut store, None, None), now, Elevation::Elevated);

+        let out = r.out.expect("elevated show prints");

+        assert!(out.contains("otpauth://"));

+        assert!(out.contains('█'), "QR present on re-provision: {out}");

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


Bash

cargo test -p spt --bin spt 2>&1 | grep -E "test result|FAILED" | head -4

Re-run unit tests


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


grep -rln "show-totp" --include="*.md" --include="*.toml" --include="*.rs" . 2>/dev/null | grep -v target | grep -v "M7-PLAN\|M4-\|M5-\|M6-"

Sweep targets for show-code rename


./CONTEXT.md
./crates/spt/src/cli.rs
./crates/spt-daemon/tests/twohost.rs
./docs/adr/0005-totp-seeded-spake2-node-pairing.md
./docs/TWO-HOST-RUNBOOK.md
./docs-site/src/cli/reference.md
./docs-site/src/networking/overview.md
./ROADMAP.md
./traceable-reqs.toml


grep -n "show-totp" CONTEXT.md docs/adr/0005-totp-seeded-spake2-node-pairing.md docs/TWO-HOST-RUNBOOK.md docs-site/src/networking/overview.md ROADMAP.md traceable-reqs.toml crates/spt-daemon/tests/twohost.rs | head -20

List exact lines


CONTEXT.md:499:Every node in a subnet holds that subnet's seed, so the user can fetch the *current* code for **any subnet the node belongs to** from any node in it — no phone required if a trusted node is handy. Because a node may be in several subnets (*subnet membership*), the fetch is **per-subnet**: with several subnets and no name given, the CLI prompts *"Show TOTP for which subnet?"*; `spt subnet show-totp [name]` bypasses the prompt (the scripted path). Minting a new subnet is its own verb (`spt subnet create <name>`), not a fetch flag. The code is offered optionally as a **QR / `otpauth://` URI** so the seed can be stored directly in an authenticator app (Google Authenticator etc.).
docs/adr/0005-totp-seeded-spake2-node-pairing.md:55:- **Code fetch is per-subnet** (a node holds *N* seeds): `spt pair show-totp` prompts which subnet (or `--subnet <name>` / `--create-new` to bypass), offered optionally as a QR / `otpauth://` URI.
docs/adr/0005-totp-seeded-spake2-node-pairing.md:83:Surface rename, same semantics: `spt pair show-totp [--subnet|--create-new]`
docs/adr/0005-totp-seeded-spake2-node-pairing.md:84:became `spt subnet show-totp [NAME]` / `spt subnet create <NAME>` /
docs/TWO-HOST-RUNBOOK.md:34:  `spt pair show-totp` screen. **The wire ceremony is the real SPAKE2/TOTP
docs-site/src/networking/overview.md:22:  machine, `spt subnet show-totp` prints the current six digits (and an
docs-site/src/networking/overview.md:66:`spt subnet` (`status` · `create` · `join` · `show-totp`) ·
ROADMAP.md:45:- **M7 — Subnet & quickstart UX. 📋 planned (2026-06-05** — grill session; decisions ratified, task breakdown [`M7-PLAN.md`](./M7-PLAN.md)). Makes subnet setup a first-class, guided, secure product surface and the public quickstart honest about personas: the **join ceremony gets its product surface** (always-on daemon pairing responder + guided `spt subnet join` CLI initiator — today the rig-proven SPAKE2 wire is test-only) · new `spt subnet` noun namespace (`status [NAME] [--nodes]` / `create` / `show-totp` / `join`; `spt pair` deleted) · **elevation-gated membership mutations** (`create`/`join` — trust-boundary changes; ADR-0005 amendment) · **node labels** (hostname-default, gossiped, addressable in `@node` qualifiers with refuse-on-ambiguity) · `spt poll` removed (`ready` + `ready --once`) · **`spt how-to <topic>`** in-binary agent instructions (anti-drift: prompts point agents at the binary's own guidance) · quickstart rewritten as human-pairs / agents-message prompt blocks (lea + sergey; collapsible optional pairing section) · README/docs-site sweep + `quickstart_e2e` + a product-surface join rung on the twohost ladder. Runs **before** `spt-claude-code` scoping — CLI breaking changes are cheapest while no adapter depends on the surface.
traceable-reqs.toml:278:required_stages = ["impl", "unit"]  # activated M4-D2g: `spt pair show-totp [--subnet <name>|--create-new <name>]` surfaces a subnet's current code + otpauth:// URI off this node (cli.rs decide_show_totp). M7 NOTE: surface moves to `spt subnet show-totp [name]` and creation splits into `spt subnet create <name>` (REQ-SUBNET-1); the gate/resolution semantics here are unchanged. Per-subnet resolution: single auto-selects, multi refuses without --subnet (no guessing), --create-new mints a sole-holder subnet (SubnetStore::create_subnet). Code reuses spt_net TotpSeed (matches the ceremony exactly), never reimplemented. unit = single/multi/create-new/empty/unknown resolution + code-matches-seed. The cross-node fetch (off a *remote* paired node over IPC) is D4/D5; int = two-host at D9.
traceable-reqs.toml:299:title = "spt subnet noun namespace: status view (bare + status [NAME] [--nodes]), create (QR/otpauth), show-totp; spt pair deleted"
traceable-reqs.toml:300:required_stages = ["impl", "unit"]  # activated M7 D1 (2026-06-05): namespace + create/show-totp ports land; D2 adds the full status views. M7 D1/D2 (plan decisions 3, 5, 6, 11). Bare `spt subnet` = flagless status (names, paired-node counts, endpoint counts; never epochs/codes; hint footer; zero-subnet explanatory text). `status [NAME] [--nodes]` adds per-node rows (label, online/offline via gossip-recency + probe-stale hybrid, online/total endpoints). `create <NAME>` mints (sole seed-holder) + prints code/otpauth/terminal-QR + join hint. `show-totp [name]` keeps REQ-PAIR-3/6 semantics. `spt pair` namespace removed outright (no shim).
crates/spt-daemon/tests/twohost.rs:24://!   `spt subnet show-totp` screen (the wire ceremony itself is the REAL


perl -pi -e 's/`spt subnet show-totp \[name\]` bypasses/`spt subnet show-code [name]` bypasses/; s/"Show TOTP for which subnet\?"/"Show the code for which subnet?"/' CONTEXT.md
perl -pi -e 's/became `spt subnet show-totp \[NAME\]`/became `spt subnet show-code [NAME]` (named `show-totp` for five days)/' docs/adr/0005-totp-seeded-spake2-node-pairing.md
perl -pi -e 's/`spt subnet show-totp` prints the current six digits/`spt subnet show-code` prints the current six digits/; s/`join` · `show-totp`\)/`join` · `show-code`)/' docs-site/src/networking/overview.md
perl -pi -e 's/`spt subnet show-totp` screen/`spt subnet show-code` screen/' crates/spt-daemon/tests/twohost.rs
perl -pi -e 's/`spt pair show-totp` screen/`spt subnet show-code` screen/' docs/TWO-HOST-RUNBOOK.md
perl -pi -e 's{/ `create` / `show-totp` / `join`}{/ `create` / `show-code` / `join`}' ROADMAP.md
perl -pi -e 's/surface moves to `spt subnet show-totp \[name\]`/surface moves to `spt subnet show-code [name]` (renamed from show-totp in the M7 UX grill)/' traceable-reqs.toml
perl -pi -e 's/create \(QR\/otpauth\), show-totp; spt pair deleted/create (QR\/otpauth), show-code; spt pair deleted/' traceable-reqs.toml
perl -pi -e 's/`show-totp \[name\]` keeps REQ-PAIR-3\/6 semantics/`show-code [name]` (nee show-totp) keeps REQ-PAIR-3\/6 semantics + now re-provisions the full QR\/expiry material/' traceable-reqs.toml
grep -rn "show-totp" CONTEXT.md docs-site/src ROADMAP.md docs/TWO-HOST-RUNBOOK.md crates/spt-daemon/tests/twohost.rs traceable-reqs.toml 2>/dev/null | grep -v reference.md

Sweep rename across docs + registry


traceable-reqs.toml:278:required_stages = ["impl", "unit"]  # activated M4-D2g: `spt pair show-totp [--subnet <name>|--create-new <name>]` surfaces a subnet's current code + otpauth:// URI off this node (cli.rs decide_show_totp). M7 NOTE: surface moves to `spt subnet show-code [name]` (renamed from show-totp in the M7 UX grill) and creation splits into `spt subnet create <name>` (REQ-SUBNET-1); the gate/resolution semantics here are unchanged. Per-subnet resolution: single auto-selects, multi refuses without --subnet (no guessing), --create-new mints a sole-holder subnet (SubnetStore::create_subnet). Code reuses spt_net TotpSeed (matches the ceremony exactly), never reimplemented. unit = single/multi/create-new/empty/unknown resolution + code-matches-seed. The cross-node fetch (off a *remote* paired node over IPC) is D4/D5; int = two-host at D9.
traceable-reqs.toml:300:required_stages = ["impl", "unit"]  # activated M7 D1 (2026-06-05): namespace + create/show-totp ports land; D2 adds the full status views. M7 D1/D2 (plan decisions 3, 5, 6, 11). Bare `spt subnet` = flagless status (names, paired-node counts, endpoint counts; never epochs/codes; hint footer; zero-subnet explanatory text). `status [NAME] [--nodes]` adds per-node rows (label, online/offline via gossip-recency + probe-stale hybrid, online/total endpoints). `create <NAME>` mints (sole seed-holder) + prints code/otpauth/terminal-QR + join hint. `show-code [name]` (nee show-totp) keeps REQ-PAIR-3/6 semantics + now re-provisions the full QR/expiry material. `spt pair` namespace removed outright (no shim).


14	  Nodes also carry a human **label** (the hostname by default): views render
15	  `HFENDULEAM (bcead52b…)`, and `@node` qualifiers accept the label or a
16	  key prefix — several machines sharing a label are never guessed between.
17	- **Subnets** — machines join into named groups. A subnet shares: the
18	  endpoint registry (who exists, where, what state), context sync for its
19	  endpoints, notifications, and staged self-updates. Nothing is shared with
20	  nodes outside the subnet, ever.
21	- **Joining** — a one-time, code-authenticated ceremony. On a member
22	  machine, `spt subnet show-code` prints the current six digits (and an
23	  `otpauth://` URI — put the seed in your authenticator app); on the new
24	  machine, `spt subnet join <name>` finds a member over LAN + relay and
25	  runs the exchange. The code bootstraps a PAKE key exchange — the code is
26	  never the key, and a wrong guess learns nothing. Both sides pin each
27	  other's node keys on success (trust-on-first-use; key changes warn and
28	  never auto-apply). Every member machine answers join attempts
29	  automatically — no arming step on the existing fleet.
30	- **Elevation gates** — `subnet create` (reveals a fresh subnet's joining
...
58	## What rides it
59
60	Cross-machine `send`/`ring`, registry replication, two-tier mind sync,
61	remote attach, remote suspend/wake, file transfer, notification replication,
62	and peer-propagated self-update — all over the same subnet substrate.
63
64	## Commands
65
66	`spt subnet` (`status` · `create` · `join` · `show-code`) ·
67	`spt resources` · the qualified addressing forms (`[subnet:]id[@node]`,
68	where `@node` is a label or key prefix) — [CLI reference](../cli/reference.md).


1	//! Two-host cross-node E2E — the M4-PLAN §D9b ladder on a REAL rig (D9-6).
2	//!
3	//! **Env-gated: runs only with `SPT_TWO_HOST=1` + a role** — CI never sets it,
4	//! so this is a silent no-op there; the real runs happen on the rig hosts
5	//! (HFENDULEAM Windows ↔ gravity Linux) per `docs/TWO-HOST-RUNBOOK.md`. Unlike
6	//! every loopback E2E in this suite, the two sides here are **separate
7	//! processes on separate machines** talking over a real LAN/WAN path.
8	//!
9	//! ## The ladder (one run climbs all of it)
10	//!
11	//! pair → register → cross-node message → remote-drive (attach + file
12	//! transfer) → Psyche sync (both directions) → update self-heal → notif
13	//! cross-node (insert on A fires on B; dismiss on B replicates to A).
14	//!
15	//! ## Roles + rendezvous (no runtime address exchange)
16	//!
17	//! Every identity and address is **derived from a shared secret** + static
18	//! env, so either side can be driven non-interactively (the gravity leg runs
19	//! under a commit-message-gated CI step; no SSH between the rig hosts):
20	//!
21	//! - node identities: `sha256(secret:"node-a"/"node-b")` → [`Identity`] seeds;
22	//! - the subnet TOTP seed: `sha256(secret:"subnet-totp")[..20]` — role A reads
23	//!   the 6-digit code off it exactly like a user reading the seed-holder's
24	//!   `spt subnet show-code` screen (the wire ceremony itself is the REAL
25	//!   SPAKE2/TOTP exchange — only the operator's eyeballs are simulated);
26	//! - the release signing key: `sha256(secret:"release")`;
27	//! - dialable addresses: derived node id + `SPT_TWO_HOST_PEER_IP` + the fixed
28	//!   ports ([`BindScope::Port`]) — id-only tickets with one static ip:port,
29	//!   relays and discovery disabled.
30	//!
31	//! Role B (seed-holder, server) seeds its stores, answers the pairing
32	//! ceremony, then runs the production dispatcher + peer pump and *reacts*
33	//! (dismisses the marker notif, asserts its rungs, waits for A's done-file).
34	//! Role A (joiner, driver) pairs, then drives every remaining rung and
35	//! asserts convergence. Cross-host barriers are the protocols themselves —
36	//! poll-until-deadline on observable store state, never wall-clock guesses.
37	//!
38	//! ## Env knobs
39	//!
40	//! | var | role | meaning |
41	//! |-----|------|---------|
42	//! | `SPT_TWO_HOST` | both | `1` activates (anything else: silent skip) |
43	//! | `SPT_TWO_HOST_ROLE` | both | `a` (joiner/driver) or `b` (seed-holder/server) |
44	//! | `SPT_TWO_HOST_SECRET` | both | shared secret (identical on both hosts) |
45	//! | `SPT_TWO_HOST_PEER_IP` | both | the OTHER host's LAN IP |
46	//! | `SPT_TWO_HOST_SUBNET` | both | subnet name (default `twohost`) |
47	//! | `SPT_TWO_HOST_PORT_A` | both | A's broker UDP port (default 7460) |
48	//! | `SPT_TWO_HOST_PORT_B` | both | B's broker UDP port (default 7461) |
49	//! | `SPT_TWO_HOST_PORT_PAIR` | both | B's pre-trust pairing port (default 7462) |
50	//! | `SPT_TWO_HOST_WAIT_SECS` | both | per-rung convergence deadline (default 300) |
51
52	use std::net::{IpAddr, SocketAddr};
53	use std::path::PathBuf;
54	use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
55	use std::sync::Arc;
56	use std::thread;
57	use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
58
59	use ed25519_dalek::{Signer, SigningKey};
60	use sha2::{Digest, Sha256};
61
62	use spt_daemon::brain::Brain;
63	use spt_daemon::broker::Broker;
64	use spt_daemon::dispatch::{run_dispatch_loop, DispatchPaths};
65	use spt_daemon::nethost::{NetConfig, NetHost};
66	use spt_daemon::peerloop::{run_peer_pump, PeerResolver, PumpCadence, PumpConfig, PumpHooks, PumpPaths};
67	use spt_daemon::registryhost::RegistryHost;
68	use spt_daemon::relcache::ReleaseCache;
69	use spt_daemon::release::{sha256_hex, ReleaseMetadata, SignedRelease};
70	use spt_daemon::update::BROKER_RESOURCE_ABI;
71	use spt_net::net::endpoint::{
72	    addr_for_node_hex, BindScope, EndpointAddr, LocalDiscovery, RelayPolicy,
73	};
74	use spt_net::net::pairing::totp::TotpSeed;
75	use spt_net::net::wanmsg::WanMessage;
76	use spt_proto::identity::Identity;
77	use spt_store::contextstore::ContextStore;
78	use spt_store::epoch::EpochSource;
79	use spt_store::info::{self, InfoJson};
80	use spt_store::notif::{NotifRow, NotifStore};
81	use spt_store::perch::{self, ParentHint};
82	use spt_store::spool;
83	use spt_store::subnet::SubnetStore;
84	use spt_store::trust::TrustStore;
85	use spt_store::visibility::VisibilityStore;
86	use tempfile::TempDir;
87
88	const ID_A: &str = "ling-twohost-a";
89	const ID_B: &str = "ling-twohost-b";
90	const WAN_BODY: &str = "two-host hello";
91	const NOTIF_BODY: &str = "twohost ping";
92	const DRIVE_MARKER: &str = "twohost-drive";
93	const RELEASE_BYTES: &[u8] = b"twohost propagated binary v6";
94	// M5 rungs (D9a).
95	const PRESENCE_BODY: &str = "twohost presence ping";
96	const TOAST_BODY: &str = "twohost toast me";
97	const SHELL_ALIAS: &str = "Toast";
98
99	/// The rig's static configuration, fully determined by env (no runtime
100	/// exchange between the hosts).
101	struct Rig {
102	    subnet: String,
103	    peer_ip: IpAddr,
104	    port_a: u16,
105	    port_b: u16,
106	    wait: Duration,
107	    id_a: Identity,
108	    id_b: Identity,
109	    totp: TotpSeed,
110	    release_sk: SigningKey,
111	}
112
113	impl Rig {
114	    /// Read the rig from env iff `SPT_TWO_HOST=1` and the role matches;
115	    /// `None` = silent skip (the CI path).
116	    fn from_env(role: &str) -> Option<Rig> {
117	        if std::env::var("SPT_TWO_HOST").as_deref() != Ok("1") {
118	            return None;
119	        }
120	        if std::env::var("SPT_TWO_HOST_ROLE").ok()?.to_ascii_lowercase() != role {
121	            return None;
122	        }
123	        let secret = std::env::var("SPT_TWO_HOST_SECRET")
124	            .expect("SPT_TWO_HOST_SECRET must be set (same value on both hosts)");
125	        let peer_ip: IpAddr = std::env::var("SPT_TWO_HOST_PEER_IP")
126	            .expect("SPT_TWO_HOST_PEER_IP must be set (the other host's IP)")
127	            .parse()
128	            .expect("SPT_TWO_HOST_PEER_IP parses as an IP address");
129	        let mut t20 = [0u8; 20];
130	        t20.copy_from_slice(&derive32(&secret, "subnet-totp")[..20]);
131	        Some(Rig {
132	            subnet: env_or_str("SPT_TWO_HOST_SUBNET", "twohost"),
133	            peer_ip,
134	            port_a: env_or("SPT_TWO_HOST_PORT_A", 7460),
135	            port_b: env_or("SPT_TWO_HOST_PORT_B", 7461),
136	            // SPT_TWO_HOST_PORT_PAIR retired (M7 D3): the pairing responder
137	            // rides the broker endpoint — there is no dedicated pairing port.
138	            wait: Duration::from_secs(env_or("SPT_TWO_HOST_WAIT_SECS", 300)),
139	            id_a: Identity::from_seed(&derive32(&secret, "node-a")),
140	            id_b: Identity::from_seed(&derive32(&secret, "node-b")),
141	            totp: TotpSeed::from_bytes(t20),
142	            release_sk: SigningKey::from_bytes(&derive32(&secret, "release")),
143	        })
144	    }
145
146	    fn a_hex(&self) -> String {
147	        self.id_a.public_key().to_hex()
148	    }
149	    fn b_hex(&self) -> String {
150	        self.id_b.public_key().to_hex()
151	    }
152
153	    /// The peer's broker address as the brain-IPC JSON value: derived node id
154	    /// + the static ip:port (no relay, no discovery — one direct path).
155	    fn peer_broker_addr(&self, role: &str) -> serde_json::Value {
156	        let (hex, port) = match role {
157	            "a" => (self.b_hex(), self.port_b),
158	            _ => (self.a_hex(), self.port_a),
159	        };
160	        let addr = addr_for_node_hex(&hex)
161	            .expect("derived node hex is a valid key")
162	            .with_ip_addr(SocketAddr::new(self.peer_ip, port));
163	        serde_json::to_value(&addr).expect("EndpointAddr serializes")
164	    }
165
166	    /// B's pairing-responder address (role A's joiner dials this). M7 D3:
167	    /// the responder rides B's BROKER endpoint (pairing ALPN beside the
168	    /// trusted surface) — same node id, same fixed port; no dedicated
169	    /// pairing endpoint exists anymore.

... [864 lines truncated] ...


grep -n "PORT_PAIR" docs/TWO-HOST-RUNBOOK.md crates/spt-daemon/tests/twohost.rs

Find stale PORT_PAIR docs


docs/TWO-HOST-RUNBOOK.md:52:| `SPT_TWO_HOST_PORT_PAIR` | both | B's pre-trust pairing UDP port | default `7462` |
crates/spt-daemon/tests/twohost.rs:49://! | `SPT_TWO_HOST_PORT_PAIR` | both | B's pre-trust pairing port (default 7462) |
crates/spt-daemon/tests/twohost.rs:136:            // SPT_TWO_HOST_PORT_PAIR retired (M7 D3): the pairing responder


perl -ni -e 'print unless /SPT_TWO_HOST_PORT_PAIR.*pre-trust pairing/' docs/TWO-HOST-RUNBOOK.md crates/spt-daemon/tests/twohost.rs && grep -c "PORT_PAIR" docs/TWO-HOST-RUNBOOK.md crates/spt-daemon/tests/twohost.rs

Drop stale PORT_PAIR table rows


docs/TWO-HOST-RUNBOOK.md:0
crates/spt-daemon/tests/twohost.rs:1


cargo test -p spt 2>&1 | grep -E "test result" | awk '{s+=$4; f+=$6} END {print "spt: "s" passed, "f" failed"}'; cargo test -p spt-daemon --test twohost 2>&1 | grep "test result"; cargo run -p xtask -- gen 2>&1 | tail -1 && cargo run -p xtask -- check 2>&1 | tail -1; traceable-reqs check >/dev/null 2>&1; echo "reqs=$?"

Full gates + reference regen


spt: 99 passed, 0 failed
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
xtask gen: wrote C:\Users\decid\Documents\projects\spt-core\docs-site/src/cli/reference.md
xtask check: OK
reqs=0


cargo build -p spt --release 2>&1 | tail -1; H=$(mktemp -d); SPT_HOME=$H ./target/release/spt.exe subnet show-code 2>&1; echo "exit=$?"; SPT_HOME=$H ./target/release/spt.exe subnet create x 2>&1 | head -2; echo "exit=$?"

Release build + smoke new verbs (unelevated)


    Finished `release` profile [optimized] target(s) in 8.87s
NO_SUBNETS: this node belongs to no subnet — mint one with `spt subnet create <name>`
exit=2
ELEVATION_REQUIRED: creating subnet 'x' reveals its joining secret — re-run elevated (run as administrator / root)
exit=0


git add -A && git commit -m "$(cat <<'EOF'
feat(cli): M7 UX revision — show-totp -> show-code, humanized ceremony lines, code expiry

From the acceptance-run grill (2026-06-06), rolled into M7 pre-publish:

- `spt subnet show-totp` renamed `show-code` (user mental model is "the
  code", not the RFC). No alias — zero users. It is now the full
  RE-PROVISIONING surface: code + otpauth:// URI + terminal QR + expiry —
  finally fulfilling ADR-0005's "re-provision the QR from any
  still-trusted node" with an actual command.
- Code expiry shown in local time, floored to the minute, on create and
  show-code. The floor is computed off the ±1-acceptance window's end
  (always a :00/:30 boundary), so the printed minute is ALWAYS still a
  true promise — a naive floor of the current window could print a time
  already past. New dep: chrono (clock feature only; std has no local tz).
- Ceremony lines humanized (admin-gated output is read, not grepped):
  `SUBNET CREATED: BIGNET` / `JOINED SUBNET: BIGNET` /
  `NOW THE SYNC HOME FOR: alice` (the adoption line now prints AFTER the
  created/joined line — save_minted returns the adopted ids; the caller
  owns the ceremony order). Hot-path tags (SENT/QUEUED/READY) untouched.
- create's closing guidance: "Run `spt subnet join <name>` on other
  machines to have them join." + "Code expires at HH:MM. To pair again,
  scan the QR with your auth app or run `spt subnet show-code`."
- Sweep: docs-site (networking), CONTEXT.md fetch wording, ROADMAP,
  TWO-HOST-RUNBOOK (+ retired PORT_PAIR rows), traceable-reqs
  annotations, reference regen.

spt suite 99 green; xtask check + traceable-reqs check green.

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

Commit UX revision


warning: in the working copy of 'CONTEXT.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'Cargo.lock', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'ROADMAP.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/tests/twohost.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt/Cargo.toml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt/src/cli.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs-site/src/cli/reference.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs-site/src/networking/overview.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs/TWO-HOST-RUNBOOK.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs/adr/0005-totp-seeded-spake2-node-pairing.md', LF will be replaced by CRLF the next time Git touches it
[dev-freeform d6c9543] feat(cli): M7 UX revision — show-totp -> show-code, humanized ceremony lines, code expiry
 11 files changed, 161 insertions(+), 87 deletions(-)
d6c9543 feat(cli): M7 UX revision — show-totp -> show-code, humanized ceremony lines, code expiry


Redeploy both machines (includes eeb6f01 banner + d6c9543 UX):


PowerShell

Copy-Item target\release\spt.exe "$env:LOCALAPPDATA\spt-core\bin\spt.exe" -Force; & "$env:LOCALAPPDATA\spt-core\bin\spt.exe" subnet show-code --help | Select-Object -First 1

Redeploy local install


Show a subnet's current 6-digit pairing code (+ URI and QR).


Bash

ssh david@100.100.62.43 'cd ~/actions-runner/_work/spt-core/spt-core && git fetch origin dev-freeform && git checkout -q d6c9543 2>/dev/null || git checkout -q FETCH_HEAD; git log -1 --oneline; export PATH=$HOME/.cargo/bin:$PATH && cargo build --release -p spt > /tmp/spt-release-build2.log 2>&1 && cp target/release/spt ~/.local/bin/spt && echo INSTALLED && ~/.local/bin/spt subnet show-code --help | head -1' 2>&1

Pull + rebuild + reinstall on gravity


Command running in background with ID: bejy2lmw8. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\bejy2lmw8.output. You will be notified when it completes. To check interim output, use Read on that file path.


<task-notification>
<task-id>bejy2lmw8</task-id>
<tool-use-id>toolu_01PSBnCkuyq74qGDFMqJpJib</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\bejy2lmw8.output</output-file>
<status>completed</status>
<summary>Background command "Pull + rebuild + reinstall on gravity" completed (exit code 0)</summary>
</task-notification>


git push origin dev-freeform 2>&1 | tail -2

Push dev-freeform with UX commits


To https://github.com/SaberMage/spt-core.git
   7a22a63..d6c9543  dev-freeform -> dev-freeform


Prompt:
<task-notification>
<task-id>bejy2lmw8</task-id>
<tool-use-id>toolu_01PSBnCkuyq74qGDFMqJpJib</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\bejy2lmw8.output</output-file>
<status>completed</status>
<summary>Background command "Pull + rebuild + reinstall on gravity" completed (exit code 0)</summary>
</task-notification>


1	From https://github.com/SaberMage/spt-core
2	 * branch            dev-freeform -> FETCH_HEAD
3	7a22a63 fix(daemon): M7 [twohost] — responder must let the joiner close first (Done/Seed close race)
4	INSTALLED
5	error: unrecognized subcommand 'show-code'
6
7	  tip: a similar subcommand exists: 'show-totp'
8
9	Usage: spt subnet [COMMAND]
10
11	For more information, try '--help'.
12

From https://github.com/SaberMage/spt-core
 * branch            dev-freeform -> FETCH_HEAD
7a22a63 fix(daemon): M7 [twohost] — responder must let the joiner close first (Done/Seed close race)
INSTALLED
error: unrecognized subcommand 'show-code'

  tip: a similar subcommand exists: 'show-totp'

Usage: spt subnet [COMMAND]

For more information, try '--help'.


As predicted — gravity built the old tip. Re-running post-push:


ssh david@100.100.62.43 'cd ~/actions-runner/_work/spt-core/spt-core && git fetch origin dev-freeform && git checkout -q d6c9543 && git log -1 --oneline && export PATH=$HOME/.cargo/bin:$PATH && cargo build --release -p spt > /tmp/spt-release-build3.log 2>&1 && cp target/release/spt ~/.local/bin/spt && echo INSTALLED && ~/.local/bin/spt subnet show-code --help | head -1' 2>&1

Gravity redeploy at d6c9543


Command running in background with ID: boox2xmvv. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\boox2xmvv.output. You will be notified when it completes. To check interim output, use Read on that file path.


State while gravity rebuilds:

- **Shipped** (eeb6f01 + d6c9543, pushed — CI running single-host jobs): show-code rename + QR/expiry re-provisioning, expiry math off ±1 window end (printed minute always true), humanized ceremony lines in your specified order, sync-home after created/joined, daemon activation + banner, doc/registry sweeps. All gates green.
- **HFENDULEAM** ✓ redeployed (`show-code` answers from install dir).
- **gravity** ⏳ rebuilding at d6c9543 — will confirm.

Will report gravity + CI when they land. Then both machines are ready for the acceptance run — elevated `spt subnet create <name>` here, `spt subnet join <name>` on gravity.


<task-notification>
<task-id>boox2xmvv</task-id>
<tool-use-id>toolu_01WZALjzLT9cSgf6rbZCA7cz</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\boox2xmvv.output</output-file>
<status>completed</status>
<summary>Background command "Gravity redeploy at d6c9543" completed (exit code 0)</summary>
</task-notification>


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


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


1	d6c9543 feat(cli): M7 UX revision — show-totp -> show-code, humanized ceremony lines, code expiry
2	From https://github.com/SaberMage/spt-core
3	 * branch            dev-freeform -> FETCH_HEAD
4	INSTALLED
5	Show a subnet's current 6-digit pairing code (+ URI and QR).
6

d6c9543 feat(cli): M7 UX revision — show-totp -> show-code, humanized ceremony lines, code expiry
From https://github.com/SaberMage/spt-core
 * branch            dev-freeform -> FETCH_HEAD
INSTALLED
Show a subnet's current 6-digit pairing code (+ URI and QR).


**Both machines deployed at d6c9543** — gravity's `show-code` answers. CI:


gh run list --branch dev-freeform --limit 1 2>&1

Check CI for pushed commits


in_progress		feat(cli): M7 UX revision — show-totp -> show-code, humanized ceremon…	ci	dev-freeform	push	27058900698	11m12s	2026-06-06T09:40:29Z


CI running (single-host jobs only — no `[twohost]` tag on these commits; the rig already proved the wire at 7a22a63 and nothing daemon-side changed since except the banner, which is CLI-local).

**Acceptance-ready.** Your script:

```powershell
# HFENDULEAM — admin terminal (real home this time, BIGNET already exists —
# either reuse it or wipe %LOCALAPPDATA%\spt-core first for the "fresh" run):
spt subnet create <name>
```
Expect: code → URI → QR → join/expiry guidance → `SUBNET CREATED:` → `NOW THE SYNC HOME FOR:` — with the coming-online banner above it all if the daemon was down.

```sh
# gravity — sudo:
sudo ~/.local/bin/spt subnet join <name>     # prompts for the code
~/.local/bin/spt subnet status --nodes
```
Expect: `JOINED SUBNET: <name>`, then both sides' `status --nodes` showing two labeled nodes.

I'll watch CI quietly; run when ready and feed me the outputs.


when i run `sudo spt subnet join`, i get error "sudo: spt: command not found"


`sudo` resets PATH (`secure_path`) — `~/.local/bin` isn't in root's. But there's a second trap behind it: sudo also resets `HOME` → root's `~/.spt-core` → the seed would land in **root's** universe and david's `status` would show nothing. Do it this way on gravity:

```sh
# 1. as david — bring the daemon up under YOUR account first
#    (any daemon-needing command spawns it detached; NO_DIGEST is expected noise):
~/.local/bin/spt digest x

# 2. join — elevated, but binary by absolute path and HOME pinned to yours:
sudo HOME=$HOME ~/.local/bin/spt subnet join <name>

# 3. back as david:
~/.local/bin/spt subnet status --nodes
```

Why step 1 matters: with the daemon already alive as david, the elevated join's `ensure_running` sees it and the ceremony executes *inside david's daemon* (the broker runs the joiner leg) — no root-owned daemon, no root-owned state files.

Honest note: this is a real Linux-elevation UX wart, not just your shell. Windows elevation keeps the same user profile; `sudo` switches universes. Logging it as a design follow-up (gate mechanics on Linux: pre-existing user daemon required / polkit / sudo-group check) so M8 decides properly. Run the steps; feed me outputs.


for the design followup, i'd prefer if 1. installation symlinks spt binary to sudo home 2. first `sudo spt` detects that it's elevated and prompts user to pick the default user account for execution. from there, a `sudo spt` daemon launch always launches under the default user account. --- ran `spt subnet join` for the fully guided flow. it sat searching for awhile and eventually timed out with "NO_SEED_HOLDER". tried twice, happened twice. confirmed both machines have matching clocks. --- side note: machines should prioritize using a well-known world clock server FIRST, fallback to local time.


Three threads. Capturing the two design items, then debugging the real failure — which I can do from here (this box is the seed-holder).

First the likely cause: **HFENDULEAM has no daemon.** Your BIGNET create ran under the *old* binary (pre-activation fix) — it minted the seed and exited; nothing since has started a daemon on the real home. Dark seed-holder ⇒ `NO_SEED_HOLDER`, exactly the gap we fixed but never re-ran create for. Verifying:


PowerShell

Get-Process spt -ErrorAction SilentlyContinue | Select-Object Id,Path; & "$env:LOCALAPPDATA\spt-core\bin\spt.exe" subnet 2>&1

Check daemon + real-home subnet state


SUBNET  NODES  ENDPOINTS
BIGNET  1      0

hint: `spt subnet create <name>` mints a new subnet on this machine
hint: `spt subnet join <name>` pairs this machine into one (elevated)
hint: `spt subnet show-code [name]` shows a subnet's joining code (elevated)
   Id Path
   -- ----
54424


Bash

grep -rn "fn seed_socket_name\|fn broker_socket_name\|fn digest_socket_name" crates/spt-daemon/src/*.rs | head -4

Find socket name derivation


crates/spt-daemon/src/endpoint.rs:32:pub fn seed_socket_name() -> String {
crates/spt-daemon/src/endpoint.rs:37:pub fn broker_socket_name() -> String {
crates/spt-daemon/src/endpoint.rs:43:pub fn digest_socket_name() -> String {


1	//! Canonical per-machine daemon endpoint names + pid path (B6).
2	//!
3	//! The daemon binds two local-IPC listeners — the broker (PTY hosting, B1/B2)
4	//! and the seed-control channel (B6) — under names derived from the active
5	//! `spt_home`. Deriving from `spt_home` keeps each isolated home (a test's
6	//! `SPT_HOME` override, a per-user install) on its own daemon, so parallel test
7	//! processes never collide on one machine while a real install gets one stable
8	//! per-home daemon. The auto-start spawn inherits `SPT_HOME`, so the spawned
9	//! daemon computes the **same** names as the client that started it — that
10	//! shared derivation is the whole of the rendezvous (no registry lookup).
11	// [impl->REQ-DAEMON-3]
12
13	use std::hash::{Hash, Hasher};
14	use std::path::PathBuf;
15
16	use spt_store::perch::spt_home;
17
18	/// A deterministic short tag for the active spt home. `DefaultHasher::new()` is
19	/// seeded with fixed keys (it is **not** randomized like `RandomState`), so the
20	/// tag is stable across processes that share the same `spt_home` — exactly what
21	/// the auto-start handshake needs (the spawned daemon must independently derive
22	/// the launching client's socket name).
23	fn home_tag() -> String {
24	    let mut h = std::collections::hash_map::DefaultHasher::new();
25	    spt_home().hash(&mut h);
26	    format!("{:016x}", h.finish())
27	}
28
29	/// The seed-control listener name (B6): the channel `api seed`/`api listen`
30	/// connect to for the in-memory seed map. It is also the daemon's liveness
31	/// anchor — present for the daemon's whole lifetime.
32	pub fn seed_socket_name() -> String {
33	    format!("spt-daemon-seed-{}.sock", home_tag())
34	}
35
36	/// The broker listener name (B1/B2): PTY hosting + gapless handoff.
37	pub fn broker_socket_name() -> String {
38	    format!("spt-daemon-broker-{}.sock", home_tag())
39	}
40
41	/// The digest-control listener name (B8): the channel `spt digest <id>` connects
42	/// to for a snapshot pull or a delta-stream subscribe (ADR-0008 / REQ-TERM-4).
43	pub fn digest_socket_name() -> String {
44	    format!("spt-daemon-digest-{}.sock", home_tag())
45	}
46
47	/// The daemon's pid file: `<spt_home>/daemon.pid`. Best-effort breadcrumb the
48	/// running daemon writes on start — discoverable for management/cleanup. Not a
49	/// liveness signal (liveness is the seed-control ping); a stale pid here is
50	/// harmless because callers probe the socket, never this file.
51	pub fn daemon_pid_path() -> PathBuf {
52	    spt_home().join("daemon.pid")
53	}
54

//! Canonical per-machine daemon endpoint names + pid path (B6).
//!
//! The daemon binds two local-IPC listeners — the broker (PTY hosting, B1/B2)
//! and the seed-control channel (B6) — under names derived from the active
//! `spt_home`. Deriving from `spt_home` keeps each isolated home (a test's
//! `SPT_HOME` override, a per-user install) on its own daemon, so parallel test
//! processes never collide on one machine while a real install gets one stable
//! per-home daemon. The auto-start spawn inherits `SPT_HOME`, so the spawned
//! daemon computes the **same** names as the client that started it — that
//! shared derivation is the whole of the rendezvous (no registry lookup).
// [impl->REQ-DAEMON-3]

use std::hash::{Hash, Hasher};
use std::path::PathBuf;

use spt_store::perch::spt_home;

/// A deterministic short tag for the active spt home. `DefaultHasher::new()` is
/// seeded with fixed keys (it is **not** randomized like `RandomState`), so the
/// tag is stable across processes that share the same `spt_home` — exactly what
/// the auto-start handshake needs (the spawned daemon must independently derive
/// the launching client's socket name).
fn home_tag() -> String {
    let mut h = std::collections::hash_map::DefaultHasher::new();
    spt_home().hash(&mut h);
    format!("{:016x}", h.finish())
}

/// The seed-control listener name (B6): the channel `api seed`/`api listen`
/// connect to for the in-memory seed map. It is also the daemon's liveness
/// anchor — present for the daemon's whole lifetime.
pub fn seed_socket_name() -> String {
    format!("spt-daemon-seed-{}.sock", home_tag())
}

/// The broker listener name (B1/B2): PTY hosting + gapless handoff.
pub fn broker_socket_name() -> String {
    format!("spt-daemon-broker-{}.sock", home_tag())
}

/// The digest-control listener name (B8): the channel `spt digest <id>` connects
/// to for a snapshot pull or a delta-stream subscribe (ADR-0008 / REQ-TERM-4).
pub fn digest_socket_name() -> String {
    format!("spt-daemon-digest-{}.sock", home_tag())
}

/// The daemon's pid file: `<spt_home>/daemon.pid`. Best-effort breadcrumb the
/// running daemon writes on start — discoverable for management/cleanup. Not a
/// liveness signal (liveness is the seed-control ping); a stale pid here is
/// harmless because callers probe the socket, never this file.
pub fn daemon_pid_path() -> PathBuf {
    spt_home().join("daemon.pid")
}


Sockets are home-scoped — no shadowing possible. Identify 54424 + real-home daemon liveness:


PowerShell

Get-Content "$env:LOCALAPPDATA\spt-core\daemon.pid" -ErrorAction SilentlyContinue; Get-CimInstance Win32_Process -Filter "ProcessId=54424" | Select-Object ProcessId,CommandLine | Format-List

Real-home daemon pid + identify process 54424


54424

ProcessId   : 54424
CommandLine :

54424

ProcessId   : 54424
CommandLine :


for the design followup, i'd prefer if 1. installation symlinks spt binary to sudo home 2. first `sudo spt` detects that it's elevated and prompts user to pick the default user account for execution.…


Bash

ssh david@100.100.62.43 "ip -4 addr show scope global | grep inet" 2>&1; echo ---; ipconfig | grep -A1 "IPv4" | head -8; echo ---; grep -n "RelayChoice\|relay" crates/spt-daemon/src/config.rs | head -10

Compare subnets + daemon relay default


    inet 192.168.1.114/24 brd 192.168.1.255 scope global dynamic noprefixroute wlo1
    inet 100.100.62.43/32 scope global tailscale0
---
   IPv4 Address. . . . . . . . . . . : 100.68.35.65
   Subnet Mask . . . . . . . . . . . : 255.255.255.255
--
   IPv4 Address. . . . . . . . . . . : 192.168.1.81
   Subnet Mask . . . . . . . . . . . : 255.255.255.0
---
44:/// Which relay stance the broker's net endpoint runs (ADR-0002 / REQ-NET-2) —
45:/// the `"relay"` knob in `daemon.json`. Maps onto `spt-net`'s `RelayPolicy` in
47:/// The self-hosted variant (`RelayPolicy::SelfHosted`) needs a relay-map URL
51:pub enum RelayChoice {
52:    /// n0's public relays — the zero-config default (`"relay": "n0"`).
54:    /// No relays: direct/LAN only (`"relay": "disabled"`).
72:    relay: Option<String>,
106:    /// The broker net endpoint's relay stance (D4a, REQ-NET-2). Default
107:    /// [`RelayChoice::N0`] — the ADR-0002 zero-config path. An unrecognized
110:    pub relay: RelayChoice,


Edit

            Some("no-seed-holder") => {
                eprintln!(
                    "NO_SEED_HOLDER:{name}: no member answered the rendezvous.\n\
                     - Is a member machine online (its spt daemon running)?\n\
                     - Same subnet name on both sides? (names are exact)\n\
                     - Are both clocks within ±30s? (the rendezvous derives from the time)"
                );
                return 1;
            }

            Some("no-seed-holder") => {
                eprintln!(
                    "NO_SEED_HOLDER:{name}: no member answered the rendezvous.\n\
                     - Is a member machine online (its spt daemon running)?\n\
                     - Same subnet name on both sides? (names are exact)\n\
                     - Are both clocks within ±30s? (the rendezvous derives from the time)\n\
                     detail: {}",
                    reply.detail
                );
                return 1;
            }


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)

            Some("no-seed-holder") => {
                eprintln!(
                    "NO_SEED_HOLDER:{name}: no member answered the rendezvous.\n\
                     - Is a member machine online (its spt daemon running)?\n\
                     - Same subnet name on both sides? (names are exact)\n\
                     - Are both clocks within ±30s? (the rendezvous derives from the time)"
                );
                return 1;
            }

            Some("no-seed-holder") => {
                eprintln!(
                    "NO_SEED_HOLDER:{name}: no member answered the rendezvous.\n\
                     - Is a member machine online (its spt daemon running)?\n\
                     - Same subnet name on both sides? (names are exact)\n\
                     - Are both clocks within ±30s? (the rendezvous derives from the time)\n\
                     detail: {}",
                    reply.detail
                );
                return 1;
            }

-                     - Are both clocks within ±30s? (the rendezvous derives from the time)"

+                     - Are both clocks within ±30s? (the rendezvous derives from the time)\n\


Bash

cargo build -p spt --release 2>&1 | tail -1 && git add -A && git commit -m "$(cat <<'EOF'
fix(cli): surface the rendezvous failure detail on NO_SEED_HOLDER

The guided join's no-seed-holder arm printed the actionable hints but
discarded reply.detail — the actual dial/resolution error chain (mDNS
miss vs relay lookup failure vs connect timeout), exactly what live
debugging of the first real two-machine join needs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EOF
)" && git push origin dev-freeform 2>&1 | tail -1

Build + commit + push diagnostic fix


    Finished `release` profile [optimized] target(s) in 7.57s
warning: in the working copy of 'crates/spt/src/cli.rs', LF will be replaced by CRLF the next time Git touches it
[dev-freeform b5e2485] fix(cli): surface the rendezvous failure detail on NO_SEED_HOLDER
 1 file changed, 3 insertions(+), 1 deletion(-)
   d6c9543..b5e2485  dev-freeform -> dev-freeform


ssh david@100.100.62.43 'cd ~/actions-runner/_work/spt-core/spt-core && git fetch origin dev-freeform && git checkout -q b5e2485 && export PATH=$HOME/.cargo/bin:$PATH && cargo build --release -p spt > /tmp/spt-b5.log 2>&1 && cp target/release/spt ~/.local/bin/spt && echo INSTALLED $(git log -1 --format=%h)' 2>&1

Rebuild joiner on gravity with detail surfacing


Command running in background with ID: b4ae3ykq8. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\b4ae3ykq8.output. You will be notified when it completes. To check interim output, use Read on that file path.


<task-notification>
<task-id>b4ae3ykq8</task-id>
<tool-use-id>toolu_017hJPTN2C7YD4aoQucMkbWY</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\b4ae3ykq8.output</output-file>
<status>failed</status>
<summary>Background command "Rebuild joiner on gravity with detail surfacing" failed with exit code 1</summary>
</task-notification>


Edit

| CLI nounification (M8 candidate; ratified shape from the 2026-06-06 grill):

| Linux elevation model (user-ratified direction 2026-06-06): (1) install symlinks the binary into a sudo-reachable path (e.g. `/usr/local/bin`) so `sudo spt` resolves; (2) first `sudo spt` detects elevation and prompts once for the DEFAULT USER ACCOUNT — thereafter any `sudo spt` daemon launch always runs the daemon (and state) under that account, never root (kills the `sudo HOME=$HOME` dance and the root-owned-state hazard) | M7 acceptance run (gravity: `sudo: spt: command not found`, then the HOME-universe split) | Needs installer + first-run-state design; Windows elevation has no equivalent problem (same user profile) | M8, with the installer's next touch |
| Clock source priority for TOTP/rendezvous: query a well-known world clock (NTP) FIRST, fall back to local time — machines with skewed local clocks currently derive wrong steps/rendezvous tokens silently | M7 acceptance grill (user 2026-06-06) | Needs an NTP client dep + offset caching + offline fallback semantics; ±1 window covers small skew today | First field report of clock-skew join failures, or M8 polish |
| CLI nounification (M8 candidate; ratified shape from the 2026-06-06 grill):