40	            IdentityError::BadLength { expected, got } => {
41	                write!(f, "bad length: expected {expected} bytes, got {got}")
42	            }
43	            IdentityError::BadPublicKey => write!(f, "invalid ed25519 public key"),
44	        }
45	    }
46	}
47
48	impl std::error::Error for IdentityError {}
49
50	/// A full keypair: signs on behalf of an endpoint/node. Holds secret key
51	/// material; never serialize the seed onto the wire — only [`PublicKey`] is
52	/// public-facing.
53	#[derive(Clone)]
54	pub struct Identity {
55	    signing: SigningKey,
56	}
57
58	impl Identity {
59	    /// Generate a fresh random identity from the OS CSPRNG.
60	    // [impl->REQ-NODE-IDENTITY]
61	    pub fn generate() -> Identity {
62	        Identity {
63	            signing: SigningKey::generate(&mut OsRng),
64	        }
65	    }
66
67	    /// Reconstruct an identity from its 32-byte secret seed (deterministic).
68	    // [impl->REQ-NODE-IDENTITY]
69	    pub fn from_seed(seed: &[u8; SEED_LEN]) -> Identity {
70	        Identity {
71	            signing: SigningKey::from_bytes(seed),
72	        }
73	    }
74
75	    /// The 32-byte secret seed. Treat as sensitive — for persistence only.
76	    pub fn seed(&self) -> [u8; SEED_LEN] {
77	        self.signing.to_bytes()
78	    }
79
80	    /// This identity's public key (its stable, shareable name).
81	    pub fn public_key(&self) -> PublicKey {
82	        PublicKey(self.signing.verifying_key())
83	    }
84
85	    /// Produce a detached 64-byte signature over `msg`.
86	    // [impl->REQ-NODE-IDENTITY]
87	    pub fn sign(&self, msg: &[u8]) -> [u8; SIGNATURE_LEN] {
88	        self.signing.sign(msg).to_bytes()
89	    }
90	}
91
92	impl std::fmt::Debug for Identity {
93	    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94	        // Never leak secret material in Debug output.
95	        f.debug_struct("Identity")
96	            .field("public_key", &self.public_key().to_hex())
97	            .finish_non_exhaustive()
98	    }
99	}
100
101	/// An Ed25519 public key — the shareable identity / node name.
102	#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103	pub struct PublicKey(VerifyingKey);
104
105	impl PublicKey {
106	    /// The 32 raw public-key bytes.
107	    pub fn to_bytes(&self) -> [u8; PUBLIC_KEY_LEN] {
108	        self.0.to_bytes()
109	    }

            IdentityError::BadLength { expected, got } => {
                write!(f, "bad length: expected {expected} bytes, got {got}")
            }
            IdentityError::BadPublicKey => write!(f, "invalid ed25519 public key"),
        }
    }
}

impl std::error::Error for IdentityError {}

/// A full keypair: signs on behalf of an endpoint/node. Holds secret key
/// material; never serialize the seed onto the wire — only [`PublicKey`] is
/// public-facing.
#[derive(Clone)]
pub struct Identity {
    signing: SigningKey,
}

impl Identity {
    /// Generate a fresh random identity from the OS CSPRNG.
    // [impl->REQ-NODE-IDENTITY]
    pub fn generate() -> Identity {
        Identity {
            signing: SigningKey::generate(&mut OsRng),
        }
    }

    /// Reconstruct an identity from its 32-byte secret seed (deterministic).
    // [impl->REQ-NODE-IDENTITY]
    pub fn from_seed(seed: &[u8; SEED_LEN]) -> Identity {
        Identity {
            signing: SigningKey::from_bytes(seed),
        }
    }

    /// The 32-byte secret seed. Treat as sensitive — for persistence only.
    pub fn seed(&self) -> [u8; SEED_LEN] {
        self.signing.to_bytes()
    }

    /// This identity's public key (its stable, shareable name).
    pub fn public_key(&self) -> PublicKey {
        PublicKey(self.signing.verifying_key())
    }

    /// Produce a detached 64-byte signature over `msg`.
    // [impl->REQ-NODE-IDENTITY]
    pub fn sign(&self, msg: &[u8]) -> [u8; SIGNATURE_LEN] {
        self.signing.sign(msg).to_bytes()
    }
}

impl std::fmt::Debug for Identity {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // Never leak secret material in Debug output.
        f.debug_struct("Identity")
            .field("public_key", &self.public_key().to_hex())
            .finish_non_exhaustive()
    }
}

/// An Ed25519 public key — the shareable identity / node name.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PublicKey(VerifyingKey);

impl PublicKey {
    /// The 32 raw public-key bytes.
    pub fn to_bytes(&self) -> [u8; PUBLIC_KEY_LEN] {
        self.0.to_bytes()
    }


Grep

pub mod release|pub use|IPC_PROTOCOL_VERSION


137:pub mod release;
154:pub use access::{access_check, classify_inbound, note_outbound, AccessDecision, InboundClass};
155:pub use attach::{request_attach, send_attach_input, serve_attach, AttachServeOutcome};
156:pub use adapter_update::{
160:pub use applyhost::{apply_staged, ApplyStagedOutcome};
161:pub use brain::{Brain, BrainState, BrokerEvent};
162:pub use broker::Broker;
163:pub use config::{DaemonConfig, RelayChoice};
164:pub use consent::{decide as decide_consent, most_recently_active, ConsentDecision, ConsentTarget};
165:pub use daemon::{ensure_running, is_running, Daemon};
166:pub use digest::{ingest_chunk, DigestEngine, DigestUpdate};
167:pub use digesthub::{
171:pub use effect::{EffectJournal, EffectKey, EffectKind, Outcome};
172:pub use endpoint::{broker_socket_name, daemon_pid_path, digest_socket_name, seed_socket_name};
173:pub use lifecycle::{BrainLifecycle, TickReport};
174:pub use linkhost::{
177:pub use nethost::{NetConfig, NetHost, NET_EFFECT_SESSION};
178:pub use notif::{
183:pub use notifsync::{apply_notif_feed, emit_notif_feed, NotifApplyVerdict, NotifPolicy};
184:pub use propagate::{request_update, serve_update, UpdatePullOutcome, UpdateServeOutcome};
185:pub use psyrelay::{
188:pub use relay::Relay;
189:pub use relcache::ReleaseCache;
190:pub use resting::{
195:pub use resthost::{request_rest, serve_rest, RestRequestOutcome, RestServeOutcome};

[Showing results with pagination = limit: 25]


crates\spt-daemon\src\applyhost.rs:241:            brain_ipc_version: crate::IPC_PROTOCOL_VERSION,
crates\spt-daemon\src\codec.rs:59:    use crate::frame::IPC_PROTOCOL_VERSION;
crates\spt-daemon\src\codec.rs:71:        assert_eq!(got.protocol_version, IPC_PROTOCOL_VERSION);
crates\spt-daemon\src\frame.rs:27:pub const IPC_PROTOCOL_VERSION: u32 = 1;
crates\spt-daemon\src\frame.rs:49:    /// The protocol version the sender speaks ([`IPC_PROTOCOL_VERSION`]).
crates\spt-daemon\src\frame.rs:56:    /// A `Hello` for this build, declaring [`IPC_PROTOCOL_VERSION`].

[Showing results with pagination = limit: 6]


Edit

/// The default channel pin when `release-keys.json` names none.
pub const DEFAULT_CHANNEL: &str = "stable";

/// The default channel pin when `release-keys.json` names none.
pub const DEFAULT_CHANNEL: &str = "stable";

/// The release trust anchor **compiled into every binary** (ADR-0015,
/// REQ-REL-3): two Ed25519 release keys — the active `primary` (signs every
/// release) and the never-used offline `recovery` (exists only as off-machine
/// seed backups; rotates in a fresh primary if the primary is ever lost).
/// `(key_id, verifying-key-hex)` pairs.
///
/// Filled at the v0.1.0 key ceremony (`cargo run -p xtask -- release-keygen`,
/// run by the maintainer; the seeds never touch this repo). Until then the
/// builtin set is empty and verification stays fail-closed exactly as before.
///
/// `release-keys.json` (identity dir) *overlays* this set: file keys add to
/// it, file revocations void builtin ids too — so a compromised builtin key
/// is still revocable per node without a rebuild.
// [impl->REQ-REL-3]
pub const BUILTIN_RELEASE_KEYS: &[(&str, &str)] = &[
    // ("rel-primary-2026", "<64-hex verifying key — minted at the ceremony>"),
    // ("rel-recovery-2026", "<64-hex verifying key — minted at the ceremony>"),
];


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

/// The default channel pin when `release-keys.json` names none.
pub const DEFAULT_CHANNEL: &str = "stable";

/// The release trust anchor **compiled into every binary** (ADR-0015,
/// REQ-REL-3): two Ed25519 release keys — the active `primary` (signs every
/// release) and the never-used offline `recovery` (exists only as off-machine
/// seed backups; rotates in a fresh primary if the primary is ever lost).
/// `(key_id, verifying-key-hex)` pairs.
///
/// Filled at the v0.1.0 key ceremony (`cargo run -p xtask -- release-keygen`,
/// run by the maintainer; the seeds never touch this repo). Until then the
/// builtin set is empty and verification stays fail-closed exactly as before.
///
/// `release-keys.json` (identity dir) *overlays* this set: file keys add to
/// it, file revocations void builtin ids too — so a compromised builtin key
/// is still revocable per node without a rebuild.
// [impl->REQ-REL-3]
pub const BUILTIN_RELEASE_KEYS: &[(&str, &str)] = &[
    // ("rel-primary-2026", "<64-hex verifying key — minted at the ceremony>"),
    // ("rel-recovery-2026", "<64-hex verifying key — minted at the ceremony>"),
];

+/// The release trust anchor **compiled into every binary** (ADR-0015,

+/// REQ-REL-3): two Ed25519 release keys — the active `primary` (signs every

+/// release) and the never-used offline `recovery` (exists only as off-machine

+/// seed backups; rotates in a fresh primary if the primary is ever lost).

+/// `(key_id, verifying-key-hex)` pairs.

+/// Filled at the v0.1.0 key ceremony (`cargo run -p xtask -- release-keygen`,

+/// run by the maintainer; the seeds never touch this repo). Until then the

+/// builtin set is empty and verification stays fail-closed exactly as before.

+/// `release-keys.json` (identity dir) *overlays* this set: file keys add to

+/// it, file revocations void builtin ids too — so a compromised builtin key

+/// is still revocable per node without a rebuild.

+pub const BUILTIN_RELEASE_KEYS: &[(&str, &str)] = &[

+    // ("rel-primary-2026", "<64-hex verifying key — minted at the ceremony>"),

+    // ("rel-recovery-2026", "<64-hex verifying key — minted at the ceremony>"),

/// On-disk trusted-release-keys config (`identity/release-keys.json`) — the

/// production [`VerifyPolicy`] source (D9-1). Human-editable JSON:

/// `{"keys": {"<key_id>": "<verifying-key-hex>"}, "revoked": ["<key_id>"],


    /// Load from an explicit keys file (tests; the pump E2E points each
    /// in-process "node" at its own). Unparseable key hex is skipped loud —
    /// a typo must not silently widen or narrow the trusted set unnoticed.
    // [impl->REQ-UPD-2]
    pub fn load_from(path: &std::path::Path, current_version: u64, now_ms: u64) -> VerifyPolicy {
        let file: ReleaseKeysFile = std::fs::read_to_string(path)
            .ok()
            .and_then(|raw| serde_json::from_str(&raw).ok())
            .unwrap_or_default();
        let mut trusted_keys = BTreeMap::new();
        for (key_id, hex) in &file.keys {
            match parse_verifying_key(hex) {
                Ok(vk) => {
                    trusted_keys.insert(key_id.clone(), vk);
                }
                Err(_) => eprintln!("RELEASE_KEY_BAD:{key_id} — skipped (not a valid key)"),
            }
        }
        VerifyPolicy {
            trusted_keys,
            revoked_keys: file.revoked,
            pinned_channel: file.channel.unwrap_or_else(|| DEFAULT_CHANNEL.to_string()),
            current_version,
            now_ms,
        }
    }
}

    /// Load from an explicit keys file (tests; the pump E2E points each
    /// in-process "node" at its own). Unparseable key hex is skipped loud —
    /// a typo must not silently widen or narrow the trusted set unnoticed.
    ///
    /// The policy starts from [`BUILTIN_RELEASE_KEYS`] (the ADR-0015 trust
    /// anchor every binary carries), then the file overlays it: file keys
    /// add (and may shadow a builtin id), file revocations void builtin ids
    /// too. A missing/malformed file leaves exactly the builtin set.
    // [impl->REQ-UPD-2]
    // [impl->REQ-REL-3]
    pub fn load_from(path: &std::path::Path, current_version: u64, now_ms: u64) -> VerifyPolicy {
        let file: ReleaseKeysFile = std::fs::read_to_string(path)
            .ok()
            .and_then(|raw| serde_json::from_str(&raw).ok())
            .unwrap_or_default();
        Self::assemble(BUILTIN_RELEASE_KEYS, &file, current_version, now_ms)
    }

    /// Merge mechanics, separated for testability with injected builtin
    /// pairs: builtin first, file overlays, revocations apply to both.
    fn assemble(
        builtin: &[(&str, &str)],
        file: &ReleaseKeysFile,
        current_version: u64,
        now_ms: u64,
    ) -> VerifyPolicy {
        let mut trusted_keys = BTreeMap::new();
        for (key_id, hex) in builtin {
            match parse_verifying_key(hex) {
                Ok(vk) => {
                    trusted_keys.insert(key_id.to_string(), vk);
                }
                // A bad builtin entry is a build defect — loud, never silent.
                Err(_) => eprintln!("RELEASE_KEY_BAD:builtin:{key_id} — skipped (not a valid key)"),
            }
        }
        for (key_id, hex) in &file.keys {
            match parse_verifying_key(hex) {
                Ok(vk) => {
                    trusted_keys.insert(key_id.clone(), vk);
                }
                Err(_) => eprintln!("RELEASE_KEY_BAD:{key_id} — skipped (not a valid key)"),
            }
        }
        VerifyPolicy {
            trusted_keys,
            revoked_keys: file.revoked.clone(),
            pinned_channel: file
                .channel
                .clone()
                .unwrap_or_else(|| DEFAULT_CHANNEL.to_string()),
            current_version,
            now_ms,
        }
    }
}


    /// Load from an explicit keys file (tests; the pump E2E points each
    /// in-process "node" at its own). Unparseable key hex is skipped loud —
    /// a typo must not silently widen or narrow the trusted set unnoticed.
    // [impl->REQ-UPD-2]
    pub fn load_from(path: &std::path::Path, current_version: u64, now_ms: u64) -> VerifyPolicy {
        let file: ReleaseKeysFile = std::fs::read_to_string(path)
            .ok()
            .and_then(|raw| serde_json::from_str(&raw).ok())
            .unwrap_or_default();
        let mut trusted_keys = BTreeMap::new();
        for (key_id, hex) in &file.keys {
            match parse_verifying_key(hex) {
                Ok(vk) => {
                    trusted_keys.insert(key_id.clone(), vk);
                }
                Err(_) => eprintln!("RELEASE_KEY_BAD:{key_id} — skipped (not a valid key)"),
            }
        }
        VerifyPolicy {
            trusted_keys,
            revoked_keys: file.revoked,
            pinned_channel: file.channel.unwrap_or_else(|| DEFAULT_CHANNEL.to_string()),
            current_version,
            now_ms,
        }
    }
}

    /// Load from an explicit keys file (tests; the pump E2E points each
    /// in-process "node" at its own). Unparseable key hex is skipped loud —
    /// a typo must not silently widen or narrow the trusted set unnoticed.
    ///
    /// The policy starts from [`BUILTIN_RELEASE_KEYS`] (the ADR-0015 trust
    /// anchor every binary carries), then the file overlays it: file keys
    /// add (and may shadow a builtin id), file revocations void builtin ids
    /// too. A missing/malformed file leaves exactly the builtin set.
    // [impl->REQ-UPD-2]
    // [impl->REQ-REL-3]
    pub fn load_from(path: &std::path::Path, current_version: u64, now_ms: u64) -> VerifyPolicy {
        let file: ReleaseKeysFile = std::fs::read_to_string(path)
            .ok()
            .and_then(|raw| serde_json::from_str(&raw).ok())
            .unwrap_or_default();
        Self::assemble(BUILTIN_RELEASE_KEYS, &file, current_version, now_ms)
    }

    /// Merge mechanics, separated for testability with injected builtin
    /// pairs: builtin first, file overlays, revocations apply to both.
    fn assemble(
        builtin: &[(&str, &str)],
        file: &ReleaseKeysFile,
        current_version: u64,
        now_ms: u64,
    ) -> VerifyPolicy {
        let mut trusted_keys = BTreeMap::new();
        for (key_id, hex) in builtin {
            match parse_verifying_key(hex) {
                Ok(vk) => {
                    trusted_keys.insert(key_id.to_string(), vk);
                }
                // A bad builtin entry is a build defect — loud, never silent.
                Err(_) => eprintln!("RELEASE_KEY_BAD:builtin:{key_id} — skipped (not a valid key)"),
            }
        }
        for (key_id, hex) in &file.keys {
            match parse_verifying_key(hex) {
                Ok(vk) => {
                    trusted_keys.insert(key_id.clone(), vk);
                }
                Err(_) => eprintln!("RELEASE_KEY_BAD:{key_id} — skipped (not a valid key)"),
            }
        }
        VerifyPolicy {
            trusted_keys,
            revoked_keys: file.revoked.clone(),
            pinned_channel: file
                .channel
                .clone()
                .unwrap_or_else(|| DEFAULT_CHANNEL.to_string()),
            current_version,
            now_ms,
        }
    }
}

+    /// The policy starts from [`BUILTIN_RELEASE_KEYS`] (the ADR-0015 trust

+    /// anchor every binary carries), then the file overlays it: file keys

+    /// add (and may shadow a builtin id), file revocations void builtin ids

+    /// too. A missing/malformed file leaves exactly the builtin set.

+        Self::assemble(BUILTIN_RELEASE_KEYS, &file, current_version, now_ms)

+    /// Merge mechanics, separated for testability with injected builtin

+    /// pairs: builtin first, file overlays, revocations apply to both.

+            match parse_verifying_key(hex) {

+                    trusted_keys.insert(key_id.to_string(), vk);

+                // A bad builtin entry is a build defect — loud, never silent.

+                Err(_) => eprintln!("RELEASE_KEY_BAD:builtin:{key_id} — skipped (not a valid key)"),

-            revoked_keys: file.revoked,

-            pinned_channel: file.channel.unwrap_or_else(|| DEFAULT_CHANNEL.to_string()),

+            revoked_keys: file.revoked.clone(),

+                .unwrap_or_else(|| DEFAULT_CHANNEL.to_string()),


M6-D1: spt-releases repo + public face (README, licenses)

Create SaberMage/spt-releases private via gh; author README.md per grill decision 7; LICENSE-MIT + LICENSE-BINARY; Pages config deferred to D7 flip. Activate REQ-REL-1 doc stage.

Creating spt-releases repo + public face

M6-D2: installer scripts (install.sh / install.ps1)

Non-interactive: latest-release lookup → platform asset fetch → sha256 verify → per-OS install root → user PATH → print absolute path. Idempotent. CI rung against staged artifact + spt --version. Activate REQ-INSTALL-1/2/3/5.

M6-D3: manifest JSON Schema (schemars) + drift gate

schemars derives on spt-runtime manifest types; emit manifest.schema.json at stable site-root path + release asset; CI regenerate+diff gate. Activate REQ-DOCS-4 (partial), REQ-DOCS-5.

M6-D4: docs-site Tier 1 content (mdBook)

mdBook skeleton + Starlight-inspired theme; mental model; human quickstart; adapter quickstart (mock-adapter); harness-contract vertical complete; shell getting-started (spt-shell-notify); stubs for remaining verticals. Activate REQ-DOCS-1/2/3.

M6-D5: agent-export + generation plumbing

llms.txt + llms-full.txt; raw .md alongside HTML; CLI ref from clap; rustdoc → Pages; source-repo CI publish job → spt-releases Pages branch; drift gates. REQ-REL-1 impl, REQ-DOCS-4/5 complete.

M6-D6: release pipeline + signing ceremony

Workspace → 0.1.0; tag-triggered build matrix both runners; cross-repo release via fine-grained token; key ceremony per ADR-0015 (primary + recovery, embed both pubkeys in VerifyPolicy trusted set, seed backups); manual SignedRelease signing. Activate REQ-REL-2/3.

M6-D7: v0.1.0 ship + flip public + acceptance run

Tag v0.1.0; ceremony; publish release; flip spt-releases public; Pages live; acceptance proof per M6-PLAN (clean-machine install, quickstarts, llms.txt + schema fetch, staged v0.1.1 verify, recovery-key verify).


Unit tests for the merge + two-key semantics:


    // ---- M6-D6: the embedded two-key trust anchor (ADR-0015) ----

    /// Builtin pairs merge under a file overlay: builtin keys trusted with no
    /// file at all; file keys add; file revocations void builtin ids too
    /// (compromise recovery without a rebuild).
    // [unit->REQ-REL-3]
    #[test]
    fn builtin_keys_merge_and_revoke_under_file_overlay() {
        let primary = signing_key(31);
        let recovery = signing_key(32);
        let p_hex = hex_encode(primary.verifying_key().as_bytes());
        let r_hex = hex_encode(recovery.verifying_key().as_bytes());
        let builtin: Vec<(&str, &str)> =
            vec![("rel-primary", p_hex.as_str()), ("rel-recovery", r_hex.as_str())];

        // No file: exactly the builtin set, default channel.
        let pol = VerifyPolicy::assemble(&builtin, &ReleaseKeysFile::default(), 0, 0);
        assert_eq!(pol.trusted_keys.len(), 2);
        assert!(pol.trusted_keys.contains_key("rel-primary"));
        assert!(pol.trusted_keys.contains_key("rel-recovery"));

        // File overlay: adds a key AND revokes the builtin primary.
        let extra = signing_key(33);
        let file = ReleaseKeysFile {
            keys: BTreeMap::from([(
                "node-extra".to_string(),
                hex_encode(extra.verifying_key().as_bytes()),
            )]),
            revoked: BTreeSet::from(["rel-primary".to_string()]),
            channel: None,
        };
        let pol = VerifyPolicy::assemble(&builtin, &file, 5, 1_000);
        assert_eq!(pol.trusted_keys.len(), 3, "file key added to the builtin set");

        // The revoked BUILTIN primary is rejected even with a valid signature…
        let mut meta = meta_for(6, b"bin");
        meta.key_id = "rel-primary".to_string();
        let signed = sign_release(&primary, &meta);
        let mut pol2 = pol.clone();
        pol2.pinned_channel = CHANNEL.to_string();
        assert_eq!(
            verify_metadata(&signed, &pol2),
            Err(RejectReason::RevokedKey("rel-primary".to_string()))
        );

        // …while the same release signed by the RECOVERY key verifies: the
        // ADR-0015 loss/compromise story — the second already-trusted key
        // keeps the update chain alive.
        let mut meta_r = meta_for(6, b"bin");
        meta_r.key_id = "rel-recovery".to_string();
        let signed_r = sign_release(&recovery, &meta_r);
        let got = verify_metadata(&signed_r, &pol2).expect("recovery-signed release verifies");
        assert_eq!(got.key_id, "rel-recovery");
    }

    /// The shipped builtin table itself parses clean: every entry must be a
    /// valid (id, 64-hex Ed25519 key) pair — a typo at the ceremony fails the
    /// build's tests, not a node in the field.
    // [unit->REQ-REL-3]
    #[test]
    fn shipped_builtin_table_is_well_formed() {
        for (key_id, hex) in BUILTIN_RELEASE_KEYS {
            parse_verifying_key(hex)
                .unwrap_or_else(|e| panic!("builtin release key {key_id:?} invalid: {e}"));
            assert!(!key_id.trim().is_empty());
        }
    }

    #[test]
    fn hex_roundtrips() {


    // ---- M6-D6: the embedded two-key trust anchor (ADR-0015) ----

    /// Builtin pairs merge under a file overlay: builtin keys trusted with no
    /// file at all; file keys add; file revocations void builtin ids too
    /// (compromise recovery without a rebuild).
    // [unit->REQ-REL-3]
    #[test]
    fn builtin_keys_merge_and_revoke_under_file_overlay() {
        let primary = signing_key(31);
        let recovery = signing_key(32);
        let p_hex = hex_encode(primary.verifying_key().as_bytes());
        let r_hex = hex_encode(recovery.verifying_key().as_bytes());
        let builtin: Vec<(&str, &str)> =
            vec![("rel-primary", p_hex.as_str()), ("rel-recovery", r_hex.as_str())];

        // No file: exactly the builtin set, default channel.
        let pol = VerifyPolicy::assemble(&builtin, &ReleaseKeysFile::default(), 0, 0);
        assert_eq!(pol.trusted_keys.len(), 2);
        assert!(pol.trusted_keys.contains_key("rel-primary"));
        assert!(pol.trusted_keys.contains_key("rel-recovery"));

        // File overlay: adds a key AND revokes the builtin primary.
        let extra = signing_key(33);
        let file = ReleaseKeysFile {
            keys: BTreeMap::from([(
                "node-extra".to_string(),
                hex_encode(extra.verifying_key().as_bytes()),
            )]),
            revoked: BTreeSet::from(["rel-primary".to_string()]),
            channel: None,
        };
        let pol = VerifyPolicy::assemble(&builtin, &file, 5, 1_000);
        assert_eq!(pol.trusted_keys.len(), 3, "file key added to the builtin set");

        // The revoked BUILTIN primary is rejected even with a valid signature…
        let mut meta = meta_for(6, b"bin");
        meta.key_id = "rel-primary".to_string();
        let signed = sign_release(&primary, &meta);
        let mut pol2 = pol.clone();
        pol2.pinned_channel = CHANNEL.to_string();
        assert_eq!(
            verify_metadata(&signed, &pol2),
            Err(RejectReason::RevokedKey("rel-primary".to_string()))
        );

        // …while the same release signed by the RECOVERY key verifies: the
        // ADR-0015 loss/compromise story — the second already-trusted key
        // keeps the update chain alive.
        let mut meta_r = meta_for(6, b"bin");
        meta_r.key_id = "rel-recovery".to_string();
        let signed_r = sign_release(&recovery, &meta_r);
        let got = verify_metadata(&signed_r, &pol2).expect("recovery-signed release verifies");
        assert_eq!(got.key_id, "rel-recovery");
    }

    /// The shipped builtin table itself parses clean: every entry must be a
    /// valid (id, 64-hex Ed25519 key) pair — a typo at the ceremony fails the
    /// build's tests, not a node in the field.
    // [unit->REQ-REL-3]
    #[test]
    fn shipped_builtin_table_is_well_formed() {
        for (key_id, hex) in BUILTIN_RELEASE_KEYS {
            parse_verifying_key(hex)
                .unwrap_or_else(|e| panic!("builtin release key {key_id:?} invalid: {e}"));
            assert!(!key_id.trim().is_empty());
        }
    }

    #[test]
    fn hex_roundtrips() {

+    // ---- M6-D6: the embedded two-key trust anchor (ADR-0015) ----

+    /// Builtin pairs merge under a file overlay: builtin keys trusted with no

+    /// file at all; file keys add; file revocations void builtin ids too

+    /// (compromise recovery without a rebuild).

+    fn builtin_keys_merge_and_revoke_under_file_overlay() {

+        let recovery = signing_key(32);

+        let p_hex = hex_encode(primary.verifying_key().as_bytes());

+        let r_hex = hex_encode(recovery.verifying_key().as_bytes());

+        let builtin: Vec<(&str, &str)> =

+            vec![("rel-primary", p_hex.as_str()), ("rel-recovery", r_hex.as_str())];

+        // No file: exactly the builtin set, default channel.

+        let pol = VerifyPolicy::assemble(&builtin, &ReleaseKeysFile::default(), 0, 0);

+        assert_eq!(pol.trusted_keys.len(), 2);

+        assert!(pol.trusted_keys.contains_key("rel-primary"));

+        assert!(pol.trusted_keys.contains_key("rel-recovery"));

+        // File overlay: adds a key AND revokes the builtin primary.

+                "node-extra".to_string(),

+                hex_encode(extra.verifying_key().as_bytes()),

+            revoked: BTreeSet::from(["rel-primary".to_string()]),

+        let pol = VerifyPolicy::assemble(&builtin, &file, 5, 1_000);

+        assert_eq!(pol.trusted_keys.len(), 3, "file key added to the builtin set");

+        // The revoked BUILTIN primary is rejected even with a valid signature…

+        let mut meta = meta_for(6, b"bin");

+        meta.key_id = "rel-primary".to_string();

+        let signed = sign_release(&primary, &meta);

+        pol2.pinned_channel = CHANNEL.to_string();

+            verify_metadata(&signed, &pol2),

+            Err(RejectReason::RevokedKey("rel-primary".to_string()))

+        // …while the same release signed by the RECOVERY key verifies: the

+        // ADR-0015 loss/compromise story — the second already-trusted key

+        // keeps the update chain alive.

+        let mut meta_r = meta_for(6, b"bin");

+        meta_r.key_id = "rel-recovery".to_string();

+        let signed_r = sign_release(&recovery, &meta_r);

+        let got = verify_metadata(&signed_r, &pol2).expect("recovery-signed release verifies");

+        assert_eq!(got.key_id, "rel-recovery");

+    /// The shipped builtin table itself parses clean: every entry must be a

+    /// valid (id, 64-hex Ed25519 key) pair — a typo at the ceremony fails the

+    /// build's tests, not a node in the field.

+    fn shipped_builtin_table_is_well_formed() {

+        for (key_id, hex) in BUILTIN_RELEASE_KEYS {

+                .unwrap_or_else(|e| panic!("builtin release key {key_id:?} invalid: {e}"));

+            assert!(!key_id.trim().is_empty());


Now the ceremony + signing tooling in xtask:


[dependencies]
# M6-D6 release tooling: keygen reuses the spt-proto identity primitive
# (REQ-NODE-IDENTITY — never a second Ed25519 implementation); release-sign
# emits the exact SignedRelease/ReleaseMetadata shapes the self-updater
# verifies (spt-daemon::release), with the IPC/ABI constants read from code.
spt-proto = { path = "../spt-proto" }
spt-daemon = { path = "../spt-daemon" }
serde_json = "1"


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

[dependencies]
# M6-D6 release tooling: keygen reuses the spt-proto identity primitive
# (REQ-NODE-IDENTITY — never a second Ed25519 implementation); release-sign
# emits the exact SignedRelease/ReleaseMetadata shapes the self-updater
# verifies (spt-daemon::release), with the IPC/ABI constants read from code.
spt-proto = { path = "../spt-proto" }
spt-daemon = { path = "../spt-daemon" }
serde_json = "1"

[package]
name = "xtask"
version = "0.0.0"
edition = "2021"
publish = false
description = "spt-core docs/site generation tasks (M6-D5): CLI reference from clap help, llms.txt link gate, llms-full.txt concatenation, publish-tree assembly. Run via `cargo run -p xtask -- <cmd>`."

[dependencies]

+# M6-D6 release tooling: keygen reuses the spt-proto identity primitive

+# (REQ-NODE-IDENTITY — never a second Ed25519 implementation); release-sign

+# emits the exact SignedRelease/ReleaseMetadata shapes the self-updater

+# verifies (spt-daemon::release), with the IPC/ABI constants read from code.

+spt-daemon = { path = "../spt-daemon" }


fn main() {
    let args: Vec<String> = std::env::args().skip(1).collect();
    let cmd = args.first().map(String::as_str).unwrap_or("");
    match cmd {
        "gen" => gen(false),
        "check" => {
            gen(true);
            check_llms_links();
            eprintln!("xtask check: OK");
        }
        "site" => {
            let out = args.get(1).expect("usage: xtask site <outdir>");
            site(Path::new(out));
        }
        _ => {
            eprintln!("usage: xtask <gen|check|site <outdir>>");
            std::process::exit(2);
        }
    }
}

fn main() {
    let args: Vec<String> = std::env::args().skip(1).collect();
    let cmd = args.first().map(String::as_str).unwrap_or("");
    match cmd {
        "gen" => gen(false),
        "check" => {
            gen(true);
            check_llms_links();
            eprintln!("xtask check: OK");
        }
        "site" => {
            let out = args.get(1).expect("usage: xtask site <outdir>");
            site(Path::new(out));
        }
        "release-keygen" => release_keygen(&args[1..]),
        "release-sign" => release_sign(&args[1..]),
        _ => {
            eprintln!(
                "usage: xtask <gen|check|site <outdir>|release-keygen <key-id>|release-sign …>"
            );
            std::process::exit(2);
        }
    }
}


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)

fn main() {
    let args: Vec<String> = std::env::args().skip(1).collect();
    let cmd = args.first().map(String::as_str).unwrap_or("");
    match cmd {
        "gen" => gen(false),
        "check" => {
            gen(true);
            check_llms_links();
            eprintln!("xtask check: OK");
        }
        "site" => {
            let out = args.get(1).expect("usage: xtask site <outdir>");
            site(Path::new(out));
        }
        _ => {
            eprintln!("usage: xtask <gen|check|site <outdir>>");
            std::process::exit(2);
        }
    }
}

fn main() {
    let args: Vec<String> = std::env::args().skip(1).collect();
    let cmd = args.first().map(String::as_str).unwrap_or("");
    match cmd {
        "gen" => gen(false),
        "check" => {
            gen(true);
            check_llms_links();
            eprintln!("xtask check: OK");
        }
        "site" => {
            let out = args.get(1).expect("usage: xtask site <outdir>");
            site(Path::new(out));
        }
        "release-keygen" => release_keygen(&args[1..]),
        "release-sign" => release_sign(&args[1..]),
        _ => {
            eprintln!(
                "usage: xtask <gen|check|site <outdir>|release-keygen <key-id>|release-sign …>"
            );
            std::process::exit(2);
        }
    }
}

+        "release-keygen" => release_keygen(&args[1..]),

+        "release-sign" => release_sign(&args[1..]),

-            eprintln!("usage: xtask <gen|check|site <outdir>>");

+                "usage: xtask <gen|check|site <outdir>|release-keygen <key-id>|release-sign …>"


fn copy_tree_all(from: &Path, to: &Path) {

// ------------------------------------------------------------ release (D6)

/// The key ceremony's minting half (ADR-0015): generate one Ed25519 release
/// keypair and print it to stdout — NOTHING is written to disk. The
/// maintainer runs this twice (primary + recovery), stores both seeds
/// off-machine (password manager + paper/QR, physically separate), embeds
/// both PUBLIC keys in `BUILTIN_RELEASE_KEYS` (spt-daemon::release), and
/// discards the terminal scrollback.
// [impl->REQ-REL-3]
fn release_keygen(args: &[String]) {
    let key_id = args.first().expect("usage: xtask release-keygen <key-id>");
    let id = spt_proto::identity::Identity::generate();
    let seed_hex: String = id.seed().iter().map(|b| format!("{b:02x}")).collect();
    println!("key_id:     {key_id}");
    println!("public_hex: {}", id.public_key().to_hex());
    println!("seed_hex:   {seed_hex}");
    println!();
    println!("1. Store seed_hex in your password manager (and a paper/QR backup,");
    println!("   physically separate). The seed never touches a repo or CI.");
    println!("2. Add (\"{key_id}\", \"<public_hex>\") to BUILTIN_RELEASE_KEYS in");
    println!("   crates/spt-daemon/src/release.rs and rebuild.");
    println!("3. Clear this terminal's scrollback.");
}

/// The manual local signing step (ADR-0015 — no CI secrets): produce the
/// exact `SignedRelease` JSON the self-updater consumes, one per platform
/// artifact. The signing seed comes from the SPT_RELEASE_SEED env var
/// (paste from the password manager into the shell session, never argv).
///
/// `xtask release-sign --key-id <id> --version <u64> [--expires-days <n>]
///  <artifact>…` writes `<artifact>.release.json` beside each artifact.
// [impl->REQ-REL-2]
// [impl->REQ-REL-3]
fn release_sign(args: &[String]) {
    let mut key_id = None;
    let mut version = None;
    let mut expires_days: u64 = 365;
    let mut artifacts: Vec<PathBuf> = Vec::new();
    let mut it = args.iter();
    while let Some(a) = it.next() {
        match a.as_str() {
            "--key-id" => key_id = it.next().cloned(),
            "--version" => version = it.next().and_then(|v| v.parse::<u64>().ok()),
            "--expires-days" => {
                expires_days = it.next().and_then(|v| v.parse().ok()).expect("--expires-days <n>")
            }
            other => artifacts.push(PathBuf::from(other)),
        }
    }
    let key_id = key_id.expect("--key-id <id> (must match a BUILTIN_RELEASE_KEYS id)");
    let version = version.expect("--version <u64> (the monotonic release counter)");
    assert!(!artifacts.is_empty(), "at least one artifact to sign");

    let seed_hex = std::env::var("SPT_RELEASE_SEED")
        .expect("SPT_RELEASE_SEED env var (the signing seed, from the password manager)");
    let seed: Vec<u8> = (0..seed_hex.len())
        .step_by(2)
        .map(|i| u8::from_str_radix(&seed_hex[i..i + 2], 16).expect("seed is hex"))
        .collect();
    let seed: [u8; 32] = seed.as_slice().try_into().expect("seed is 32 bytes");
    let identity = spt_proto::identity::Identity::from_seed(&seed);
    println!("signing as: {} (key_id {key_id})", identity.public_key().to_hex());

    let now_ms = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_millis() as u64;
    for artifact in &artifacts {
        let bytes = std::fs::read(artifact).expect("read artifact");
        let meta = spt_daemon::release::ReleaseMetadata {
            version,
            channel: spt_daemon::release::DEFAULT_CHANNEL.to_string(),
            expires_at_ms: now_ms + expires_days * 24 * 60 * 60 * 1000,
            key_id: key_id.clone(),
            artifact_sha256: spt_daemon::release::sha256_hex(&bytes),
            brain_ipc_version: spt_daemon::IPC_PROTOCOL_VERSION,
            broker_resource_abi: spt_daemon::update::BROKER_RESOURCE_ABI,
        };
        let metadata_json = serde_json::to_string(&meta).expect("serialize metadata");
        let sig = identity.sign(metadata_json.as_bytes());
        let sig_hex: String = sig.iter().map(|b| format!("{b:02x}")).collect();
        let signed = spt_daemon::release::SignedRelease { metadata_json, signature_hex: sig_hex };
        let out = artifact.with_extension(format!(
            "{}release.json",
            artifact
                .extension()
                .map(|e| format!("{}.", e.to_string_lossy()))
                .unwrap_or_default()
        ));
        std::fs::write(&out, serde_json::to_string_pretty(&signed).unwrap()).expect("write");
        println!(
            "signed {} (sha256 {}) -> {}",
            artifact.display(),
            meta.artifact_sha256,
            out.display()
        );
    }
}

fn copy_tree_all(from: &Path, to: &Path) {


// ------------------------------------------------------------ release (D6)

/// The key ceremony's minting half (ADR-0015): generate one Ed25519 release
/// keypair and print it to stdout — NOTHING is written to disk. The
/// maintainer runs this twice (primary + recovery), stores both seeds
/// off-machine (password manager + paper/QR, physically separate), embeds
/// both PUBLIC keys in `BUILTIN_RELEASE_KEYS` (spt-daemon::release), and
/// discards the terminal scrollback.
// [impl->REQ-REL-3]
fn release_keygen(args: &[String]) {
    let key_id = args.first().expect("usage: xtask release-keygen <key-id>");
    let id = spt_proto::identity::Identity::generate();
    let seed_hex: String = id.seed().iter().map(|b| format!("{b:02x}")).collect();
    println!("key_id:     {key_id}");
    println!("public_hex: {}", id.public_key().to_hex());
    println!("seed_hex:   {seed_hex}");
    println!();
    println!("1. Store seed_hex in your password manager (and a paper/QR backup,");
    println!("   physically separate). The seed never touches a repo or CI.");
    println!("2. Add (\"{key_id}\", \"<public_hex>\") to BUILTIN_RELEASE_KEYS in");
    println!("   crates/spt-daemon/src/release.rs and rebuild.");
    println!("3. Clear this terminal's scrollback.");
}

/// The manual local signing step (ADR-0015 — no CI secrets): produce the
/// exact `SignedRelease` JSON the self-updater consumes, one per platform
/// artifact. The signing seed comes from the SPT_RELEASE_SEED env var
/// (paste from the password manager into the shell session, never argv).
///
/// `xtask release-sign --key-id <id> --version <u64> [--expires-days <n>]
///  <artifact>…` writes `<artifact>.release.json` beside each artifact.
// [impl->REQ-REL-2]
// [impl->REQ-REL-3]
fn release_sign(args: &[String]) {
    let mut key_id = None;
    let mut version = None;
    let mut expires_days: u64 = 365;
    let mut artifacts: Vec<PathBuf> = Vec::new();
    let mut it = args.iter();
    while let Some(a) = it.next() {
        match a.as_str() {
            "--key-id" => key_id = it.next().cloned(),
            "--version" => version = it.next().and_then(|v| v.parse::<u64>().ok()),
            "--expires-days" => {
                expires_days = it.next().and_then(|v| v.parse().ok()).expect("--expires-days <n>")
            }
            other => artifacts.push(PathBuf::from(other)),
        }
    }
    let key_id = key_id.expect("--key-id <id> (must match a BUILTIN_RELEASE_KEYS id)");
    let version = version.expect("--version <u64> (the monotonic release counter)");
    assert!(!artifacts.is_empty(), "at least one artifact to sign");

    let seed_hex = std::env::var("SPT_RELEASE_SEED")
        .expect("SPT_RELEASE_SEED env var (the signing seed, from the password manager)");
    let seed: Vec<u8> = (0..seed_hex.len())
        .step_by(2)
        .map(|i| u8::from_str_radix(&seed_hex[i..i + 2], 16).expect("seed is hex"))
        .collect();
    let seed: [u8; 32] = seed.as_slice().try_into().expect("seed is 32 bytes");
    let identity = spt_proto::identity::Identity::from_seed(&seed);
    println!("signing as: {} (key_id {key_id})", identity.public_key().to_hex());

    let now_ms = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_millis() as u64;
    for artifact in &artifacts {
        let bytes = std::fs::read(artifact).expect("read artifact");
        let meta = spt_daemon::release::ReleaseMetadata {
            version,
            channel: spt_daemon::release::DEFAULT_CHANNEL.to_string(),
            expires_at_ms: now_ms + expires_days * 24 * 60 * 60 * 1000,
            key_id: key_id.clone(),
            artifact_sha256: spt_daemon::release::sha256_hex(&bytes),
            brain_ipc_version: spt_daemon::IPC_PROTOCOL_VERSION,
            broker_resource_abi: spt_daemon::update::BROKER_RESOURCE_ABI,
        };
        let metadata_json = serde_json::to_string(&meta).expect("serialize metadata");
        let sig = identity.sign(metadata_json.as_bytes());
        let sig_hex: String = sig.iter().map(|b| format!("{b:02x}")).collect();
        let signed = spt_daemon::release::SignedRelease { metadata_json, signature_hex: sig_hex };
        let out = artifact.with_extension(format!(
            "{}release.json",
            artifact
                .extension()
                .map(|e| format!("{}.", e.to_string_lossy()))
                .unwrap_or_default()
        ));
        std::fs::write(&out, serde_json::to_string_pretty(&signed).unwrap()).expect("write");
        println!(
            "signed {} (sha256 {}) -> {}",
            artifact.display(),
            meta.artifact_sha256,
            out.display()
        );
    }
}

fn copy_tree_all(from: &Path, to: &Path) {

eprintln!("xtask site: publish tree assembled at {}", out.display());

+/// The key ceremony's minting half (ADR-0015): generate one Ed25519 release

+/// keypair and print it to stdout — NOTHING is written to disk. The

+/// maintainer runs this twice (primary + recovery), stores both seeds

+/// off-machine (password manager + paper/QR, physically separate), embeds

+/// both PUBLIC keys in `BUILTIN_RELEASE_KEYS` (spt-daemon::release), and

+    let key_id = args.first().expect("usage: xtask release-keygen <key-id>");

+    let id = spt_proto::identity::Identity::generate();

+    let seed_hex: String = id.seed().iter().map(|b| format!("{b:02x}")).collect();

+    println!("public_hex: {}", id.public_key().to_hex());

+    println!("seed_hex:   {seed_hex}");

+    println!("1. Store seed_hex in your password manager (and a paper/QR backup,");

+    println!("   physically separate). The seed never touches a repo or CI.");

+    println!("2. Add (\"{key_id}\", \"<public_hex>\") to BUILTIN_RELEASE_KEYS in");

+    println!("   crates/spt-daemon/src/release.rs and rebuild.");

+    println!("3. Clear this terminal's scrollback.");

+/// The manual local signing step (ADR-0015 — no CI secrets): produce the

+/// exact `SignedRelease` JSON the self-updater consumes, one per platform

+/// artifact. The signing seed comes from the SPT_RELEASE_SEED env var

+/// (paste from the password manager into the shell session, never argv).

+/// `xtask release-sign --key-id <id> --version <u64> [--expires-days <n>]

+///  <artifact>…` writes `<artifact>.release.json` beside each artifact.

+    let mut artifacts: Vec<PathBuf> = Vec::new();

+            "--key-id" => key_id = it.next().cloned(),

+            "--version" => version = it.next().and_then(|v| v.parse::<u64>().ok()),

+                expires_days = it.next().and_then(|v| v.parse().ok()).expect("--expires-days <n>")

+            other => artifacts.push(PathBuf::from(other)),

+    let key_id = key_id.expect("--key-id <id> (must match a BUILTIN_RELEASE_KEYS id)");

+    let version = version.expect("--version <u64> (the monotonic release counter)");

+    assert!(!artifacts.is_empty(), "at least one artifact to sign");

+    let seed_hex = std::env::var("SPT_RELEASE_SEED")

+        .expect("SPT_RELEASE_SEED env var (the signing seed, from the password manager)");

+    let seed: Vec<u8> = (0..seed_hex.len())

+        .map(|i| u8::from_str_radix(&seed_hex[i..i + 2], 16).expect("seed is hex"))

+    let seed: [u8; 32] = seed.as_slice().try_into().expect("seed is 32 bytes");

+    let identity = spt_proto::identity::Identity::from_seed(&seed);

+    println!("signing as: {} (key_id {key_id})", identity.public_key().to_hex());

+    let now_ms = std::time::SystemTime::now()

+        .duration_since(std::time::UNIX_EPOCH)

+        let bytes = std::fs::read(artifact).expect("read artifact");

+        let meta = spt_daemon::release::ReleaseMetadata {

+            channel: spt_daemon::release::DEFAULT_CHANNEL.to_string(),

+            expires_at_ms: now_ms + expires_days * 24 * 60 * 60 * 1000,

+            artifact_sha256: spt_daemon::release::sha256_hex(&bytes),

+            brain_ipc_version: spt_daemon::IPC_PROTOCOL_VERSION,

+            broker_resource_abi: spt_daemon::update::BROKER_RESOURCE_ABI,

+        let metadata_json = serde_json::to_string(&meta).expect("serialize metadata");

+        let sig = identity.sign(metadata_json.as_bytes());

+        let sig_hex: String = sig.iter().map(|b| format!("{b:02x}")).collect();

+        let signed = spt_daemon::release::SignedRelease { metadata_json, signature_hex: sig_hex };

+        let out = artifact.with_extension(format!(

+                .map(|e| format!("{}.", e.to_string_lossy()))

+        std::fs::write(&out, serde_json::to_string_pretty(&signed).unwrap()).expect("write");

for entry in std::fs::read_dir(from).expect("read dir") {


PowerShell

cd C:\Users\decid\Documents\projects\spt-core; Select-String -Path crates\spt-daemon\src\lib.rs -Pattern "IPC_PROTOCOL_VERSION|BROKER_RESOURCE_ABI|pub mod update|pub mod frame"

Check spt-daemon exports


crates\spt-daemon\src\lib.rs:121:pub mod frame;
crates\spt-daemon\src\lib.rs:147:pub mod update;
crates\spt-daemon\src\lib.rs:198:    accept_hello, Envelope, HandshakeError, Hello, Role, IPC_PROTOCOL_VERSION,
crates\spt-daemon\src\lib.rs:208:    UpdateClass, UpdatePlan, BROKER_RESOURCE_ABI,


cd C:\Users\decid\Documents\projects\spt-core; cargo build -q -p xtask 2>&1 | Select-Object -First 10; "build=$LASTEXITCODE"; cargo test -q -p spt-daemon release 2>&1 | Select-String "test result|FAILED" | Select-Object -First 4

Build xtask + run release tests


build=0

test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 161 filtered out; finished in 0.03s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 3 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s


Trust anchor green. Workspace → 0.1.0 (single shared version):


cd C:\Users\decid\Documents\projects\spt-core; Select-String -Path crates\*\Cargo.toml,adapters\mock\Cargo.toml -Pattern '^version' | %{ "$($_.Path): $($_.Line)" }

Current member versions


C:\Users\decid\Documents\projects\spt-core\crates\spt\Cargo.toml: version = "0.0.0"
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\Cargo.toml: version = "0.0.0"
C:\Users\decid\Documents\projects\spt-core\crates\spt-live\Cargo.toml: version = "0.0.0"
C:\Users\decid\Documents\projects\spt-core\crates\spt-msg\Cargo.toml: version = "0.0.0"
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\Cargo.toml: version = "0.0.0"
C:\Users\decid\Documents\projects\spt-core\crates\spt-proto\Cargo.toml: version = "0.0.0"
C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\Cargo.toml: version = "0.0.0"
C:\Users\decid\Documents\projects\spt-core\crates\spt-store\Cargo.toml: version = "0.0.0"
C:\Users\decid\Documents\projects\spt-core\crates\spt-term\Cargo.toml: version = "0.0.0"
C:\Users\decid\Documents\projects\spt-core\crates\xtask\Cargo.toml: version = "0.0.0"
C:\Users\decid\Documents\projects\spt-core\adapters\mock\Cargo.toml: version = "0.0.0"


cd C:\Users\decid\Documents\projects\spt-core; foreach ($f in (Get-ChildItem crates\*\Cargo.toml) + (Get-Item adapters\mock\Cargo.toml)) { (Get-Content $f.FullName -Raw) -replace '(?m)^version = "0\.0\.0"', 'version.workspace = true' | Set-Content $f.FullName -NoNewline }; Select-String -Path crates\*\Cargo.toml,adapters\mock\Cargo.toml -Pattern '^version' | %{ $_.Line } | Select-Object -Unique

Switch members to workspace version inheritance


version.workspace = true


Edit

[workspace]
resolver = "2"

# M6-D6 (grill decision 9): ONE shared version for every crate and the binary
# — the product version (v0.1.0 tag, `spt --version`). The wire-protocol
# version stays its own counter (REQ-ARCH-3) and the SignedRelease `version`
# is the separate monotonic release counter; neither conflates with this.
[workspace.package]
version = "0.1.0"


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

[workspace]
resolver = "2"

# M6-D6 (grill decision 9): ONE shared version for every crate and the binary
# — the product version (v0.1.0 tag, `spt --version`). The wire-protocol
# version stays its own counter (REQ-ARCH-3) and the SignedRelease `version`
# is the separate monotonic release counter; neither conflates with this.
[workspace.package]
version = "0.1.0"

# spt-core cargo workspace (M0). Acyclically-layered crates per R-ARCH-1:
#   spt-proto -> spt-store -> spt-msg -> {spt-net, spt-term, spt-runtime} -> spt-live -> spt-daemon -> spt
# M0 builds the bottom two: spt-proto (wire) + spt-store (persistence).
# M1 adds spt-msg (local messaging) — third layer, one-way above spt-store.
# M2a adds spt-runtime (harness contract: manifest schema + AgentRuntime) — fourth layer.
# M2b adds spt-live (live-agent lifecycle executor) — fifth layer, daemon-bound (not public SDK).
# M3a adds spt-term (session-surface mechanism: ConPTY/forkpty PTY layer) — sibling of spt-runtime, above spt-msg.
# M3b adds spt-daemon (broker/brain supervisor) — seventh layer, above spt-live, below spt; internal (not public SDK).
# M4 adds spt-net (P2P WAN/LAN transport) — sibling of spt-term/spt-runtime, above spt-msg; internal, default-on `net` feature.
[workspace]
resolver = "2"
members = ["crates/spt-proto", "crates/spt-store", "crates/spt-msg", "crates/spt-net", "crates/spt-term", "crates/spt-runtime", "crates/spt-live", "crates/spt-daemon", "crates/spt", "adapters/mock", "crates/xtask"]

+# M6-D6 (grill decision 9): ONE shared version for every crate and the binary

+# — the product version (v0.1.0 tag, `spt --version`). The wire-protocol

+# version stays its own counter (REQ-ARCH-3) and the SignedRelease `version`

+# is the separate monotonic release counter; neither conflates with this.


1	[package]
2	name = "spt"
3	version.workspace = true
4	edition = "2021"
5	publish = false
6	description = "spt-core CLI: the single `spt` binary. Thin command layer over spt-msg (send/ring/ready/poll/list/stop/whoami) + the harness-contract `api` surface (spt-runtime)."
7
8	[[bin]]
9	name = "spt"
10	path = "src/main.rs"
11


1	[package]
2	name = "spt-runtime"
3	version.workspace = true
4	edition = "2021"
5	publish = false
6	description = "spt-core harness contract: runtime-manifest schema + AgentRuntime/ManifestRuntime (opaque-template substitution + spawn-session seam). The outbound half of the harness-agnostic contract (M2a). Public SDK (R-ARCH-2)."
7
8	[dependencies]
9	spt-proto = { path = "../spt-proto" }
10	spt-store = { path = "../spt-store" }
11	spt-msg = { path = "../spt-msg" }


1	[package]
2	name = "xtask"
3	version.workspace = true
4	edition = "2021"
5	publish = false
6	description = "spt-core docs/site generation tasks (M6-D5): CLI reference from clap help, llms.txt link gate, llms-full.txt concatenation, publish-tree assembly. Run via `cargo run -p xtask -- <cmd>`."
7
8	[dependencies]
9	# M6-D6 release tooling: keygen reuses the spt-proto identity primitive
10	# (REQ-NODE-IDENTITY — never a second Ed25519 implementation); release-sign
11	# emits the exact SignedRelease/ReleaseMetadata shapes the self-updater


PowerShell

cd C:\Users\decid\Documents\projects\spt-core; cargo build -q -p spt --bin spt 2>&1 | Select-Object -First 4; .\target\debug\spt.exe --version

Build + verify version reports 0.1.0


error: package ID specification `spt` did not match any packages
spt 0.0.0


cd C:\Users\decid\Documents\projects\spt-core; cargo build -p spt 2>&1 | Select-Object -First 12

See workspace parse error


Error: Exit code 101
warning: C:\Users\decid\Documents\projects\spt-core\Cargo.toml: unused manifest key: workspace.package.members
error: package ID specification `spt` did not match any packages


1	# spt-core cargo workspace (M0). Acyclically-layered crates per R-ARCH-1:
2	#   spt-proto -> spt-store -> spt-msg -> {spt-net, spt-term, spt-runtime} -> spt-live -> spt-daemon -> spt
3	# M0 builds the bottom two: spt-proto (wire) + spt-store (persistence).
4	# M1 adds spt-msg (local messaging) — third layer, one-way above spt-store.
5	# M2a adds spt-runtime (harness contract: manifest schema + AgentRuntime) — fourth layer.
6	# M2b adds spt-live (live-agent lifecycle executor) — fifth layer, daemon-bound (not public SDK).
7	# M3a adds spt-term (session-surface mechanism: ConPTY/forkpty PTY layer) — sibling of spt-runtime, above spt-msg.
8	# M3b adds spt-daemon (broker/brain supervisor) — seventh layer, above spt-live, below spt; internal (not public SDK).
9	# M4 adds spt-net (P2P WAN/LAN transport) — sibling of spt-term/spt-runtime, above spt-msg; internal, default-on `net` feature.
10	[workspace]
11	resolver = "2"
12
13	# M6-D6 (grill decision 9): ONE shared version for every crate and the binary
14	# — the product version (v0.1.0 tag, `spt --version`). The wire-protocol
15	# version stays its own counter (REQ-ARCH-3) and the SignedRelease `version`
16	# is the separate monotonic release counter; neither conflates with this.
17	[workspace.package]
18	version = "0.1.0"
19	members = ["crates/spt-proto", "crates/spt-store", "crates/spt-msg", "crates/spt-net", "crates/spt-term", "crates/spt-runtime", "crates/spt-live", "crates/spt-daemon", "crates/spt", "adapters/mock", "crates/xtask"]
20

# spt-core cargo workspace (M0). Acyclically-layered crates per R-ARCH-1:
#   spt-proto -> spt-store -> spt-msg -> {spt-net, spt-term, spt-runtime} -> spt-live -> spt-daemon -> spt
# M0 builds the bottom two: spt-proto (wire) + spt-store (persistence).
# M1 adds spt-msg (local messaging) — third layer, one-way above spt-store.
# M2a adds spt-runtime (harness contract: manifest schema + AgentRuntime) — fourth layer.
# M2b adds spt-live (live-agent lifecycle executor) — fifth layer, daemon-bound (not public SDK).
# M3a adds spt-term (session-surface mechanism: ConPTY/forkpty PTY layer) — sibling of spt-runtime, above spt-msg.
# M3b adds spt-daemon (broker/brain supervisor) — seventh layer, above spt-live, below spt; internal (not public SDK).
# M4 adds spt-net (P2P WAN/LAN transport) — sibling of spt-term/spt-runtime, above spt-msg; internal, default-on `net` feature.
[workspace]
resolver = "2"

# M6-D6 (grill decision 9): ONE shared version for every crate and the binary
# — the product version (v0.1.0 tag, `spt --version`). The wire-protocol
# version stays its own counter (REQ-ARCH-3) and the SignedRelease `version`
# is the separate monotonic release counter; neither conflates with this.
[workspace.package]
version = "0.1.0"
members = ["crates/spt-proto", "crates/spt-store", "crates/spt-msg", "crates/spt-net", "crates/spt-term", "crates/spt-runtime", "crates/spt-live", "crates/spt-daemon", "crates/spt", "adapters/mock", "crates/xtask"]


Write

# spt-core cargo workspace (M0). Acyclically-layered crates per R-ARCH-1:
#   spt-proto -> spt-store -> spt-msg -> {spt-net, spt-term, spt-runtime} -> spt-live -> spt-daemon -> spt
# M0 builds the bottom two: spt-proto (wire) + spt-store (persistence).
# M1 adds spt-msg (local messaging) — third layer, one-way above spt-store.
# M2a adds spt-runtime (harness contract: manifest schema + AgentRuntime) — fourth layer.
# M2b adds spt-live (live-agent lifecycle executor) — fifth layer, daemon-bound (not public SDK).
# M3a adds spt-term (session-surface mechanism: ConPTY/forkpty PTY layer) — sibling of spt-runtime, above spt-msg.
# M3b adds spt-daemon (broker/brain supervisor) — seventh layer, above spt-live, below spt; internal (not public SDK).
# M4 adds spt-net (P2P WAN/LAN transport) — sibling of spt-term/spt-runtime, above spt-msg; internal, default-on `net` feature.
[workspace]
resolver = "2"
members = ["crates/spt-proto", "crates/spt-store", "crates/spt-msg", "crates/spt-net", "crates/spt-term", "crates/spt-runtime", "crates/spt-live", "crates/spt-daemon", "crates/spt", "adapters/mock", "crates/xtask"]

# M6-D6 (grill decision 9): ONE shared version for every crate and the binary
# — the product version (v0.1.0 tag, `spt --version`). The wire-protocol
# version stays its own counter (REQ-ARCH-3) and the SignedRelease `version`
# is the separate monotonic release counter; neither conflates with this.
[workspace.package]
version = "0.1.0"


# spt-core cargo workspace (M0). Acyclically-layered crates per R-ARCH-1:
#   spt-proto -> spt-store -> spt-msg -> {spt-net, spt-term, spt-runtime} -> spt-live -> spt-daemon -> spt
# M0 builds the bottom two: spt-proto (wire) + spt-store (persistence).
# M1 adds spt-msg (local messaging) — third layer, one-way above spt-store.
# M2a adds spt-runtime (harness contract: manifest schema + AgentRuntime) — fourth layer.
# M2b adds spt-live (live-agent lifecycle executor) — fifth layer, daemon-bound (not public SDK).
# M3a adds spt-term (session-surface mechanism: ConPTY/forkpty PTY layer) — sibling of spt-runtime, above spt-msg.
# M3b adds spt-daemon (broker/brain supervisor) — seventh layer, above spt-live, below spt; internal (not public SDK).
# M4 adds spt-net (P2P WAN/LAN transport) — sibling of spt-term/spt-runtime, above spt-msg; internal, default-on `net` feature.
[workspace]
resolver = "2"
members = ["crates/spt-proto", "crates/spt-store", "crates/spt-msg", "crates/spt-net", "crates/spt-term", "crates/spt-runtime", "crates/spt-live", "crates/spt-daemon", "crates/spt", "adapters/mock", "crates/xtask"]

# M6-D6 (grill decision 9): ONE shared version for every crate and the binary
# — the product version (v0.1.0 tag, `spt --version`). The wire-protocol
# version stays its own counter (REQ-ARCH-3) and the SignedRelease `version`
# is the separate monotonic release counter; neither conflates with this.
[workspace.package]
version = "0.1.0"

+members = ["crates/spt-proto", "crates/spt-store", "crates/spt-msg", "crates/spt-net", "crates/spt-term", "crates/spt-runtime", "crates/spt-live", "crates/spt-daemon", "crates/spt", "adapters/mock", "crates/xtask"]

-members = ["crates/spt-proto", "crates/spt-store", "crates/spt-msg", "crates/spt-net", "crates/spt-term", "crates/spt-runtime", "crates/spt-live", "crates/spt-daemon", "crates/spt", "adapters/mock", "crates/xtask"]

# spt-core cargo workspace (M0). Acyclically-layered crates per R-ARCH-1:
#   spt-proto -> spt-store -> spt-msg -> {spt-net, spt-term, spt-runtime} -> spt-live -> spt-daemon -> spt
# M0 builds the bottom two: spt-proto (wire) + spt-store (persistence).
# M1 adds spt-msg (local messaging) — third layer, one-way above spt-store.
# M2a adds spt-runtime (harness contract: manifest schema + AgentRuntime) — fourth layer.
# M2b adds spt-live (live-agent lifecycle executor) — fifth layer, daemon-bound (not public SDK).
# M3a adds spt-term (session-surface mechanism: ConPTY/forkpty PTY layer) — sibling of spt-runtime, above spt-msg.
# M3b adds spt-daemon (broker/brain supervisor) — seventh layer, above spt-live, below spt; internal (not public SDK).
# M4 adds spt-net (P2P WAN/LAN transport) — sibling of spt-term/spt-runtime, above spt-msg; internal, default-on `net` feature.
[workspace]
resolver = "2"

# M6-D6 (grill decision 9): ONE shared version for every crate and the binary
# — the product version (v0.1.0 tag, `spt --version`). The wire-protocol
# version stays its own counter (REQ-ARCH-3) and the SignedRelease `version`
# is the separate monotonic release counter; neither conflates with this.
[workspace.package]
version = "0.1.0"
members = ["crates/spt-proto", "crates/spt-store", "crates/spt-msg", "crates/spt-net", "crates/spt-term", "crates/spt-runtime", "crates/spt-live", "crates/spt-daemon", "crates/spt", "adapters/mock", "crates/xtask"]


PowerShell

cd C:\Users\decid\Documents\projects\spt-core; cargo build -q -p spt --bin spt 2>&1 | Select-Object -First 4; .\target\debug\spt.exe --version

Rebuild + version check


spt 0.1.0


`spt 0.1.0` — matches the docs. Now the release workflow:


Write

# M6-D6: the tag-triggered release pipeline (grill decision 9, ADR-0015).
#
# CI's half of the ceremony: build the platform binaries on both self-hosted
# runners, assemble the asset set the self-updater + installer consume
# (binaries, SHA256SUMS, manifest.schema.json, mock-adapter.zip), and create
# a DRAFT release on SaberMage/spt-releases (cross-repo via the
# RELEASES_TOKEN fine-grained PAT).
#
# The maintainer's half stays manual and local (no signing keys in CI):
#   1. download the draft's binaries (or use the local builds)
#   2. SPT_RELEASE_SEED=<seed> cargo run -p xtask -- release-sign \
#        --key-id rel-primary-2026 --version <N> spt-x86_64-* …
#   3. gh release upload <tag> *.release.json --repo SaberMage/spt-releases
#   4. publish the draft
#
# [impl->REQ-REL-2]
name: release

on:
  push:
    tags: ["v*"]

jobs:
  build:
    strategy:
      fail-fast: false
      matrix:
        include:
          - runner: [self-hosted, Linux, gravity]
            asset: spt-x86_64-linux
            built: target/release/spt
          - runner: [self-hosted, Windows, hfenduleam]
            asset: spt-x86_64-windows.exe
            built: target/release/spt.exe
    runs-on: ${{ matrix.runner }}
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v5
      - name: Build release binary
        run: cargo build --release -p spt --bin spt
      - name: Stage asset
        shell: bash
        run: |
          mkdir -p dist
          cp "${{ matrix.built }}" "dist/${{ matrix.asset }}"
      - uses: actions/upload-artifact@v4
        with:
          name: ${{ matrix.asset }}
          path: dist/${{ matrix.asset }}
          if-no-files-found: error

  assemble:
    needs: build
    runs-on: [self-hosted, Linux, gravity]
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v5
      - uses: actions/download-artifact@v4
        with:
          path: dist
          merge-multiple: true
      - name: Assemble asset set
        # The exact set REQ-REL-2 names: platform binaries + SHA256SUMS (what
        # install.sh/.ps1 verify) + the manifest schema + the mock-adapter
        # source zip (the adapter quickstart's reference download).
        run: |
          cd dist
          sha256sum spt-x86_64-linux spt-x86_64-windows.exe > SHA256SUMS
          cp ../crates/spt-runtime/manifest.schema.json .
          (cd .. && zip -r dist/mock-adapter.zip adapters/mock \
             -x 'adapters/mock/target/*')
          ls -la
      - name: Create draft release on spt-releases
        env:
          GH_TOKEN: ${{ secrets.RELEASES_TOKEN }}
        run: |
          gh release create "${GITHUB_REF_NAME}" \
            --repo SaberMage/spt-releases \
            --draft \
            --title "spt ${GITHUB_REF_NAME}" \
            --notes "spt-core ${GITHUB_REF_NAME}. Binaries are sha256-bound by SHA256SUMS; the SignedRelease metadata (*.release.json, uploaded at the manual signing step) is what \`spt update\` verifies against the embedded two-key trust anchor." \
            dist/spt-x86_64-linux \
            dist/spt-x86_64-windows.exe \
            dist/SHA256SUMS \
            dist/manifest.schema.json \
            dist/mock-adapter.zip


File created successfully at: C:\Users\decid\Documents\projects\spt-core\.github\workflows\release.yml (file state is current in your context — no need to Read it back)

# M6-D6: the tag-triggered release pipeline (grill decision 9, ADR-0015).
#
# CI's half of the ceremony: build the platform binaries on both self-hosted
# runners, assemble the asset set the self-updater + installer consume
# (binaries, SHA256SUMS, manifest.schema.json, mock-adapter.zip), and create
# a DRAFT release on SaberMage/spt-releases (cross-repo via the
# RELEASES_TOKEN fine-grained PAT).
#
# The maintainer's half stays manual and local (no signing keys in CI):
#   1. download the draft's binaries (or use the local builds)
#   2. SPT_RELEASE_SEED=<seed> cargo run -p xtask -- release-sign \
#        --key-id rel-primary-2026 --version <N> spt-x86_64-* …
#   3. gh release upload <tag> *.release.json --repo SaberMage/spt-releases
#   4. publish the draft
#
# [impl->REQ-REL-2]
name: release

on:
  push:
    tags: ["v*"]

jobs:
  build:
    strategy:
      fail-fast: false
      matrix:
        include:
          - runner: [self-hosted, Linux, gravity]
            asset: spt-x86_64-linux
            built: target/release/spt
          - runner: [self-hosted, Windows, hfenduleam]
            asset: spt-x86_64-windows.exe
            built: target/release/spt.exe
    runs-on: ${{ matrix.runner }}
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v5
      - name: Build release binary
        run: cargo build --release -p spt --bin spt
      - name: Stage asset
        shell: bash
        run: |
          mkdir -p dist
          cp "${{ matrix.built }}" "dist/${{ matrix.asset }}"
      - uses: actions/upload-artifact@v4
        with:
          name: ${{ matrix.asset }}
          path: dist/${{ matrix.asset }}
          if-no-files-found: error

  assemble:
    needs: build
    runs-on: [self-hosted, Linux, gravity]
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v5
      - uses: actions/download-artifact@v4
        with:
          path: dist
          merge-multiple: true
      - name: Assemble asset set
        # The exact set REQ-REL-2 names: platform binaries + SHA256SUMS (what
        # install.sh/.ps1 verify) + the manifest schema + the mock-adapter
        # source zip (the adapter quickstart's reference download).
        run: |
          cd dist
          sha256sum spt-x86_64-linux spt-x86_64-windows.exe > SHA256SUMS
          cp ../crates/spt-runtime/manifest.schema.json .
          (cd .. && zip -r dist/mock-adapter.zip adapters/mock \
             -x 'adapters/mock/target/*')
          ls -la
      - name: Create draft release on spt-releases
        env:
          GH_TOKEN: ${{ secrets.RELEASES_TOKEN }}
        run: |
          gh release create "${GITHUB_REF_NAME}" \
            --repo SaberMage/spt-releases \
            --draft \
            --title "spt ${GITHUB_REF_NAME}" \
            --notes "spt-core ${GITHUB_REF_NAME}. Binaries are sha256-bound by SHA256SUMS; the SignedRelease metadata (*.release.json, uploaded at the manual signing step) is what \`spt update\` verifies against the embedded two-key trust anchor." \
            dist/spt-x86_64-linux \
            dist/spt-x86_64-windows.exe \
            dist/SHA256SUMS \
            dist/manifest.schema.json \
            dist/mock-adapter.zip


Edit

id = "REQ-REL-2"
title = "Release asset set consumable by the self-updater: platform binaries, SHA256SUMS, SignedRelease metadata, manifest schema, mock-adapter zip; tag-triggered cross-repo pipeline"
required_stages = []  # activates at M6 start (D6/D7)

id = "REQ-REL-2"
title = "Release asset set consumable by the self-updater: platform binaries, SHA256SUMS, SignedRelease metadata, manifest schema, mock-adapter zip; tag-triggered cross-repo pipeline"
required_stages = ["impl"]  # impl activated M6-D6: release.yml — tag-triggered (v*), build matrix on BOTH self-hosted runners, assemble job emits the exact REQ-named asset set and creates a DRAFT cross-repo release on spt-releases via RELEASES_TOKEN; the SignedRelease assets join at the manual local signing step (xtask release-sign emits the exact spt-daemon::release::SignedRelease shape, fields from code constants). int (the D7 acceptance: staged release VERIFIES against the embedded trusted set) activates at D7


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

id = "REQ-REL-2"
title = "Release asset set consumable by the self-updater: platform binaries, SHA256SUMS, SignedRelease metadata, manifest schema, mock-adapter zip; tag-triggered cross-repo pipeline"
required_stages = []  # activates at M6 start (D6/D7)

id = "REQ-REL-2"
title = "Release asset set consumable by the self-updater: platform binaries, SHA256SUMS, SignedRelease metadata, manifest schema, mock-adapter zip; tag-triggered cross-repo pipeline"
required_stages = ["impl"]  # impl activated M6-D6: release.yml — tag-triggered (v*), build matrix on BOTH self-hosted runners, assemble job emits the exact REQ-named asset set and creates a DRAFT cross-repo release on spt-releases via RELEASES_TOKEN; the SignedRelease assets join at the manual local signing step (xtask release-sign emits the exact spt-daemon::release::SignedRelease shape, fields from code constants). int (the D7 acceptance: staged release VERIFIES against the embedded trusted set) activates at D7

-required_stages = []  # activates at M6 start (D6/D7)

+required_stages = ["impl"]  # impl activated M6-D6: release.yml — tag-triggered (v*), build matrix on BOTH self-hosted runners, assemble job emits the exact REQ-named asset set and creates a DRAFT cross-repo release on spt-releases via RELEASES_TOKEN; the SignedRelease assets join at the manual local signing step (xtask release-sign emits the exact spt-daemon::release::SignedRelease shape, fields from code constants). int (the D7 acceptance: staged release VERIFIES against the embedded trusted set) activates at D7

title = "Two-key release-signing trust anchor: primary + offline never-used recovery, both pubkeys embedded in the binary's trusted set, manual local signing (ADR-0015)"


id = "REQ-REL-3"
title = "Two-key release-signing trust anchor: primary + offline never-used recovery, both pubkeys embedded in the binary's trusted set, manual local signing (ADR-0015)"
required_stages = []  # activates at M6 start (D6)

id = "REQ-REL-3"
title = "Two-key release-signing trust anchor: primary + offline never-used recovery, both pubkeys embedded in the binary's trusted set, manual local signing (ADR-0015)"
required_stages = ["impl", "unit"]  # activated M6-D6: BUILTIN_RELEASE_KEYS compiled into every binary (release.rs) with release-keys.json as a pure OVERLAY (file adds/shadows keys; file revocations void builtin ids — per-node compromise recovery without a rebuild; empty builtin set stays fail-closed); ceremony tooling = xtask release-keygen (mints via the spt-proto identity primitive, prints to stdout only, seeds never touch disk/repo/CI) + release-sign (manual local signing off SPT_RELEASE_SEED env). unit = merge/overlay/revoke-builtin + recovery-key-signed-release-verifies + shipped-table-well-formed. The ceremony itself (mint 2 keypairs, embed pubkeys, back up seeds) = maintainer action before the v0.1.0 tag


id = "REQ-REL-3"
title = "Two-key release-signing trust anchor: primary + offline never-used recovery, both pubkeys embedded in the binary's trusted set, manual local signing (ADR-0015)"
required_stages = []  # activates at M6 start (D6)

id = "REQ-REL-3"
title = "Two-key release-signing trust anchor: primary + offline never-used recovery, both pubkeys embedded in the binary's trusted set, manual local signing (ADR-0015)"
required_stages = ["impl", "unit"]  # activated M6-D6: BUILTIN_RELEASE_KEYS compiled into every binary (release.rs) with release-keys.json as a pure OVERLAY (file adds/shadows keys; file revocations void builtin ids — per-node compromise recovery without a rebuild; empty builtin set stays fail-closed); ceremony tooling = xtask release-keygen (mints via the spt-proto identity primitive, prints to stdout only, seeds never touch disk/repo/CI) + release-sign (manual local signing off SPT_RELEASE_SEED env). unit = merge/overlay/revoke-builtin + recovery-key-signed-release-verifies + shipped-table-well-formed. The ceremony itself (mint 2 keypairs, embed pubkeys, back up seeds) = maintainer action before the v0.1.0 tag

-required_stages = []  # activates at M6 start (D6)

+required_stages = ["impl", "unit"]  # activated M6-D6: BUILTIN_RELEASE_KEYS compiled into every binary (release.rs) with release-keys.json as a pure OVERLAY (file adds/shadows keys; file revocations void builtin ids — per-node compromise recovery without a rebuild; empty builtin set stays fail-closed); ceremony tooling = xtask release-keygen (mints via the spt-proto identity primitive, prints to stdout only, seeds never touch disk/repo/CI) + release-sign (manual local signing off SPT_RELEASE_SEED env). unit = merge/overlay/revoke-builtin + recovery-key-signed-release-verifies + shipped-table-well-formed. The ceremony itself (mint 2 keypairs, embed pubkeys, back up seeds) = maintainer action before the v0.1.0 tag

# ───────────────────────────── Documentation ────────────────────────────────


PowerShell

cd C:\Users\decid\Documents\projects\spt-core; cargo build --workspace --all-targets -q 2>&1 | Select-Object -First 4; "build=$LASTEXITCODE"; cargo clippy --workspace --all-targets -- -D warnings 2>&1 | Select-Object -Last 2; cargo test --workspace -q 2>&1 | Select-String "test result: FAILED|error" | Select-Object -First 4; traceable-reqs check > $null 2>&1; "check=$LASTEXITCODE"

Full gates: build + clippy + tests + traceability


build=0
    Checking xtask v0.1.0 (C:\Users\decid\Documents\projects\spt-core\crates\xtask)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 15.04s
check=0


All gates green. Committing D6:


cd C:\Users\decid\Documents\projects\spt-core; git add -A; git commit -m @'
feat(release): D6 — release pipeline, two-key trust anchor, signing tooling

ADR-0015 in code: BUILTIN_RELEASE_KEYS compiled into every binary as the
release trust anchor; identity/release-keys.json becomes a pure overlay
(file keys add/shadow; file revocations void builtin ids too — per-node
compromise recovery without a rebuild). Empty until the key ceremony, so
verification stays fail-closed exactly as before. Unit-proven: builtin
merge/overlay/revocation + a recovery-key-signed release verifying against
the same anchor (the ADR-0015 loss story) + shipped-table well-formedness
(a ceremony typo fails tests, not field nodes).

Ceremony tooling (xtask): release-keygen mints a keypair via the spt-proto
identity primitive and prints to stdout only (seeds never touch disk, repo,
or CI); release-sign is the manual local signing step — SPT_RELEASE_SEED
env, emits per-artifact SignedRelease JSON in the exact shape the
self-updater consumes (IPC/ABI constants read from code, never typed).

release.yml: tag-triggered (v*) build matrix on both self-hosted runners →
assemble the REQ-REL-2 asset set (platform binaries, SHA256SUMS,
manifest.schema.json, mock-adapter.zip) → DRAFT cross-repo release on
spt-releases via RELEASES_TOKEN. Signing stays out of CI by design.

Workspace → single shared 0.1.0 ([workspace.package]; spt --version now
reports the documented "spt 0.1.0"). REQ-REL-2 impl, REQ-REL-3 impl+unit
activated. 139/139 green; full sweep green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'@ | Select-Object -Last 2; git push 2>&1 | Select-Object -Last 1

Commit + push D6


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 'Cargo.toml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'adapters/mock/Cargo.toml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/Cargo.toml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/src/release.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-live/Cargo.toml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-msg/Cargo.toml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-net/Cargo.toml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-proto/Cargo.toml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-runtime/Cargo.toml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-store/Cargo.toml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-term/Cargo.toml', 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/xtask/Cargo.toml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/xtask/src/main.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of '.github/workflows/release.yml', LF will be replaced by CRLF the next time Git touches it
 17 files changed, 374 insertions(+), 50 deletions(-)
 create mode 100644 .github/workflows/release.yml
   992547d..3f0904d  dev-freeform -> dev-freeform


Updated task #6 status


cd C:\Users\decid\Documents\projects\spt-core; gh run list --branch dev-freeform --limit 2

CI status for D5/D6 pushes


queued		feat(release): D6 — release pipeline, two-key trust anchor, signing t…	ci	dev-freeform	push	27035838393	7s	2026-06-05T19:32:04Z
completed	success	feat(docs): D5 — agent-export + generation plumbing (xtask, publish p…	ci	dev-freeform	push	27035318083	7m12s	2026-06-05T19:20:51Z


Write

<live-context>
Generation 22 — **M6 D1–D6 EXECUTED in one session (2026-06-05); paused at the D7 ship gate (user actions required).**

## timeline
- D1 `e430327`: SaberMage/spt-releases created (private) + README (grill decision 7) + LICENSE-MIT/LICENSE-BINARY (royalty-free adapter clause) pushed (spt-releases@2debda8). REQ-REL-1 doc.
- D2 `2ab8bad`+`f8e0f8a`: installer/install.sh+.ps1 (non-interactive, sha256, user PATH, env knobs, ASSET_BASE CI/air-gap override); oneliner_e2e.rs staged-release rung (named so because *install*-named exes trip Windows UAC heuristic, os error 740); .gitattributes pins LF. REQ-INSTALL-1/2/3/5 activated.
- **Side quest**: traceable-reqs v0.1.2 shipped (BigscreenVR/traceable-reqs@aaf234d) — shell/PowerShell `#` tag scanning (REQ-SCAN-SHELL-PS, full doc/impl/unit/int); released + installed locally; spt-core ci.yml now source-pins it self-healing on gravity (build in $RUNNER_TEMP — in-checkout clone gets adopted by the workspace). Scan roots grew: installer/, CONTEXT.md (file root), docs-site/, .github/.
- D3 `be4e46c`: manifest_json_schema() from the SAME schemars derives that parse manifests; checked-in crates/spt-runtime/manifest.schema.json; drift test (SPT_BLESS=1 regen). REQ-DOCS-4 doc/impl/unit, REQ-DOCS-5 impl/int.
- D4: docs-site/ mdBook corpus (Starlight-styled CSS), quickstarts with REAL captured outputs, complete harness-contract vertical, shell getting-started via spt-shell-notify, vertical overviews; quickstart_e2e.rs runs the published messaging flow step-for-step (anti-rot). REQ-DOCS-1 doc, -2 doc+int, -3 doc.
- D5 `992547d`: crates/xtask — gen (CLI ref from --help, platform-normalized), check (CLI-ref drift + llms link gate, in CI both OSes), site (full Pages payload: HTML + raw .md + llms.txt/llms-full + schema + scripts + rustdoc); docs-publish.yml (tag/dispatch → force-push gh-pages on spt-releases). REQ-DOCS-1 impl, REQ-REL-1 impl.
- D6 `3f0904d`: BUILTIN_RELEASE_KEYS embedded trust anchor (empty until ceremony; release-keys.json = overlay, file revocations void builtin ids); xtask release-keygen/release-sign (manual local signing, SPT_RELEASE_SEED env); release.yml (tag → both runners → draft cross-repo release with binaries/SHA256SUMS/schema/mock-adapter.zip); workspace → single 0.1.0 ([workspace.package]). REQ-REL-2 impl, REQ-REL-3 impl+unit.
- 139/139 traceable-reqs green throughout; full sweep + clippy green; D5 CI run green, D6 run in flight.

## D7 ship gate — USER actions before I can proceed
1. Key ceremony: `cargo run -p xtask -- release-keygen rel-primary-2026` (and rel-recovery-2026); seeds → password manager + paper, separate; hand me the two public_hex to embed in BUILTIN_RELEASE_KEYS.
2. RELEASES_TOKEN: fine-grained PAT (Contents r/w on SaberMage/spt-releases only) → `gh secret set RELEASES_TOKEN -R SaberMage/spt-core`.
3. Then: embed pubkeys → tag v0.1.0 → CI draft release → user signs locally (release-sign) → upload *.release.json → publish → flip repo public → Pages live → acceptance run (clean-machine install → quickstarts → llms/schema fetch → staged v0.1.1-shaped verify + recovery-key verify). REQ-REL-2 int + REQ-DOCS arcs close there.

## flags for user
- Licenses say "Copyright (c) 2026 SaberMage" (pseudonym) — change before flip if desired.
- Binary --help text leaks internal nomenclature (REQ-*/ADR-*/M4/M5/"owlery"/CONTEXT §) into the public CLI reference — recommend a help-text polish pass before the v0.1.0 tag.
</live-context>
<project-context>
M6 stage-setting D1–D6 DELIVERED (2026-06-05): spt-releases repo live (private; README + license split), non-interactive installers + CI rung, schemars manifest schema + drift gate, Tier-1 mdBook docs corpus + quickstart anti-rot rung, xtask generation plumbing + docs-publish pipeline, release pipeline + embedded two-key trust anchor + signing tooling, workspace 0.1.0. D7 (tag, sign, flip public, acceptance) awaits user: key ceremony + RELEASES_TOKEN secret. Side quest: traceable-reqs v0.1.2 (sh/ps1 scanning). After M6: spt-claude-code grill.
</project-context>


File created successfully at: C:\Users\decid\Documents\projects\spt-core\.claude\doyle-commune.md (file state is current in your context — no need to Read it back)

<live-context>
Generation 22 — **M6 D1–D6 EXECUTED in one session (2026-06-05); paused at the D7 ship gate (user actions required).**

## timeline
- D1 `e430327`: SaberMage/spt-releases created (private) + README (grill decision 7) + LICENSE-MIT/LICENSE-BINARY (royalty-free adapter clause) pushed (spt-releases@2debda8). REQ-REL-1 doc.
- D2 `2ab8bad`+`f8e0f8a`: installer/install.sh+.ps1 (non-interactive, sha256, user PATH, env knobs, ASSET_BASE CI/air-gap override); oneliner_e2e.rs staged-release rung (named so because *install*-named exes trip Windows UAC heuristic, os error 740); .gitattributes pins LF. REQ-INSTALL-1/2/3/5 activated.
- **Side quest**: traceable-reqs v0.1.2 shipped (BigscreenVR/traceable-reqs@aaf234d) — shell/PowerShell `#` tag scanning (REQ-SCAN-SHELL-PS, full doc/impl/unit/int); released + installed locally; spt-core ci.yml now source-pins it self-healing on gravity (build in $RUNNER_TEMP — in-checkout clone gets adopted by the workspace). Scan roots grew: installer/, CONTEXT.md (file root), docs-site/, .github/.
- D3 `be4e46c`: manifest_json_schema() from the SAME schemars derives that parse manifests; checked-in crates/spt-runtime/manifest.schema.json; drift test (SPT_BLESS=1 regen). REQ-DOCS-4 doc/impl/unit, REQ-DOCS-5 impl/int.
- D4: docs-site/ mdBook corpus (Starlight-styled CSS), quickstarts with REAL captured outputs, complete harness-contract vertical, shell getting-started via spt-shell-notify, vertical overviews; quickstart_e2e.rs runs the published messaging flow step-for-step (anti-rot). REQ-DOCS-1 doc, -2 doc+int, -3 doc.
- D5 `992547d`: crates/xtask — gen (CLI ref from --help, platform-normalized), check (CLI-ref drift + llms link gate, in CI both OSes), site (full Pages payload: HTML + raw .md + llms.txt/llms-full + schema + scripts + rustdoc); docs-publish.yml (tag/dispatch → force-push gh-pages on spt-releases). REQ-DOCS-1 impl, REQ-REL-1 impl.
- D6 `3f0904d`: BUILTIN_RELEASE_KEYS embedded trust anchor (empty until ceremony; release-keys.json = overlay, file revocations void builtin ids); xtask release-keygen/release-sign (manual local signing, SPT_RELEASE_SEED env); release.yml (tag → both runners → draft cross-repo release with binaries/SHA256SUMS/schema/mock-adapter.zip); workspace → single 0.1.0 ([workspace.package]). REQ-REL-2 impl, REQ-REL-3 impl+unit.
- 139/139 traceable-reqs green throughout; full sweep + clippy green; D5 CI run green, D6 run in flight.

## D7 ship gate — USER actions before I can proceed
1. Key ceremony: `cargo run -p xtask -- release-keygen rel-primary-2026` (and rel-recovery-2026); seeds → password manager + paper, separate; hand me the two public_hex to embed in BUILTIN_RELEASE_KEYS.
2. RELEASES_TOKEN: fine-grained PAT (Contents r/w on SaberMage/spt-releases only) → `gh secret set RELEASES_TOKEN -R SaberMage/spt-core`.
3. Then: embed pubkeys → tag v0.1.0 → CI draft release → user signs locally (release-sign) → upload *.release.json → publish → flip repo public → Pages live → acceptance run (clean-machine install → quickstarts → llms/schema fetch → staged v0.1.1-shaped verify + recovery-key verify). REQ-REL-2 int + REQ-DOCS arcs close there.

## flags for user
- Licenses say "Copyright (c) 2026 SaberMage" (pseudonym) — change before flip if desired.
- Binary --help text leaks internal nomenclature (REQ-*/ADR-*/M4/M5/"owlery"/CONTEXT §) into the public CLI reference — recommend a help-text polish pass before the v0.1.0 tag.
</live-context>
<project-context>
M6 stage-setting D1–D6 DELIVERED (2026-06-05): spt-releases repo live (private; README + license split), non-interactive installers + CI rung, schemars manifest schema + drift gate, Tier-1 mdBook docs corpus + quickstart anti-rot rung, xtask generation plumbing + docs-publish pipeline, release pipeline + embedded two-key trust anchor + signing tooling, workspace 0.1.0. D7 (tag, sign, flip public, acceptance) awaits user: key ceremony + RELEASES_TOKEN secret. Side quest: traceable-reqs v0.1.2 (sh/ps1 scanning). After M6: spt-claude-code grill.
</project-context>


M6 D1–D6 done, one session. Summary:

**Delivered** (all pushed, CI green through D5; D6 run in flight):
- **D1** — `SaberMage/spt-releases` live (private): README per grill spec, MIT/freeware license split with royalty-free adapter clause
- **D2** — `install.sh`/`install.ps1` (non-interactive, sha256-verified, user PATH, idempotent) + staged-release CI rung on both runners. Side quest: **traceable-reqs v0.1.2** shipped (own repo) — `.sh`/`.ps1` tag scanning; CI self-heals the pinned build on gravity
- **D3** — manifest JSON Schema from the parsing derives themselves + drift gate
- **D4** — full Tier-1 docs corpus (mdBook, Starlight-styled), real captured outputs, complete harness-contract vertical, quickstart anti-rot test
- **D5** — xtask generation plumbing (CLI ref from `--help`, llms.txt/llms-full, raw-`.md`, rustdoc) + `docs-publish.yml` pipeline
- **D6** — embedded two-key trust anchor (`BUILTIN_RELEASE_KEYS`, file-overlay revocable), `release-keygen`/`release-sign` ceremony tooling, tag-triggered draft-release pipeline, workspace → **0.1.0**

139/139 traceability green throughout.

**D7 ship gate — needs your hands.** Plain mode for the sequence:

1. **Key ceremony** (ADR-0015) — run twice, store seeds, give me only the public keys:
   ```
   cargo run -p xtask -- release-keygen rel-primary-2026
   cargo run -p xtask -- release-keygen rel-recovery-2026
   ```
   Each prints `public_hex` + `seed_hex`. Seeds → password manager (+ paper backup, physically separate). Paste the two **public_hex** values to me; I embed them in `BUILTIN_RELEASE_KEYS`. Then clear the terminal scrollback.

2. **RELEASES_TOKEN** — create a fine-grained PAT (github.com → Settings → Developer settings → Fine-grained tokens): repository access = `SaberMage/spt-releases` only, permission = Contents read/write. Then:
   ```
   gh secret set RELEASES_TOKEN -R SaberMage/spt-core
   ```

3. Then I tag `v0.1.0` → CI builds the draft release → you run one local `release-sign` with the primary seed → publish, flip public, acceptance run.

**Two flags before the flip:**
- Licenses read "Copyright (c) 2026 **SaberMage**" — fine, or want a different holder name?
- The binary's `--help` text leaks internal jargon (REQ-*/ADR-*/M4/M5/"owlery") into the public CLI reference. Recommend a help-text polish pass before tagging — say the word and I'll do it while you run the ceremony.


i made some adjustments to the spt-releases README.md. please fetch it and sync it back to this repo (if it has a copy here) so the changes aren't lost during future updates/releases. --- __rel-primary-2026__
public
||```
54188f127f9c361090c90f85ac072e623d7001b311d25eca0dd5f57ad05e3ec9
```|| __rel-recovery-2026__
public
||```
bd8908b9cbb0a1c308dd0bfb514596dd9de9ea64841528100d65efa96e834ab7
```|| --- that's 1, 1, and 2 done. how can we automate the local `release-sign`? --- copyright is fine. -yes, definitely do the help-text polish pass.


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


PowerShell

cd C:\Users\decid\Documents\projects\spt-releases; git pull 2>&1 | Select-Object -Last 3; git log --oneline -3

Pull user's README edits


Fast-forward
 README.md | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)
4b8e81b Revise README
2debda8 Public face: README, MIT/freeware license split
Shell cwd was reset to C:\Users\decid\Documents\projects\spt-core


1	# SPT
2
3	## What is SPT?
4
5	A communication ecosystem for all of your computers. Interact with _any resource_, on _any machine_, **from** any machine, using any interface. Empower your LLMs to do the same.
6
7	`spt-core` is the heart of a harness-independent framework. It offers: inter-agent messaging, live agent lifecycle, terminal hosting, seamless self-update, and zero-config cross-machine networking — shipped as both a Rust library workspace and a single canonical binary (`spt` / `spt.exe`).
8
9	SPT lets coding agents talk to each other — across sessions, across projects, and across machines — without a central server. An agent running under one harness can message an agent under another, keep a persistent identity and memory across context resets, and be reached (or revived) from any paired machine. Three pieces make that up:
10	- **(a)** the `spt` binary, the per-machine core that does the work — messaging, lifecycle, terminal hosting, P2P networking — and is what this repository distributes.
11	- **(b)** *harness adapters*, thin declarative manifests that teach spt-core how to drive a specific agent harness. An adapter for Claude Code is in development; harnesses like Codex and Pi — the [pi coding agent](https://github.com/badlogic/pi-mono) — are the kind of target the contract is designed for.
12	- **(c)** *shells*, bridge applications to any resource or service that an agent can drive and be driven through. The adapter contract is public, documented, and royalty-free to build against — today's surface for third parties is the binary plus that contract.
13
14	## Status
15
16	| Capability | State |
17	|---|---|
18	| Inter-agent messaging (send / ring / ready / poll), same-machine and cross-machine | ✅ works today |
19	| Live-agent lifecycle (persistent identity, context communes, dormancy/wake) | ✅ works today |
20	| Terminal hosting + remote attach (drive a session on another node) | ✅ works today |
21	| Zero-config P2P networking (QUIC) + TOTP-seeded machine pairing, private subnets | ✅ works today |
22	| Harness contract: manifest + `spt api` (build your own adapter) | ✅ works today |
23	| Shell adapters (e.g. desktop-notification shell) + `spt adapter add` | ✅ works today |
24	| Signed, peer-propagated self-update | ✅ works today |
25	| Claude Code harness adapter (`spt-claude-code`) | 🚧 in development |
26	| OS-service registration at install (always-on after reboot) | 🔜 next |
27	| macOS builds | 🔜 next |
28	| SDK crates on crates.io | 🔜 later |
29
30	Platforms today: **Windows + Linux**.
31
32	## Install
33
34	One line, non-interactive. Downloads the latest release, sha256-verifies it, places the binary, and registers it on your user PATH.
35
36	**Linux:**
37
38	```sh
39	curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh
40	```
41
42	**Windows (PowerShell):**
43
44	```powershell
45	irm https://sabermage.github.io/spt-releases/install.ps1 | iex
46	```
47
48	Then verify:
49
50	```
51	spt --version
52	```
53
54	> **Windows note:** the PATH registration takes effect in *new* terminals. The installer prints the absolute path to the binary so you can use it immediately in the current one.
55
56	## Getting started — developers
57
58	Two agents exchanging a message, end to end, in under 10 minutes:
59
60	- **[Human quickstart](https://sabermage.github.io/spt-releases/quickstart/messaging.html)** — install, `spt ready` two agents, send and poll a message, then see what actually happened.
61
62	## Getting started — adapter developers & dev-agents
63
64	Build a working harness or shell adapter from the published contract alone:
65
66	- **[Adapter quickstart](https://sabermage.github.io/spt-releases/quickstart/adapter.html)** — a minimal adapter that satisfies the manifest + `spt api` contract, walked through via the `mock-adapter` reference (source ships with every release).
67	- **[Manifest JSON Schema](https://sabermage.github.io/spt-releases/manifest.schema.json)** — validate your adapter manifest mechanically.
68
69	## Documentation
70
71	- **Docs site:** <https://sabermage.github.io/spt-releases> — mental model, quickstarts, the complete harness-contract reference, CLI reference, rustdoc.
72	- **For AI agents:** [`llms.txt`](https://sabermage.github.io/spt-releases/llms.txt) (curated index) and [`llms-full.txt`](https://sabermage.github.io/spt-releases/llms-full.txt) (full export). Append `.md` to any doc URL for raw markdown.
73
74	## License
75
76	Split by artifact:
77
78	- **Install scripts, `mock-adapter`, and documentation content** — [MIT](./LICENSE-MIT). Copy them; that's what they're for.
79	- **The `spt` binary** — a short [proprietary freeware license](./LICENSE-BINARY): free to run and redistribute unmodified, no warranty. Building adapters, shells, or integrations against the public contract is explicitly **unrestricted and royalty-free**.


cd C:\Users\decid\Documents\projects\spt-core; New-Item -ItemType Directory -Force releases-repo | Out-Null; Copy-Item ..\spt-releases\README.md releases-repo\README.md; Copy-Item ..\spt-releases\LICENSE-MIT releases-repo\LICENSE-MIT; Copy-Item ..\spt-releases\LICENSE-BINARY releases-repo\LICENSE-BINARY; Get-ChildItem releases-repo

Mirror spt-releases main-branch files into spt-core truth dir


    Directory: C:\Users\decid\Documents\projects\spt-core\releases-repo

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---            6/5/2026 11:31 AM           1774 LICENSE-BINARY
-a---            6/5/2026 11:31 AM           1304 LICENSE-MIT
-a---            6/5/2026  4:05 PM           4807 README.md


i made some adjustments to the spt-releases README.md. please fetch it and sync it back to this repo (if it has a copy here) so the changes aren't lost during future updates/releases. --- __rel-primar…


Write

# releases-repo/ — truth copies of SaberMage/spt-releases' main branch

ADR-0014: doc truth lives in THIS repo; spt-releases is a pure publish
target. The files here (README.md, LICENSE-MIT, LICENSE-BINARY) are the
authoritative source for the spt-releases repo face. The docs-publish
workflow pushes them to spt-releases' main branch whenever they differ, so
edits made here propagate — and edits made directly on spt-releases must be
pulled back here or they'll be overwritten by the next publish.


File created successfully at: C:\Users\decid\Documents\projects\spt-core\releases-repo\SYNC.md (file state is current in your context — no need to Read it back)

# releases-repo/ — truth copies of SaberMage/spt-releases' main branch

ADR-0014: doc truth lives in THIS repo; spt-releases is a pure publish
target. The files here (README.md, LICENSE-MIT, LICENSE-BINARY) are the
authoritative source for the spt-releases repo face. The docs-publish
workflow pushes them to spt-releases' main branch whenever they differ, so
edits made here propagate — and edits made directly on spt-releases must be
pulled back here or they'll be overwritten by the next publish.