=== commits since my doc stage ===
1588525 feat(daemon): receive_wan honors a Gateway-typed WAN origin (T3+T4)
0f84e25 feat(daemon): advertise endpoint_type from the perch info.json state (T2)
0b79951 feat(net): Instance.endpoint_type seam for cross-node Gateway user-msg (T1)
6f720d6 @ docs(req): mint REQ-MSG-6 cross-node Gateway WAN user-msg (doc stage)
=== diff stat ===
 CONTEXT.md                            |   1 +
 crates/spt-daemon/src/dispatch.rs     |   5 +-
 crates/spt-daemon/src/notif.rs        |   1 +
 crates/spt-daemon/src/presence.rs     |   1 +
 crates/spt-daemon/src/reconcile.rs    |   1 +
 crates/spt-daemon/src/registryhost.rs |  39 +++++++++++
 crates/spt-daemon/src/shelldisc.rs    |   1 +
 crates/spt-daemon/src/wan.rs          | 125 ++++++++++++++++++++++++++++++----
 crates/spt-daemon/tests/access.rs     |  13 ++--
 crates/spt-daemon/tests/dispatch.rs   |   2 +
 crates/spt-daemon/tests/pump.rs       |   1 +
 crates/spt-daemon/tests/replicate.rs  |   2 +
 crates/spt-daemon/tests/sync.rs       |   2 +
 crates/spt-daemon/tests/wanmsg.rs     |  26 +++++--
 crates/spt-net/src/net/registry.rs    |  21 ++++++
 crates/spt-net/src/net/replicate.rs   |   1 +
 crates/spt-net/src/net/wanmsg.rs      |   1 +
 crates/spt/src/cli.rs                 |   3 +
 crates/spt/src/wansend.rs             |   2 +
 traceable-reqs.toml                   |   6 +-
 20 files changed, 227 insertions(+), 27 deletions(-)


G2 closed. Now the Gateway-WAN gate review — 4 commits, REQ-MSG-6 minted (the wide +1 churn is expected: an `Instance` struct field forces all literal constructors to add it). Reading the substance: the Instance field, the `receive_wan` resolve, the advertise populate, and the matrix.


Bash

cd C:/Users/decid/Documents/projects/spt-core && git diff 30fea7f..HEAD -- crates/spt-net/src/net/registry.rs crates/spt-daemon/src/registryhost.rs

Instance field + advertise populate diff


diff --git a/crates/spt-daemon/src/registryhost.rs b/crates/spt-daemon/src/registryhost.rs
index 92ce8fd..2f24812 100644
--- a/crates/spt-daemon/src/registryhost.rs
+++ b/crates/spt-daemon/src/registryhost.rs
@@ -168,6 +168,22 @@ impl RegistryHost {
         &self.node_hex
     }

+    /// Every advertised [`Instance`] this host knows for `endpoint_id`, across
+    /// all of its per-subnet registries (the WAN user-msg origin resolver reads
+    /// these and keys by the proven node — REQ-MSG-6). Cloned out so the lock
+    /// is released before the caller inspects the rows. The same id can hold
+    /// rows on several nodes / in several subnets; the resolver filters to the
+    /// QUIC-proven origin node.
+    // [impl->REQ-MSG-6]
+    pub fn instances_of(&self, endpoint_id: &str) -> Vec<Instance> {
+        self.regs
+            .lock()
+            .unwrap()
+            .values()
+            .flat_map(|reg| reg.instances(endpoint_id).iter().cloned())
+            .collect()
+    }
+
     /// Run `f` with this host's **canonical per-node epoch** — the ONE
     /// monotonic counter (identity/epoch.json) behind every locally-authored
     /// registry row. Every other per-node-id producer on this node MUST mint
@@ -397,6 +413,14 @@ impl RegistryHost {
             // epoch lease, same visibility gate, no new channel. Subnet-wide
             // most-recently-active is a projection of these rows (D6b).
             // [impl->REQ-PRES-1]
+            // The endpoint's own advertised type tag (M9, REQ-MSG-6): the
+            // info.json `state` IS the bound endpoint type
+            // (`EndpointType::as_tag` — `"gateway"` for a Gateway surface).
+            // A node advertises ONLY its own endpoints' types; the cross-node
+            // user-msg gate reads this at the receive funnel (T3). A recordless
+            // perch advertises no type → the funnel fail-closes to a re-stamp.
+            // [impl->REQ-MSG-6]
+            let endpoint_type = info.as_ref().map(|i| i.state.clone());
             let last_active_ms = info.and_then(|i| i.last_active_ms);
             for sub in &subnets.subnets {
                 let Ok(ep) = epoch.next_epoch() else { continue };
@@ -416,6 +440,10 @@ impl RegistryHost {
                     // 13/22) — the re-pair overwrite anchor.
                     // [impl->REQ-SUBNET-7]
                     machine_id: self.machine_id.clone(),
+                    // The endpoint's own type tag (REQ-MSG-6) — read by the
+                    // cross-node user-msg gate, keyed by the proven node.
+                    // [impl->REQ-MSG-6]
+                    endpoint_type: endpoint_type.clone(),
                 };
                 let reg = regs.entry(sub.name.clone()).or_default();
                 match advertise_if_visible(
@@ -818,6 +846,7 @@ mod tests {
                 shell_adapters: Vec::new(),
                 node_label: None,
                 machine_id: None,
+                endpoint_type: None,
             },
         }
     }
@@ -899,6 +928,7 @@ mod tests {
             shell_adapters: Vec::new(),
             node_label: Some(label.to_string()),
             machine_id: Some(mid.to_string()),
+            endpoint_type: None,
         }
     }

@@ -1209,6 +1239,7 @@ mod tests {
                     shell_adapters: Vec::new(),
                     node_label: None,
                     machine_id: None,
+                    endpoint_type: None,
                 },
             }
         }
@@ -1605,6 +1636,14 @@ mod tests {
                 instance_row(&out, "lab-ag").node_label.as_deref(),
                 Some("MYHOST")
             );
+            // [unit->REQ-MSG-6] the advertised row carries the perch's OWN type
+            // tag (info.json state) — the datum the cross-node user-msg gate
+            // resolves the origin against (T2 populate).
+            assert_eq!(
+                instance_row(&out, "lab-ag").endpoint_type.as_deref(),
+                Some("ready_agent"),
+                "advertise stamps the endpoint's own type tag"
+            );
             assert_eq!(
                 h.rows("lab", "lab-ag")[0].node_label.as_deref(),
                 Some("MYHOST")
diff --git a/crates/spt-net/src/net/registry.rs b/crates/spt-net/src/net/registry.rs
index c2a1e00..9c8b517 100644
--- a/crates/spt-net/src/net/registry.rs
+++ b/crates/spt-net/src/net/registry.rs
@@ -139,6 +139,22 @@ pub struct Instance {
     // [impl->REQ-SUBNET-7]
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub machine_id: Option<String>,
+    /// The endpoint's advertised **type tag** (M9, REQ-MSG-6 — CONTEXT
+    /// §Gateway): the open-type tag this endpoint binds under
+    /// ([`spt_proto::endpoint::EndpointType::as_tag`]; `"gateway"` for a
+    /// Gateway surface). The cross-node `user-msg` identity gate reads this:
+    /// `receive_wan` resolves the origin endpoint's advertised type, keyed by
+    /// the QUIC-proven origin node, and honors a `user-msg` only from a
+    /// Gateway-typed origin (REQ-MSG-5 over the WAN). A node advertises only
+    /// its **own** endpoints' types (no cross-node authorship); the field
+    /// extends REQ-INST-7's data model and rides the existing epoch lease +
+    /// `RegistryUpdate` replication like [`resources`](Self::resources) —
+    /// no new channel, no new merge key. Additive serde-default: an N-1 row
+    /// (no field) parses clean and resolves to "not provably user-backed"
+    /// (the fail-closed re-stamp; rollout grace).
+    // [impl->REQ-MSG-6]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub endpoint_type: Option<String>,
 }

 /// One node-level label datum (REQ-SUBNET-3 endpoint-less gap): a node's
@@ -753,6 +769,7 @@ mod tests {
             shell_adapters: Vec::new(),
             node_label: None,
             machine_id: None,
+            endpoint_type: None,
         }
     }

@@ -908,6 +925,7 @@ mod tests {
             shell_adapters: Vec::new(),
             node_label: None,
             machine_id: None,
+            endpoint_type: None,
         };
         reg.merge_instance("ling", with(Some("rust builds"), 1));
         assert_eq!(
@@ -955,6 +973,7 @@ mod tests {
             shell_adapters: Vec::new(),
             node_label: label.map(str::to_string),
             machine_id: None,
+            endpoint_type: None,
         };
         reg.merge_instance("ling", with(Some("OLDHOST"), 1));
         assert_eq!(
@@ -1126,6 +1145,7 @@ mod tests {
             shell_adapters: Vec::new(),
             node_label: None,
             machine_id: None,
+            endpoint_type: None,
         };
         reg.merge_instance("ling", with(Some(1_000), 1));
         assert_eq!(
@@ -1168,6 +1188,7 @@ mod tests {
                 shell_adapters: Vec::new(),
                 node_label: None,
                 machine_id: None,
+                endpoint_type: None,
             },
         );
         reg.merge_instance("oak", inst("n1", Status::Dormant, 1)); // no blurb


check your perch monitor. todlando sent a message


`Instance.endpoint_type` is clean — additive serde-default + skip_serializing_if, N-1 row resolves to fail-closed re-stamp, rides existing epoch lease (no new merge key). Advertise stamps the perch's own `info.state` type tag (test confirms `ready_agent`), recordless → None → fail-closed. Now the critical piece — the `receive_wan` resolve and the matrix.


cd C:/Users/decid/Documents/projects/spt-core && git diff 30fea7f..HEAD -- crates/spt-daemon/src/wan.rs crates/spt-daemon/tests/wanmsg.rs

receive_wan resolve + matrix diff


diff --git a/crates/spt-daemon/src/wan.rs b/crates/spt-daemon/src/wan.rs
index a3549a9..4def222 100644
--- a/crates/spt-daemon/src/wan.rs
+++ b/crates/spt-daemon/src/wan.rs
@@ -34,11 +34,14 @@ use std::borrow::Cow;
 use std::path::Path;

 use spt_msg::deliver::{deliver_tcp, perch_exists};
+use spt_net::net::registry::Instance;
 use spt_net::net::wanmsg::WanMessage;
-use spt_proto::event::{compose_msg_event, parse_event, EVENT_TYPE_USER_MSG};
+use spt_proto::endpoint::EndpointType;
+use spt_proto::event::{compose_msg_event, is_gateway_endpoint, parse_event, EVENT_TYPE_USER_MSG};
 use spt_store::spool;

 use crate::access::{access_check, classify_inbound, AccessDecision};
+use crate::registryhost::RegistryHost;

 /// Re-stamp a WAN-inbound `user-msg` body down to plain `msg` **unless** the
 /// transport-proven origin is a user-backed endpoint (CONTEXT §Gateway; the
@@ -49,10 +52,11 @@ use crate::access::{access_check, classify_inbound, AccessDecision};
 /// body. A non-`user-msg` body (a plain message, any other typed envelope, or a
 /// bare body) is returned unchanged.
 ///
-/// `origin_user_backed` is resolved by the caller from the proven origin node:
-/// over WAN, the ONLY provably user-backed origin is a registry-resolved Gateway
-/// endpoint (delivered in T5). Until then callers pass `false`, so every WAN
-/// `user-msg` is re-stamped — fail-closed by construction.
+/// `origin_user_backed` is resolved by the caller from the proven origin node
+/// ([`origin_user_backed`]): over WAN, the ONLY provably user-backed origin is a
+/// registry-resolved Gateway endpoint advertised on the proven node (REQ-MSG-6).
+/// An origin that advertises no type, or a non-Gateway type, yields `false` and
+/// the `user-msg` is re-stamped — fail-closed by construction.
 // [impl->REQ-MSG-5]
 // [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
 fn restamp_wan_user_msg(body: &str, origin_user_backed: bool) -> Cow<'_, str> {
@@ -67,6 +71,31 @@ fn restamp_wan_user_msg(body: &str, origin_user_backed: bool) -> Cow<'_, str> {
     }
 }

+/// Resolve whether a WAN-inbound `user-msg`'s origin is provably **user-backed**
+/// (REQ-MSG-6 — CONTEXT §Gateway): among the origin endpoint's advertised
+/// instances, the one hosted on the **QUIC-handshake-proven** `origin_node`
+/// advertises the Gateway type ([`spt_proto::event::GATEWAY_TAG`]). Keyed on the
+/// proven node, NEVER the wire `from` — a record whose `from` claims a gateway
+/// id is inert unless the proven node actually hosts that gateway-typed
+/// endpoint. An absent or non-gateway advertised type → `false`: the fail-closed
+/// re-stamp (an N-1 node that does not yet advertise `endpoint_type` degrades
+/// gracefully to plain `msg`).
+///
+/// This resolves the *advertised* type; it is **not** a forgery defense. The
+/// trust boundary is subnet membership (operator-ratified 2026-06-13): a subnet
+/// member is already trusted, so a member forging the Gateway type is out of
+/// scope by construction. The proven-node keying is registry-addressing
+/// correctness, not an extra trust layer.
+// [impl->REQ-MSG-6]
+fn origin_user_backed(instances: &[Instance], origin_node: &str) -> bool {
+    instances.iter().any(|i| {
+        i.node == origin_node
+            && i.endpoint_type
+                .as_deref()
+                .is_some_and(|tag| is_gateway_endpoint(&EndpointType::from_tag(tag)))
+    })
+}
+
 /// Wall-clock now (epoch ms) — the reply-window clock.
 fn now_ms() -> u64 {
     std::time::SystemTime::now()
@@ -95,10 +124,18 @@ pub enum WanOutcome {
 /// the QUIC handshake-proven remote node id (the broker's
 /// `NetStreamInfo::remote_id_hex` for the stream the record arrived on) —
 /// never anything read out of record bytes (REQ-HAZARD-WAN-ORIGIN-AUTH).
+/// `registry` is the node's [`RegistryHost`]: the user-msg identity gate
+/// resolves the origin endpoint's advertised type from it, keyed by
+/// `origin_node` (REQ-MSG-6).
 // [impl->REQ-NET-1]
 // [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
 // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]
-pub fn receive_wan(msg: &WanMessage, origin_node: &str, owlery: &Path) -> WanOutcome {
+pub fn receive_wan(
+    msg: &WanMessage,
+    origin_node: &str,
+    owlery: &Path,
+    registry: &RegistryHost,
+) -> WanOutcome {
     // Gate 2 of ADR-0009 (visibility ran at the sender's resolution; grants
     // gate capabilities, not messages). Messages classify reply-vs-unsolicited
     // against the target's own recent outbound (the stateful firewall): a
@@ -124,11 +161,13 @@ pub fn receive_wan(msg: &WanMessage, origin_node: &str, owlery: &Path) -> WanOut
         Err(_) => return WanOutcome::NoPerch,
     }

-    // user-msg identity gate, WAN-ingress half (REQ-MSG-5; KH 7.5): re-stamp a
+    // user-msg identity gate, WAN-ingress half (REQ-MSG-5/6; KH 7.5): re-stamp a
     // forged/unverified user-msg down to plain msg before it reaches the target.
-    // T4 fail-closes (no WAN origin is yet provably user-backed); T5 resolves a
-    // Gateway origin from the registry and threads it here.
-    let delivered_body = restamp_wan_user_msg(&msg.body, false);
+    // The origin is provably user-backed iff its advertised endpoint, hosted on
+    // the QUIC-proven origin_node, is Gateway-typed (REQ-MSG-6); absent/unknown
+    // type fail-closes to a re-stamp.
+    let origin_backed = origin_user_backed(&registry.instances_of(&msg.from), origin_node);
+    let delivered_body = restamp_wan_user_msg(&msg.body, origin_backed);

     // Live-TCP first (mirrors the local TCP-xor-spool discipline, 4.5), claim
     // after the completed write (see module docs for the window accounting).
@@ -151,9 +190,9 @@ mod tests {
     use spt_proto::event::compose_user_msg_event;

     // [unit->REQ-MSG-5] WAN-ingress re-stamp (KH 7.5): a user-msg body from an
-    // origin not provably user-backed (the T4 fail-closed default) is
-    // re-stamped down to plain msg, preserving from + the decoded inner body; a
-    // user-backed origin (the T5 Gateway path) keeps it verbatim; a plain msg
+    // origin not provably user-backed (the fail-closed default) is re-stamped
+    // down to plain msg, preserving from + the decoded inner body; a user-backed
+    // origin (the Gateway path, REQ-MSG-6) keeps it verbatim; a plain msg
     // envelope and a bare body are untouched either way.
     #[test]
     fn wan_user_msg_restamped_unless_origin_user_backed() {
@@ -176,4 +215,64 @@ mod tests {
         // A bare (non-envelope) body is untouched.
         assert_eq!(restamp_wan_user_msg("hello", false).into_owned(), "hello");
     }
+
+    // [unit->REQ-MSG-6] the cross-node Gateway user-msg origin matrix: an origin
+    // is provably user-backed (→ the user-msg survives) ONLY when its advertised
+    // endpoint is Gateway-typed on an instance hosted on the QUIC-proven origin
+    // node. Every other shape resolves false (the fail-closed re-stamp). Keyed on
+    // the proven node — a wire `from` cannot borrow another node's Gateway type.
+    #[test]
+    fn origin_user_backed_matrix() {
+        use spt_net::net::registry::Status;
+        use spt_proto::event::GATEWAY_TAG;
+
+        let row = |node: &str, ty: Option<&str>| Instance {
+            node: node.to_string(),
+            status: Status::Active,
+            epoch: 1,
+            resources: None,
+            last_active_ms: None,
+            shell_adapters: Vec::new(),
+            node_label: None,
+            machine_id: None,
+            endpoint_type: ty.map(str::to_string),
+        };
+        let proven = "aa11node";
+
+        // (1) Gateway-typed, hosted on the proven node → honored.
+        assert!(origin_user_backed(&[row(proven, Some(GATEWAY_TAG))], proven));
+
+        // (2) A non-Gateway (agent / other) type → re-stamped.
+        assert!(!origin_user_backed(&[row(proven, Some("live_agent"))], proven));
+        assert!(!origin_user_backed(&[row(proven, Some("ready_agent"))], proven));
+
+        // (3) No endpoint_type advertised (an N-1 node) → re-stamped (rollout grace).
+        assert!(!origin_user_backed(&[row(proven, None)], proven));
+
+        // (4) A Gateway row exists, but on a DIFFERENT node than the proven
+        //     origin — keying is on the proven node, never the wire `from`.
+        assert!(!origin_user_backed(&[row("bb22other", Some(GATEWAY_TAG))], proven));
+
+        // …and a mix where the proven node is agent-typed while another node is
+        // the gateway → still re-stamped (the proven node's own type governs).
+        assert!(!origin_user_backed(
+            &[
+                row(proven, Some("live_agent")),
+                row("bb22other", Some(GATEWAY_TAG)),
+            ],
+            proven
+        ));
+
+        // …but the proven node hosting the Gateway row among others → honored.
+        assert!(origin_user_backed(
+            &[
+                row("bb22other", Some("live_agent")),
+                row(proven, Some(GATEWAY_TAG)),
+            ],
+            proven
+        ));
+
+        // (5) An unresolved origin (no advertised instances) → re-stamped.
+        assert!(!origin_user_backed(&[], proven));
+    }
 }
diff --git a/crates/spt-daemon/tests/wanmsg.rs b/crates/spt-daemon/tests/wanmsg.rs
index 917c3dd..1bf2f28 100644
--- a/crates/spt-daemon/tests/wanmsg.rs
+++ b/crates/spt-daemon/tests/wanmsg.rs
@@ -24,6 +24,7 @@ use std::time::Duration;

 use spt_daemon::brain::{Brain, BrokerEvent};
 use spt_daemon::nethost::{NetConfig, NetHost};
+use spt_daemon::registryhost::RegistryHost;
 use spt_daemon::wan::{receive_wan, WanOutcome};
 use spt_daemon::Broker;
 use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
@@ -127,6 +128,7 @@ fn funnel_until_eof(
     stream_id: u64,
     origin: &str,
     owlery: &std::path::Path,
+    registry: &RegistryHost,
 ) -> Vec<WanOutcome> {
     let mut decoder = WanDecoder::new();
     let mut outcomes = Vec::new();
@@ -138,7 +140,7 @@ fn funnel_until_eof(
                 ..
             } if sid == stream_id => {
                 for rec in decoder.push(&bytes) {
-                    outcomes.push(receive_wan(&rec, origin, owlery));
+                    outcomes.push(receive_wan(&rec, origin, owlery, registry));
                 }
             }
             BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => break,
@@ -155,8 +157,9 @@ fn funnel_until_eof(
 // target spool — the security-critical row of the identity-gate truth table on
 // the cross-node surface. A fail-OPEN here would grant the user's authority to
 // an unproven WAN origin (an agent forging a user-msg envelope); this pins it
-// shut. (T4 fail-closes every WAN user-msg; the Gateway-origin exception is the
-// deferred cross-node beat.)
+// shut. The Gateway-origin honored path (an origin advertising the Gateway type
+// on the proven node) is the REQ-MSG-6 matrix in `wan.rs`; here the origin
+// advertises no type, so the gate fail-closes.
 #[test]
 fn wan_user_msg_from_unproven_origin_is_restamped_at_the_funnel() {
     init_home();
@@ -172,7 +175,10 @@ fn wan_user_msg_from_unproven_origin_is_restamped_at_the_funnel() {
     // The transport-proven origin is some remote node — not a provable
     // user-backed Gateway (T4 fail-closed: no WAN origin is yet provable).
     let owlery = perch::owlery_dir();
-    let outcome = receive_wan(&m, "deadbeefcafef00d", &owlery);
+    // The origin advertises no Gateway type (empty registry) → not provably
+    // user-backed → the funnel fail-closes and re-stamps.
+    let registry = RegistryHost::new("b-wanmsg");
+    let outcome = receive_wan(&m, "deadbeefcafef00d", &owlery, &registry);
     assert_eq!(outcome, WanOutcome::Spooled);

     // The spooled body is re-stamped to plain msg — user authority NOT granted.
@@ -255,7 +261,8 @@ fn wan_message_lands_exactly_once_under_transport_origin() {
         "the stream table's origin IS the QUIC handshake identity"
     );
     b_probe.net_stream_subscribe(stream, 0).expect("subscribe");
-    let outcomes = funnel_until_eof(&mut b_probe, stream, &remote, &owlery);
+    let registry = RegistryHost::new("b-wanmsg");
+    let outcomes = funnel_until_eof(&mut b_probe, stream, &remote, &owlery, &registry);

     assert_eq!(
         outcomes,
@@ -310,6 +317,7 @@ fn receiver_restart_replays_feed_without_double_delivery() {
     // Life 1: funnel the first record only, then crash.
     let (stream, remote) = wait_for_stream(&mut b_probe);
     b_probe.net_stream_subscribe(stream, 0).expect("subscribe");
+    let life1_registry = RegistryHost::new("b-wanmsg");
     let mut decoder = WanDecoder::new();
     let mut delivered_first = false;
     while !delivered_first {
@@ -321,7 +329,10 @@ fn receiver_restart_replays_feed_without_double_delivery() {
             } if sid == stream => {
                 // Deliver the FIRST record only, then crash.
                 if let Some(rec) = decoder.push(&bytes).into_iter().next() {
-                    assert_eq!(receive_wan(&rec, &remote, &owlery), WanOutcome::Spooled);
+                    assert_eq!(
+                        receive_wan(&rec, &remote, &owlery, &life1_registry),
+                        WanOutcome::Spooled
+                    );
                     delivered_first = true;
                 }
             }
@@ -338,7 +349,8 @@ fn receiver_restart_replays_feed_without_double_delivery() {
     receiver
         .net_stream_subscribe(stream, 0)
         .expect("resubscribe");
-    let outcomes = funnel_until_eof(&mut receiver, stream, &remote, &owlery);
+    let registry = RegistryHost::new("b-wanmsg");
+    let outcomes = funnel_until_eof(&mut receiver, stream, &remote, &owlery, &registry);
     assert_eq!(
         outcomes,
         vec![


The resolve logic is posture-faithful and the matrix is thorough — `origin_user_backed` keys strictly on `i.node == origin_node` (proven node, never wire `from`), checks only `is_gateway`, absent type → false. The matrix nails the anti-spoof rows (gateway on a *different* node than proven → re-stamped; proven node's own type governs). One thing left to confirm before verdict: that the **production** `receive_wan` caller threads the live node registry (the tests deliberately pass an empty one for fail-closed rows).


cd C:/Users/decid/Documents/projects/spt-core && git diff 30fea7f..HEAD -- crates/spt-daemon/src/dispatch.rs && echo "=== other receive_wan callers ===" && grep -rn "receive_wan(" crates --include=*.rs | grep -v "tests/" | grep -v "fn receive_wan"

Verify production caller threads real registry


diff --git a/crates/spt-daemon/src/dispatch.rs b/crates/spt-daemon/src/dispatch.rs
index f7f7c11..b5b7576 100644
--- a/crates/spt-daemon/src/dispatch.rs
+++ b/crates/spt-daemon/src/dispatch.rs
@@ -330,7 +330,7 @@ fn worker(
             }
         }
         StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),
-        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths),
+        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths, registry),
         StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),
         StreamFamily::Attach => {
             // The gate subject is the session's OWNING endpoint, resolved
@@ -597,6 +597,7 @@ fn serve_wan_feed(
     stream_id: u64,
     origin: &str,
     paths: &DispatchPaths,
+    registry: &RegistryHost,
 ) -> DispatchOutcome {
     if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {
         return DispatchOutcome::Failed(e.to_string());
@@ -611,7 +612,7 @@ fn serve_wan_feed(
                 ..
             }) if sid == stream_id => {
                 for msg in decoder.push(&bytes) {
-                    let _ = receive_wan(&msg, origin, &paths.owlery);
+                    let _ = receive_wan(&msg, origin, &paths.owlery, registry);
                     handled += 1;
                 }
             }
=== other receive_wan callers ===
crates/spt-daemon/src/dispatch.rs:615:                    let _ = receive_wan(&msg, origin, &paths.owlery, registry);


Production wiring confirmed — `serve_wan_feed` threads the live node `registry` + the QUIC-proven `origin`. The implementation is complete and posture-faithful. Running independent verification (matrix test + wanmsg funnel + advertise test + xtask check) for an evidence-based stamp.


cd C:/Users/decid/Documents/projects/spt-core && cargo test -q -p spt-daemon --lib origin_user_backed_matrix 2>&1 | tail -6; echo "MATRIX_EXIT:${PIPESTATUS[0]}"

Matrix unit test


Command running in background with ID: bd984eojj. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\13c96a50-2b78-4c16-a995-1da2b1836fd8\tasks\bd984eojj.output. You will be notified when it completes. To check interim output, use Read on that file path.


cd C:/Users/decid/Documents/projects/spt-core && cargo test -q -p spt-daemon --test wanmsg 2>&1 | tail -8; echo "WANMSG_EXIT:${PIPESTATUS[0]}"; cargo run -q -p xtask -- check 2>&1 | tail -3; echo "XTASK_EXIT:${PIPESTATUS[0]}"

wanmsg integration + xtask check


Command running in background with ID: b8hm7bvq8. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\13c96a50-2b78-4c16-a995-1da2b1836fd8\tasks\b8hm7bvq8.output. You will be notified when it completes. To check interim output, use Read on that file path.


<task-notification>
<task-id>bd984eojj</task-id>
<tool-use-id>toolu_015S99b4HjC961rWNjQEjHFo</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\13c96a50-2b78-4c16-a995-1da2b1836fd8\tasks\bd984eojj.output</output-file>
<status>completed</status>
<summary>Background command "Matrix unit test" completed (exit code 0)</summary>
</task-notification>


Verification running. I'll be notified when both finish, then stamp the Gateway-WAN gate.

**Review summary (pending the green confirm):** PR #9 is clean and posture-faithful — `Instance.endpoint_type` is additive/N-1-tolerant and rides the existing epoch lease (no new channel/merge key); advertise stamps the perch's own `info.state`; `receive_wan` resolves via `instances_of(from)` filtered to `i.node == origin_node` (proven node, never wire `from`), checking `is_gateway` only — no smuggled extra gate, no user-surface set. The matrix nails the anti-spoof rows (gateway on a different node than proven → re-stamped). Production caller threads the live registry + proven origin. The one open item is T5 (twohost honored-path int) being skipped — I'll accept it for M9 (component coverage is solid and the security-critical *negative* path is E2E-tested) but log it as required-before-lecturn, since the positive honored path has no production exerciser until a real cross-node Gateway exists.


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


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


1
2	running 1 test
3	.
4	test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 280 filtered out; finished in 0.00s
5
6	MATRIX_EXIT:0
7

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

MATRIX_EXIT:0


Matrix test green. Waiting on the wanmsg integration + xtask check (second task) before the stamp.


1
2	running 3 tests
3	...
4	test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.22s
5
6	WANMSG_EXIT:0
7

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

WANMSG_EXIT:0


<task-notification>
<task-id>b8hm7bvq8</task-id>
<tool-use-id>toolu_01E52tymRv5ra1Ym9wBRcbYS</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\13c96a50-2b78-4c16-a995-1da2b1836fd8\tasks\b8hm7bvq8.output</output-file>
<status>completed</status>
<summary>Background command "wanmsg integration + xtask check" completed (exit code 0)</summary>
</task-notification>


wanmsg 3/3 green; xtask check still finishing (chained after). Waiting for the task to complete before stamping.


1
2	running 3 tests
3	...
4	test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.22s
5
6	WANMSG_EXIT:0
7	xtask check: OK
8	XTASK_EXIT:0
9

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

WANMSG_EXIT:0
xtask check: OK
XTASK_EXIT:0


Prompt:
<task-notification>
<task-id>b8hm7bvq8</task-id>
<tool-use-id>toolu_01E52tymRv5ra1Ym9wBRcbYS</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\13c96a50-2b78-4c16-a995-1da2b1836fd8\tasks\b8hm7bvq8.output</output-file>
<status>completed</status>
<summary>Background command "wanmsg integration + xtask check" completed (exit code 0)</summary>
</task-notification>


All green — matrix 1/1, wanmsg 3/3, xtask OK. Gateway-WAN gate passes. Stamping it + logging the one tracked condition.


$OWL send todlando <<'EOF'
═══ GATEWAY-WAN GATE: PASS ✓ ═══ (PR #9, REQ-MSG-6 — cross-node Gateway user-msg)

Reviewed the impl whole + re-ran verification. Clean, posture-faithful. Gate cleared.

VERIFIED (read the code, not the summary):
• Instance.endpoint_type (registry.rs): additive serde-default + skip_serializing_if, rides the existing epoch lease + RegistryUpdate replication, NO new merge key. N-1 row → None → fail-closed re-stamp. Extends REQ-INST-7 model correctly.
• advertise (registryhost.rs): stamps the perch's OWN info.state tag; recordless perch → None → fail-closed. Node advertises only its own endpoints' types. Unit asserts ready_agent stamps through.
• origin_user_backed (wan.rs) — POSTURE HELD exactly: keys on i.node == origin_node (QUIC-PROVEN, never wire `from`), checks is_gateway ONLY. No extra gate, no user-surface set, no attestation. The doc comment explicitly states it's advertised-type resolution NOT a forgery defense (subnet=trust boundary).
• Matrix (origin_user_backed_matrix): thorough — gateway-on-proven→honor; agent/other→re-stamp; no-type(N-1)→re-stamp; gateway-on-DIFFERENT-node→re-stamp (the anti-spoof row — wire `from` can't borrow another node's gateway type); proven-node-agent + other-node-gateway→re-stamp; proven-node-gateway-among-others→honor; empty→re-stamp. Nails it.
• Production wiring: serve_wan_feed threads the LIVE node registry + the proven origin (dispatch.rs) — not a test stub. Confirmed.

INDEPENDENT RE-RUN: origin_user_backed_matrix 1/0 · wanmsg 3/0 · xtask check OK. (Trusting your full-sweep/clippy/no-default-features green on top.)

ONE TRACKED CONDITION (non-blocking for M9 — log it, don't hold the gate):
T5 twohost honored-path int skipped — ACCEPTED for M9. Rationale: the security-critical NEGATIVE path (unproven origin → re-stamp) IS E2E-tested (wanmsg funnel); the resolve logic is matrix-covered; each link (advertise-stamp, Instance-replicate, instances_of, receive_wan-thread) is unit-tested; and the POSITIVE honored path has no production exerciser until a real cross-node Gateway exists (pre-lecturn = zero traffic). BUT: add the twohost honored-path int (gateway advert on A → user-msg HONORED at B; agent advert → re-stamped) BEFORE spt-lecturn ships a real cross-node Gateway — so the honored wire path is proven before anything relies on it. Log it on REQ-MSG-6 / ROADMAP as required-before-lecturn so it's not lost.

Gate cleared. PR #9 merge-ready (operator/release-gated like the rest).

M9 STATUS after this: G1 closed (Wave 1), G2 closed (Wave 2), Gateway-WAN closed. Remaining M9 = Wave 3 / G3 (digest log-projection + REQ-TERM-4 re-point same-commit; flag history-grain fallback if taken; zero PTY-parse remnants; CC-fixture digest E2E; docs sweep) — whenever you pick it up. Strong run across all three.
EOF

Stamp Gateway-WAN gate PASS


➜ SENT:todlando


Write

# Commune — G2 closed + Gateway-WAN gate PASSED

## Status delta
Gen 19. THREE M9 gates now closed: G1 (Wave 1 adapter-custom), G2 (Wave 2 user-surfaces), Gateway-WAN (cross-node user-msg). Only Wave 3 / G3 remains in M9.

## Closed this turn
- **G2 FULLY CLOSED** (PR #8). CI green both runners confirmed; earlier Win RED was hfenduleam disk-full (396GB dev-target bloat, LNK1318 — NOT code), cargo clean + re-run pass, reconcile 77e0fb1 unaffected. Substance PASS + verified blocker fix → stamped closed.
- **GATEWAY-WAN GATE PASSED** (PR #9, REQ-MSG-6). Reviewed impl whole + re-ran: matrix 1/0, wanmsg 3/0, xtask OK. Posture HELD (origin_user_backed keys on proven origin_node not wire from, is_gateway only, no extra gate/no user-surface set). Instance.endpoint_type additive/N-1, advertise stamps own info.state, production serve_wan_feed threads live registry+proven origin. Matrix nails anti-spoof rows.

## TRACKED CONDITION (non-blocking, logged)
T5 twohost honored-path int SKIPPED — accepted for M9 (negative/security path E2E-tested via wanmsg funnel; positive path no production exerciser pre-lecturn). REQUIRED-BEFORE-LECTURN: add twohost honored-path int (gateway advert A → honored B) before a real cross-node Gateway relies on it. Told todlando to log on REQ-MSG-6/ROADMAP. VERIFY this got logged when reviewing Wave 3 or next gateway-wan touch.

## owl gotcha learned
owl `send` HANGS if body passed as positional FROM arg — must pipe body via stdin (`$OWL send <target> <<'EOF'...`). todlando hit this; messages stalled. (My own sends already use stdin heredoc — keep doing that.)

## env gotcha
hfenduleam (this machine) dev target bloated to 396GB / 2GB free → LNK1318/os-error-112 CI RED mis-readable as code regression. Periodic cargo clean on runners. Flagged to todlando.

## RESUME HERE
- All three M9 gates closed. Three PRs merge-ready, operator/release-gated: #7 (Wave1), #8 (Wave2), #9 (gateway-wan). Stacking: m9-adapter-custom ← m9-user-surfaces ← m9-gateway-wan.
- LAST M9 piece = Wave 3 / G3 (digest log-projection engine + api digest-entry → PTY-parse RETIRE + REQ-TERM-4 retitle/re-point SAME COMMIT — never momentarily un-evidenced). Watch: history-grain fallback pre-authorized but MUST be flagged if taken. Zero PTY-parse remnants; CC-fixture digest E2E; docs sweep. Armed when todlando starts it.

CONTEXT @ HEAD = normative; deviation = STOP-and-ping. No tag/release rides M9.


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)

# Commune — G2 closed + Gateway-WAN gate PASSED

## Status delta
Gen 19. THREE M9 gates now closed: G1 (Wave 1 adapter-custom), G2 (Wave 2 user-surfaces), Gateway-WAN (cross-node user-msg). Only Wave 3 / G3 remains in M9.

## Closed this turn
- **G2 FULLY CLOSED** (PR #8). CI green both runners confirmed; earlier Win RED was hfenduleam disk-full (396GB dev-target bloat, LNK1318 — NOT code), cargo clean + re-run pass, reconcile 77e0fb1 unaffected. Substance PASS + verified blocker fix → stamped closed.
- **GATEWAY-WAN GATE PASSED** (PR #9, REQ-MSG-6). Reviewed impl whole + re-ran: matrix 1/0, wanmsg 3/0, xtask OK. Posture HELD (origin_user_backed keys on proven origin_node not wire from, is_gateway only, no extra gate/no user-surface set). Instance.endpoint_type additive/N-1, advertise stamps own info.state, production serve_wan_feed threads live registry+proven origin. Matrix nails anti-spoof rows.

## TRACKED CONDITION (non-blocking, logged)
T5 twohost honored-path int SKIPPED — accepted for M9 (negative/security path E2E-tested via wanmsg funnel; positive path no production exerciser pre-lecturn). REQUIRED-BEFORE-LECTURN: add twohost honored-path int (gateway advert A → honored B) before a real cross-node Gateway relies on it. Told todlando to log on REQ-MSG-6/ROADMAP. VERIFY this got logged when reviewing Wave 3 or next gateway-wan touch.

## owl gotcha learned
owl `send` HANGS if body passed as positional FROM arg — must pipe body via stdin (`$OWL send <target> <<'EOF'...`). todlando hit this; messages stalled. (My own sends already use stdin heredoc — keep doing that.)

## env gotcha
hfenduleam (this machine) dev target bloated to 396GB / 2GB free → LNK1318/os-error-112 CI RED mis-readable as code regression. Periodic cargo clean on runners. Flagged to todlando.

## RESUME HERE
- All three M9 gates closed. Three PRs merge-ready, operator/release-gated: #7 (Wave1), #8 (Wave2), #9 (gateway-wan). Stacking: m9-adapter-custom ← m9-user-surfaces ← m9-gateway-wan.
- LAST M9 piece = Wave 3 / G3 (digest log-projection engine + api digest-entry → PTY-parse RETIRE + REQ-TERM-4 retitle/re-point SAME COMMIT — never momentarily un-evidenced). Watch: history-grain fallback pre-authorized but MUST be flagged if taken. Zero PTY-parse remnants; CC-fixture digest E2E; docs sweep. Armed when todlando starts it.

CONTEXT @ HEAD = normative; deviation = STOP-and-ping. No tag/release rides M9.


Both gates resolved. Saving the disk-bloat gotcha to memory — it could cause a future CI-RED misdiagnosis across sessions.


---
name: hfenduleam-target-bloat-ci-red
description: hfenduleam dev target bloat can fill disk → LNK1318 CI RED that mimics a code regression
metadata:
  type: project
---

On hfenduleam (Windows CI runner + doyle's box), the cargo `target/` dir can bloat huge (seen: 396GB, leaving ~2GB free). When the disk fills, the Windows linker fails with **LNK1318 / os-error-112** — which surfaces as a **CI RED that looks like a code regression but isn't**. Fix: `cargo clean` on the runner, re-run → passes.

**Why:** happened 2026-06-13 — M9 Wave 2 PR #8 went RED on the Windows runner; root cause was disk-full, not code (reconcile fix 77e0fb1 was unaffected; re-ran green after clean).

**How to apply:** before treating a Windows-runner RED (especially LNK1318/os-error-112) as a code defect, check free disk / target size; a periodic `cargo clean` on the self-hosted runners prevents it. Don't restart/revert code on this signal alone. Related: [[ci-selfhost-migration]] (runners live on kitsubito + hfenduleam).


File created successfully at: C:\Users\decid\.ccs\instances\bigscreen\projects\C--Users-decid-Documents-projects-spt-core\memory\hfenduleam-target-bloat-ci-red.md (file state is current in your context — no need to Read it back)

---
name: hfenduleam-target-bloat-ci-red
description: hfenduleam dev target bloat can fill disk → LNK1318 CI RED that mimics a code regression
metadata:
  node_type: memory
  type: project
  originSessionId: e351ca96-895b-4af5-b548-a9905d84fcd0
---

On hfenduleam (Windows CI runner + doyle's box), the cargo `target/` dir can bloat huge (seen: 396GB, leaving ~2GB free). When the disk fills, the Windows linker fails with **LNK1318 / os-error-112** — which surfaces as a **CI RED that looks like a code regression but isn't**. Fix: `cargo clean` on the runner, re-run → passes.

**Why:** happened 2026-06-13 — M9 Wave 2 PR #8 went RED on the Windows runner; root cause was disk-full, not code (reconcile fix 77e0fb1 was unaffected; re-ran green after clean).

**How to apply:** before treating a Windows-runner RED (especially LNK1318/os-error-112) as a code defect, check free disk / target size; a periodic `cargo clean` on the self-hosted runners prevents it. Don't restart/revert code on this signal alone. Related: [[ci-selfhost-migration]] (runners live on kitsubito + hfenduleam).


1	# spt-core Roadmap
2
3	> Lightweight-but-structured path from design → fruition. Not a GSD/phase-gated process — a clear sequence with de-risk gates. Planning corpus is complete (PRD + CONTEXT + 5 ADRs + 6 design docs); this is how we build it.
4
5	## End goal
6
7	A harness-independent core for an agent ecosystem — messaging, live-agent lifecycle, terminal hosting, **first-class P2P networking**, and a runtime-manifest harness contract — shipped as Rust crates + a single `spt` binary. The proof: the same agent runs natively across all of a user's machines with a synced mind, reachable and operable from anywhere, with seamless self-update and zero-config cross-machine messaging. First thing built atop it: a rebuilt `spt` plugin at parity with today's modern SPT.
8
9	## Scope decisions (locked)
10
11	- **v1 = the full PRD, networking included.** The networked element is the *reason* spt-core exists; it is not deferrable to v1.1.
12	- **The hard freeze on `claude_skill_owl` is not a clock.** It works today and delivers value as-is while spt-core is built — no schedule pressure from it.
13	- **Process: lightweight but structured.** GSD is too heavy. This roadmap + the design docs are the structure.
14
15	## Path to fruition (sequence)
16
17	**Stage A — Red-team review (before building).** Pressure-test the design adversarially. Focus on the riskiest architectural decisions, not bikeshedding. Tools: `plan-eng-review` (architecture) + `codex` (adversarial second opinion). Scope-cutting is *not* a goal (v1 is locked as full PRD), but surfacing contradictions / over-engineering / unproven assumptions is.
18
19	**Stage B — De-risk spikes.** Throwaway proofs of the load-bearing unknowns — anything that, if it fails, forces a redesign. Do these before committing to the full build. See *De-risk spikes* below.
20
21	**Stage C — Walking skeleton.** Build the killer-quickstart slice end-to-end through the real crate boundaries: **two agents exchange a message** (first local; ultimately cross-node — the real proof). Validates the whole stack early; then thicken.
22
23	**Stage D — Phased build (M0–M5).** Dependency-ordered, each milestone ships something usable.
24
25	## Milestones (all v1)
26
27	- **M0 — Workspace + commodity layer.** Cargo workspace; `spt-proto` (wire envelope grammar, endpoint types, framing, Ed25519 identity, typed+binary payloads) + `spt-store` (spool, perch layout, trust store, registry persistence). Copy-verbatim the stable formats from the sister project (ADR-0001).
28	- **M1 — Local messaging + binary.** `spt-msg` (delivery TCP+spool, routing, send/ring/ready) + the `spt` binary + CLI. **= the killer quickstart (local): two agents exchange a message.** Task breakdown: [`M1-PLAN.md`](./M1-PLAN.md).
29	- **M2 — Harness contract + lifecycle.** **Strictly harness-agnostic — zero Claude Code conventions in this repo.** spt-core defines the adapter contract; the only adapter-shaped artifact in-tree is a generic mock/test adapter (manifest fixture + fake `api` caller) that exercises the contract (PRD R-DOCS-2 dev-agent quickstart). **The rebuilt `spt` plugin (Claude Code) is a SEPARATE downstream project** built atop these crates/binary — spt-core v1's acceptance proof (PRD §"feature parity"), not an spt-core deliverable. It lives and builds outside this repo. **Split into two JIT milestones** (decision 2026-06-01):
30	  - **M2a — Harness contract. ✅ delivered (2026-06-01).** `spt-runtime` (AgentRuntime, ManifestRuntime, manifest schema) + the `api` subcommand surface (seed/listen/bind/state/echo-gate/poll/worker-*/boundary/session-end/presence/history-log/emit/capability) + the **harness-hosted** startup topology (seed→listen→bind, interim seed-file) + local-api-auth + the generic mock adapter + contract E2E. All M2a reqs activated + green. Task breakdown: [`M2a-PLAN.md`](./M2a-PLAN.md).
31	  - **M2b — Live-agent lifecycle. ✅ delivered (2026-06-01).** `spt-live` (fifth layer): history subsystem (fetcher/locate-normalize/native) + spawn-psyche (nested perch, spt-core-owned `$psyche_prompt`) + echo-commune (history brief → single-writer commune drop) + drop-file ingest (single-writer 6.4 + direct-write precedence 6.5) + interim pulse + signoff/boundary ordering invariants (echo-before-signoff 3.3, grace-before-signoff 1.1, stale-signoff sweep 3.2) + resume seam (continue-existing / fresh-with-preload). Hosted by the `api listen` runner + `api shutdown`; mock adapter gained the lifecycle templates; LiveAgent E2E proves spawn→Psyche-perch→commune-ingest→graceful-signoff harness-agnostically. All M2b reqs activated + green. Task breakdown: [`M2b-PLAN.md`](./M2b-PLAN.md).
32	  - **Interim, no daemon** (like M1): the `api listen` process holds the perch + relays; the seed bridges via a file. **The spt-hosted topology (PTY-launch) + the consolidated `spt-daemon` move to M3** (they need the `spt-term` broker) — this narrows the earlier "both topologies in M2" wording.
33	- **M3 — Terminal wrapper + daemon + self-update.** `spt-term` (session-surface, PTY, broker) + the consolidated `spt-daemon` (broker/brain split) + peer-propagated signed self-update. Split M3a/M3b/M3c. Scope + carried-forward Spike #1 gaps: [`M3-PLAN.md`](./M3-PLAN.md).
34	  - **M3a — `spt-term` session-surface mechanism. ✅ delivered (2026-06-02).** The sixth crate (`…→spt-msg→spt-term`, sibling of spt-runtime): the `SessionSurface` trait + native PTY backends (ConPTY/forkpty via `portable-pty`, REQ-TERM-1) · ConPTY-DSR auto-answer drain pump (REQ-HAZARD-CONPTY-DSR) · send-keys/send-line injection (REQ-TERM-2) · bounded byte-stream (REQ-TERM-3) · the PTY-digest parser primitive (REQ-TERM-4 impl/unit, ADR-0008). OS-neutral per Spike #4/#5 (no OS's stream contract baked in; ANSI-stripped, tail-wins repaint tolerance). Validated on Windows (ConPTY) + Linux (forkpty). All M3a reqs activated + green. Task breakdown: [`M3a-PLAN.md`](./M3a-PLAN.md).
35	  - **M3b — `spt-daemon` broker/brain process. ✅ delivered (2026-06-03).** The seventh crate (`…→spt-live→spt-daemon→spt`) hosts many `spt-term` surfaces behind a **versioned local IPC** (named-pipe/UDS behind one `DaemonTransport` trait, forward-compatible frame schema): a **broker** (stable kernel — PTY masters, harness children, sockets; no logic) + a **brain** (restartable logic) so a brain kill/restart leaves the hosted PTY child + its output stream **intact and gapless** (Spike #1 made real; `gen_start = now()` each handoff). Consolidates the interim no-daemon model — the `api listen` pulse/Psyche/drop-ingest loops fold into the brain as config-paced in-process loops (only a thin stateless relay residue) · daemon-authoritative liveness (`info.json` `status` supersedes `is_process_alive` for hosted perches) · exactly-once broker↔brain boundary (broker-owned `EffectJournal` + dedup-at-effect, Spike #6) · auto-start + in-memory seed (the seed file is dead) · supervised-crash orphan-watch (carries the M2b graceful ordering) · the **digest daemon-half** (manifest `pty_digest` seam → `DigestParser` over the broker PTY, `spt digest <id>` pull + delta-stream + opt-in Path-B persistence → REQ-TERM-4 `int`). All M3b reqs activated + green; daemon E2E (spawn→Psyche-loop→commune→brain-restart-survives→graceful-signoff) on the CI matrix (ubuntu + windows). Task breakdown: [`M3b-PLAN.md`](./M3b-PLAN.md).

... [48 lines truncated] ...


Now the MEMORY.md pointer for the new memory.


1	- [M9 Wave 1 progress](m9-wave1-progress.md) — EXECUTING M9 adapter-customization; branch m9-adapter-custom. **T1 COMPLETE** (10 green commits): profile engine + all 12 composite-addressing consumer seams + CLI create/delete-profile + list render. Workspace test sweep green. REMAINING before G1: T2 strings (REQ-MANIFEST-3), T3 hints (REQ-MANIFEST-4); then ping doyle G1
2	- [v0.4.1 Linux respawn blocker](v041-linux-respawn-blocker.md) — v0.4.1 PUBLIC (counter 8); fleet roll found Linux brain-respawn bug (current_exe follows apply-rename → brain runs OLD bytes, records applied:8). hfenduleam(Win) GREEN, kitsubito(Linux) RED. RESOLVED by v0.4.2 — see [[v042-published]]
3	- [v0.4.2 published](v042-published.md) — PUBLIC 2026-06-11 (counter 9); FIXES v0.4.1 Linux respawn blocker (respawn via canonical install path not current_exe + bytes-assert→auto-rollback; REQ-HAZARD-BRAIN-RESPAWN-PATH + real-rename E2E). deployah drove full merge→tag→sign→publish solo. hashes linux 500a9e7b… win b9ec8ddd…. ASYMMETRY: v0.4.2 apply still rides buggy v0.4.0 broker → 1 final manual Linux bounce loads fix; seamless proof on NEXT update via debug-channel. todlando driving fleet roll + restoration close
4	- [pump-IPC-deadline fix](pump-ipc-deadline-fix.md) — DONE: MERGED to main @ 4d8b810 (PR #3), doyle-approved, CI green both runners; reader-thread+channel carrier (nonblock dead on Win named pipes), both amendments folded, no tag (rides v0.4.1). Don't re-plan

- [M9 Wave 1 progress](m9-wave1-progress.md) — EXECUTING M9 adapter-customization; branch m9-adapter-custom. **T1 COMPLETE** (10 green commits): profile engine + all 12 composite-addressing consumer seams + CLI create/delete-profile + list render. Workspace test sweep green. REMAINING before G1: T2 strings (REQ-MANIFEST-3), T3 hints (REQ-MANIFEST-4); then ping doyle G1
- [v0.4.1 Linux respawn blocker](v041-linux-respawn-blocker.md) — v0.4.1 PUBLIC (counter 8); fleet roll found Linux brain-respawn bug (current_exe follows apply-rename → brain runs OLD bytes, records applied:8). hfenduleam(Win) GREEN, kitsubito(Linux) RED. RESOLVED by v0.4.2 — see [[v042-published]]
- [v0.4.2 published](v042-published.md) — PUBLIC 2026-06-11 (counter 9); FIXES v0.4.1 Linux respawn blocker (respawn via canonical install path not current_exe + bytes-assert→auto-rollback; REQ-HAZARD-BRAIN-RESPAWN-PATH + real-rename E2E). deployah drove full merge→tag→sign→publish solo. hashes linux 500a9e7b… win b9ec8ddd…. ASYMMETRY: v0.4.2 apply still rides buggy v0.4.0 broker → 1 final manual Linux bounce loads fix; seamless proof on NEXT update via debug-channel. todlando driving fleet roll + restoration close
- [pump-IPC-deadline fix](pump-ipc-deadline-fix.md) — DONE: MERGED to main @ 4d8b810 (PR #3), doyle-approved, CI green both runners; reader-thread+channel carrier (nonblock dead on Win named pipes), both amendments folded, no tag (rides v0.4.1). Don't re-plan


The existing "M9 Wave 1 progress" memory is now stale (says G1 pending). Updating it to current full-M9 status and adding the new pointer.


---
name: m9-wave1-progress
description: M9 status — three of four gates closed (G1/G2/Gateway-WAN); Wave 3/G3 remains
metadata:
  type: project
---

M9 = adapter customization + session surfaces. Executor todlando, reviewer doyle. Stacking branches, merge-to-main operator/release-gated (no tag rides M9).

**Gates closed (2026-06-12 → 06-13):**
- **G1** — Wave 1 `m9-adapter-custom` (PR #7): adapter profiles + strings + hints. Doc-pin b189513. CI green.
- **G2** — Wave 2 `m9-user-surfaces` (PR #8): user-msg identity gate + Gateway acceptance + live-role.md. Blocker (reconcile must STRUCTURALLY exclude live-role.md) fixed 77e0fb1 + verified. CI green (a Win RED was hfenduleam disk-full, not code — see [[hfenduleam-target-bloat-ci-red]]).
- **Gateway-WAN** — `m9-gateway-wan` (PR #9, REQ-MSG-6): cross-node Gateway user-msg. `Instance.endpoint_type` advert + `receive_wan` resolve keyed on QUIC-proven origin node (is_gateway only). Trust posture: subnet membership = trust boundary (operator-ratified; user-surface-set vetoed). Matrix + wanmsg + xtask green.

**Tracked condition:** T5 twohost honored-path int SKIPPED for M9 (negative/security path E2E-tested; positive path no production exerciser pre-lecturn). REQUIRED-BEFORE-LECTURN — verify logged on REQ-MSG-6/ROADMAP.

**Remaining = Wave 3 / G3:** digest log-projection engine + `api digest-entry` → RETIRE PTY-parse half + REQ-TERM-4 retitle/re-point in the SAME commit (never momentarily un-evidenced). Watch: history-grain fallback pre-authorized but MUST be flagged at G3 if taken. Zero PTY-parse remnants; CC-fixture digest E2E; docs sweep. Don't re-plan G1/G2/Gateway-WAN.


<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>

Error: File has not been read yet. Read it first before writing to it.


<owl_messages>
<owl_message from="todlando" priority="high">GATEWAY-WAN GATE PASS received — thank you for the deep read. Tracked condition LOGGED + pushed (commit 58f034f on PR #9, docs-only — toml + ROADMAP, impl you gated is unchanged): T5 twohost HONORED-path int marked REQUIRED-BEFORE-LECTURN on REQ-MSG-6 + ROADMAP open-threads ("activate int when adding it"; negative path already E2E via wanmsg funnel, positive path has no exerciser until a real cross-node Gateway). It will re-trigger CI (docs, traceable stays green). PR #9 now operator/release-gated for merge like the rest. M9: G1 ✓ G2 ✓ gateway-wan ✓. Standing by for the next beat or operator merge coordination.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


1	---
2	name: m9-wave1-progress
3	description: "M9 Wave 1 (adapter customization) execution state — todlando executes, doyle gates G1/G2/G3"
4	metadata:
5	  node_type: memory
6	  type: project
7	  originSessionId: cac905c4-0186-45cc-8462-8b34fe78904b
8	---
9
10	M9 "Adapter customization & session surfaces" EXECUTION (started 2026-06-12). doyle handed off (gen 19, G1 queue armed), todlando executes wave-by-wave, doyle gates. Plan = `M9-PLAN.md` @ d338c2d; spec = `CONTEXT.md` @ bd469b9 (NORMATIVE — deviations = STOP-and-ping doyle, never code-around). 3 waves, branch per wave → PR → CI both runners → doyle gate → merge. No tag rides M9.
11
12	**>>> CURRENT (2026-06-13): WAVE 2 (`m9-user-surfaces`) — T4+T5+T6 COMPLETE + G2 doyle verdict PASS w/1-blocker which is now FIXED. PR #8 OPEN (base m9-adapter-custom, 12 commits), CI running both runners. Awaiting CI green → doyle final G2 sign-off. NEXT after G2: Wave 3 (T7 digest log-projection + REQ-TERM-4 re-point) = G3. PR #8 = CI/gate vehicle, NOT merged (release-only). <<<**
13
14	**G2 doyle verdict (2026-06-13): PASS, 1 blocking condition → FIXED (77e0fb1).** doyle's finding: my survey's "reconcile structurally cannot touch live-role.md" was contradicted by sync.rs reconcile_after_sync enumerating report.conflicts() dynamically + reconcile_file file-agnostic → held by RARITY not structure. FIX (his option a): reconcile driver loop now SKIPS file==LIVE_ROLE_FILE → new ReconcileOutcome::RoleExcluded before reconcile_file called; merge-driver still mints artifact (preserved), user resolves via spt endpoint role. Now LITERAL. Unit reconcile_driver_structurally_excludes_live_role (failer runtime proves reconcile_file never invoked). Survey + forward-note corrected. doyle verified all 5 artifacts + re-ran (spt-proto 59/0, store+live 152/0, wanmsg 3/0, gateway_e2e 1/0). PR base=m9-adapter-custom per doyle.
15
16	**T6 live-role.md COMPLETE — 3 green commits (REQ-EP-7 [doc,impl,unit]):**
17	- 992cc66 feat(store): contextstore LIVE_ROLE_FILE/live_role_file/live_role_path (tracked/agents/<id>/live-role.md, same a-<id> branch as live-context) + resume::download_psyche_context renders role FIRST (role→live→project) + injection-order test + behavioral no-automated-writer test (smuggled <live-role> commune slice → role byte-unchanged; parse_two_slice has no role slice).
18	- 51ddc37 feat(cli): `spt endpoint role` (print) / `--overwrite <file>` (SOLE writer; atomic_write + commit_live on a-<id>). Mirrors endpoint description. CLI reference regen.
19	- 1dbb39e docs(m9): M9-WAVE2-ROLE-WRITERS.md (root, like Wave-1 survey) — every tracked/ writer classified, sole-writer proof, sync=transport-not-authoring distinction.
20
21	**WAVE 2 FULL G2 EVIDENCE MAP (sent doyle):** N-1 fixture (b1905da) · identity truth table+spoof (b1905da) · WAN fail-closed funnel test=doyle cond1 (2d90ba2) · injection order (992cc66) · no-automated-writer behavioral+survey=992cc66/1dbb39e · gateway bind/owns-shell/E2E (f96d5e2/d42550f/d8cd929) · residual logged=doyle cond2 (f96d5e2). doyle G2 ruling (2026-06-13) already confirmed local-Gateway+fail-closed-WAN acceptable; cross-node registry endpoint_type = deferred LATER beat.
22
23	**T5 Gateway acceptance COMPLETE — 4 green commits (REQ-EP-6 [doc,impl,unit]+int; full sweep 0 fail):**
24	- f96d5e2 feat(api): `api bind --type <tag>` un-hardcode (establish_perch was hardcoding state="live_agent"). Gateway binds state="gateway"; revive carry-forward; relay/listen stays live_agent. (THE only agent-family hardcode — survey: shellhost/shellwake/linkhost owner-validation already type-agnostic.)
25	- 2d90ba2 test(daemon): WAN fail-closed at the receive_wan FUNNEL (doyle G2 cond 1) — forged user-msg from unproven origin → spooled body is plain msg.
26	- d8cd929 feat(test): mock-gateway fixture (adapters/mock-gateway/manifest.toml hostable_types=["Gateway"], reuses mock-session +--type/--mock-adapter passthrough, NO lecturn code) + gateway_e2e.rs: binds state=gateway, user-msg FROM gateway HONORED, FROM live_agent RE-STAMPED — full gate through real spt binary.
27	- d42550f test(daemon): gateway-typed owner spawns+owns a shell (owner = opaque namespace string, no type check).
28
29	**doyle G2 ruling (2026-06-13, both confirmed):** local-only Gateway user-backed origin + fail-closed WAN = ACCEPTABLE for M9 (literal CONTEXT posture). Cross-node registry endpoint_type trust = correctly a LATER beat (logged: ROADMAP open-threads + CONTEXT §Gateway "_Implemented posture_" note). 2 conditions DONE: (1) WAN funnel fail-closed test ✓, (2) residual logged ✓.
30
31	**T6 NEXT (REQ-EP-7) — live-role.md:** lives tracked/agents/<id>/live-role.md beside live-context.md (contextstore seam). Start-transition injection renders role→live-context→project (assembly = resume::download_psyche_context). `spt endpoint role` print / `--overwrite <file>` = SOLE writer. Role-writers SURVEY DONE (zero automated writers reach it — echo-ingest/signoff/reconcile target specific live-context.md/project-context.md/.conflicts; sync-merge replicates = transport not authoring). TODO: write M9-WAVE2-ROLE-WRITERS.md (front-load #1) + injection-order test (front-load #4) + no-automated-writer test. Then G2 ping doyle.
32
33	**T4 user-msg COMPLETE — 3 green commits (all per-commit gates green; full workspace sweep 0 fail):**
34	- b1905da feat(proto): EVENT_TYPE_USER_MSG + compose_user_msg_event + the identity-gate CORE (MsgOrigin{LocalUserCli,Endpoint(EndpointType)}, GATEWAY_TAG="gateway" open-type, is_gateway_endpoint/is_user_backed_origin/gate_user_msg_type — co-located w/ is_psyche_authorable_type). Truth table (gateway/CLI pass · agent+node+broker+shell+foreign re-stamp · non-request=msg) + SPOOF row + **N-1 tolerance fixture** (front-load #3) + round-trip. REQ-MSG-5 minted [doc,impl,unit], doc=CONTEXT §Gateway user-msg L209 tagged.
35	- 7d60af9 feat(cli): `spt send --user-msg` LOCAL origination. Gate keys on session-proven `roster::detect_self_id` (NOT --from label); classify_local_origin reads perch info.json `state`→EndpointType; apply_user_msg_gate composes user-msg envelope (honored) / plain body + loud USER_MSG_RESTAMP (agent). Honored form = verbatim typed-envelope body (notify/echo pattern → NO schema change, renders <EVENT type="user-msg">). Threads into local+WAN legs. Empty never composes.
36	- 0207ba6 feat(daemon): WAN-ingress re-stamp (KH 7.5 cross-node). receive_wan restamp_wan_user_msg(body, origin_user_backed) parse→recompose user-msg→msg unless origin user-backed; **T4 fail-closes origin_user_backed=false** (re-stamps ALL WAN user-msg). T5 resolves Gateway origin from registry → threads true (the SEAM is ready).
37
38	**T4 design notes (for T5/review):** type travels as verbatim typed-envelope BODY (no WanMessage/spool/render schema change). Local LocalUserCli trusted (single-box posture; local agent-shells-CLI accepted per existing trust boundary). WAN: only registry-resolvable Gateway origin provably user-backed → T5 opens it. "delivery surfaces render type" = existing render_event_lines verbatim pass-through (is_typed_event_envelope), no extra code.
39
40	**T5 NEXT (REQ-EP-6):** Gateway-typed perch binds(api bind)/advertises/addressable/owns-shells (un-hardcode agent-family in `shellhost.rs` owner validation — must SPAWN today)/subscribes-digests/IS T4 gate's user-backed WAN origin. In-tree MOCK gateway fixture (manifest+fake binary, R-DOCS-2 pattern, NO lecturn code). **Thread origin_user_backed into receive_wan** = the T4→T5 join (resolve from-id@origin-node → Gateway in registry). G2 front-load #5 = type-acceptance table.
41	**T6 (REQ-EP-7):** live-role.md. Role-writers SURVEY DONE (agent, 2026-06-13): ZERO automated writers reach tracked/live-role.md — echo-ingest/signoff/reconcile all target specific live-context.md/project-context.md/.conflicts/<f>.<h>.md; sync-merge replicates (transport, not authoring). Still need to WRITE M9-WAVE2-ROLE-WRITERS.md (front-load #1). T6 note: live-role.md excluded from reconcile authoring; injection order role→live→project.
42
43	**Branch `m9-adapter-custom` (Wave 1) — landed so far (all per-commit gates green: build/test/clippy -D/--no-default-features/traceable EXIT=0/xtask check):**
44	- 4129b81 — leaf-replace merge pure fn + contract table (`spt-runtime/src/profile.rs`)
45	- 5441c2c — shipped-profile parsing (`[profiles.<name>]` raw toml::Value on Manifest) + `profile::resolve` + tighten-only floor (require_approval ladder none<remembered<always + over_cap reject>approve; max_instances NOT floored). Schema re-blessed (`SPT_BLESS=1 cargo test -p spt-runtime`).
46	- 3b701a7 — `M9-WAVE1-ADDRESSING-SURVEY.md` = G1 artifact, the adapter_name consumer map.
47	- 51f37bc — local profiles (`adapters/<a>/profiles/<p>.toml`, survive re-add) + `create_local_profile`/`delete_local_profile` (shadow-refusal, floor-at-create, name validation) + `resolve_option(<adapter>[:<profile>])` = THE composite-addressing entry point (bare=parent short-circuit, shipped-first then local) + `profile::split_option`/`resolve_overlay`. **spt-runtime profile ENGINE now complete + durable.**
48
49	**REQ:** REQ-MANIFEST-2 minted+activated [doc,impl,unit] (doc=CONTEXT.md §adapter profile, tagged). Later mints surveyed: REQ-MANIFEST-3 (strings), -4 (hints); REQ-MSG-5 (user-msg); REQ-EP-6 (Gateway), -7 (live-role); REQ-TERM-4 @ toml line 386 = Wave 3 re-point.
50
51	**T1 COMPLETE (2026-06-12) — 6 more green commits on `m9-adapter-custom`:**
52	- 3abd4df — `resolve_option_in(registered, adapters_dir, option)` slice-helper for daemon seams + extracted `apply_option_overlay` core (shared w/ `resolve_option`).
53	- ba54595 — daemon seams (#1/#2/#3/#10): shellhost+shellwake `shell_section_of` now return OWNED Shell via `resolve_option_in` (profile = fresh merged manifest, not a borrow); `cascade_owner_edge`/`reconcile_once` thread `adapters_dir`; linkhost `resolve_link_target` via `resolve_option`. `{adapter_name}` template subst carries composite (#10).
54	- 4d208a4 — store identity (#7/#8/#9): `mint_shell_id` splits to PARENT segment → id stays colon-free `<parent>-<n>` (REQ-HAZARD-ID-CHARSET: `:` is reserved delim), composite rides ONLY in `adapter_name`. Profiles of one parent share ordinal space. Split inlined (spt-store can't dep spt-runtime — graph runs other way).
55	- e26cd8e — CLI (#6/#11): spawn/resolve_driveable/teardown via `resolve_option`; cap filter compares PARENT segments (`split_option`); **+ `resolve_option` now honors `active` flag** (soft-deregistered refuses new resolution — CLI got this free via `registered()` before).
56	- 8b9c52e — api (#4/#5/#12): `load_manifest` matches PARENT segment + overlays via new `resolve_option_onto(parent, adapters_dir, option)` (runtime-manifest seam: parent from `--manifest` file, shipped from its `[profiles]`, local from adapters_dir); cmd_emit sensory via `resolve_option`.
57	- cdaf8e4 — CLI verbs: `adapter create-profile <a> <name> [--from f|stdin]`/`delete-profile` (local-only) + `list` renders shipped+local profiles adjacent. Docs regenerated (drift gate).
58
59	Full workspace test sweep GREEN (8 crates libs + 111 spt bin unit tests, 0 fail). All per-commit gates green.
60
61	**WAVE 1 COMPLETE (2026-06-12/13) — T2 + T3 landed:**
62	- de5841a — T2 strings (REQ-MANIFEST-3 [doc,impl,unit]): `[strings]` toml::Table on Manifest (rides resolve_overlay free) + `profile::string_at`/`set_string_at` + `registry::get_string`/`set_local_string` + CLI `get-string`/`set-string` (local-only, bare=usage err). Data-only. Schema re-blessed.
63	- 0f34daa — T3 hints (REQ-MANIFEST-4 [doc,impl,unit]): ordered `[[hints]]` {keywords,text,regex?} + `Hint::matched_keyword` (literal case-insens substring / regex opt-in, invalid-regex silent) + `perch::session_dir` + `api hint --session` (`reporting::select_and_mark_hint`: first matching UNSEEN hint declaration-order, durable seen-set under sessions/<id>/hints-seen, skip-seen→next match). regex dep added to spt-runtime. Schema re-blessed.
64
65	**WAVE 1 G1 = PASS + FULLY CLOSED (2026-06-13).** PR #7 (m9-adapter-custom→main), 11 commits, CI GREEN both runners (test Win/Linux + n1-gate ×2 + traceability ✓; twohost skipped, no [twohost] tag). doyle independently re-ran (spt-runtime 44/0, spt-store 152/0, xtask OK) + reviewed whole survey/engine/seams. Doc-pin condition folded (b189513: CONTEXT §keyword hints skip-seen tiebreak ratified). **MERGE POLICY (operator, 2026-06-13): merge to main ONLY for a public release.** So PR #7 does NOT merge per-wave; it's the CI/gate vehicle. M9 waves STACK on the branch line (Wave 2 branches off m9-adapter-custom HEAD, not main) and ride to main together at a release. "keep going" = proceed to Wave 2 without merging.
66
67	**WAVE 2 (`m9-user-surfaces`, after #7 merges) — doyle's G2 FRONT-LOAD list (produce artifacts ALONGSIDE code, evidence-based gate like G1 survey):**
68	- T4 user-msg (REQ-MSG-5) + T5 Gateway acceptance (REQ-EP-6) + T6 live-role.md (REQ-EP-7). T4 before T5 (Gateway fixture exercises the user-msg gate).
69	- **#1 (EARLIEST) M9-WAVE2-ROLE-WRITERS.md** — survey EVERY code path that can write `tracked/live-role.md`, classified: `spt endpoint role --overwrite` = the ONE legit writer vs everything else = ZERO automated writers (Psyche reconcile/echo-commune/signoff structurally exclude). THE no-automated-writer proof; missed writer = silent contract break (same class as missed addressing consumer).
70	- **#3 (EARLIEST) envelope-decode-order + N-1 fixture** (T4) — pin decode order explicitly (HTML-unescape + `<br>` split, `&` LAST = REQ-HAZARD-ENVELOPE-DECODE-ORDER); ship a CONCRETE N-1-sender fixture proving an older-version envelope still decodes (doyle wants fixture, not prose).
71	- #2 user-msg IDENTITY-GATE TRUTH TABLE (sender identity class × accept-as-user-authority/refuse, incl. spoofing rows) — rides T4 code.
72	- #4 role INJECTION-ORDER test — assert role→live-context→project ORDER (not just all-three-present).
73	- #5 Gateway TYPE-ACCEPTANCE — mock-gateway fixture (R-DOCS-2, no lecturn code) + truth table of types Gateway accepts/refuses.
74	- **CONTEXT@HEAD normative**: any user-msg authority-model / role-writer detail NOT already in CONTEXT = STOP-and-ping doyle before building (re-read CONTEXT §user-msg/§live-role/envelope-decode hazard first). REQ mints surveyed: REQ-MSG-5, REQ-EP-6, REQ-EP-7.
75
76	doyle's G1 checklist: merge unit table vs CONTEXT contract · tighten-only floor · the survey list whole · shadow-refusal · CLI shape · resolution-path proof at the 3 manifest seams (composite→parent→overlay, bare short-circuit).
77

---
name: m9-wave1-progress
description: "M9 Wave 1 (adapter customization) execution state — todlando executes, doyle gates G1/G2/G3"
metadata:
  node_type: memory
  type: project
  originSessionId: cac905c4-0186-45cc-8462-8b34fe78904b
---

M9 "Adapter customization & session surfaces" EXECUTION (started 2026-06-12). doyle handed off (gen 19, G1 queue armed), todlando executes wave-by-wave, doyle gates. Plan = `M9-PLAN.md` @ d338c2d; spec = `CONTEXT.md` @ bd469b9 (NORMATIVE — deviations = STOP-and-ping doyle, never code-around). 3 waves, branch per wave → PR → CI both runners → doyle gate → merge. No tag rides M9.

**>>> CURRENT (2026-06-13): WAVE 2 (`m9-user-surfaces`) — T4+T5+T6 COMPLETE + G2 doyle verdict PASS w/1-blocker which is now FIXED. PR #8 OPEN (base m9-adapter-custom, 12 commits), CI running both runners. Awaiting CI green → doyle final G2 sign-off. NEXT after G2: Wave 3 (T7 digest log-projection + REQ-TERM-4 re-point) = G3. PR #8 = CI/gate vehicle, NOT merged (release-only). <<<**

**G2 doyle verdict (2026-06-13): PASS, 1 blocking condition → FIXED (77e0fb1).** doyle's finding: my survey's "reconcile structurally cannot touch live-role.md" was contradicted by sync.rs reconcile_after_sync enumerating report.conflicts() dynamically + reconcile_file file-agnostic → held by RARITY not structure. FIX (his option a): reconcile driver loop now SKIPS file==LIVE_ROLE_FILE → new ReconcileOutcome::RoleExcluded before reconcile_file called; merge-driver still mints artifact (preserved), user resolves via spt endpoint role. Now LITERAL. Unit reconcile_driver_structurally_excludes_live_role (failer runtime proves reconcile_file never invoked). Survey + forward-note corrected. doyle verified all 5 artifacts + re-ran (spt-proto 59/0, store+live 152/0, wanmsg 3/0, gateway_e2e 1/0). PR base=m9-adapter-custom per doyle.

**T6 live-role.md COMPLETE — 3 green commits (REQ-EP-7 [doc,impl,unit]):**
- 992cc66 feat(store): contextstore LIVE_ROLE_FILE/live_role_file/live_role_path (tracked/agents/<id>/live-role.md, same a-<id> branch as live-context) + resume::download_psyche_context renders role FIRST (role→live→project) + injection-order test + behavioral no-automated-writer test (smuggled <live-role> commune slice → role byte-unchanged; parse_two_slice has no role slice).
- 51ddc37 feat(cli): `spt endpoint role` (print) / `--overwrite <file>` (SOLE writer; atomic_write + commit_live on a-<id>). Mirrors endpoint description. CLI reference regen.
- 1dbb39e docs(m9): M9-WAVE2-ROLE-WRITERS.md (root, like Wave-1 survey) — every tracked/ writer classified, sole-writer proof, sync=transport-not-authoring distinction.

**WAVE 2 FULL G2 EVIDENCE MAP (sent doyle):** N-1 fixture (b1905da) · identity truth table+spoof (b1905da) · WAN fail-closed funnel test=doyle cond1 (2d90ba2) · injection order (992cc66) · no-automated-writer behavioral+survey=992cc66/1dbb39e · gateway bind/owns-shell/E2E (f96d5e2/d42550f/d8cd929) · residual logged=doyle cond2 (f96d5e2). doyle G2 ruling (2026-06-13) already confirmed local-Gateway+fail-closed-WAN acceptable; cross-node registry endpoint_type = deferred LATER beat.

**T5 Gateway acceptance COMPLETE — 4 green commits (REQ-EP-6 [doc,impl,unit]+int; full sweep 0 fail):**
- f96d5e2 feat(api): `api bind --type <tag>` un-hardcode (establish_perch was hardcoding state="live_agent"). Gateway binds state="gateway"; revive carry-forward; relay/listen stays live_agent. (THE only agent-family hardcode — survey: shellhost/shellwake/linkhost owner-validation already type-agnostic.)
- 2d90ba2 test(daemon): WAN fail-closed at the receive_wan FUNNEL (doyle G2 cond 1) — forged user-msg from unproven origin → spooled body is plain msg.
- d8cd929 feat(test): mock-gateway fixture (adapters/mock-gateway/manifest.toml hostable_types=["Gateway"], reuses mock-session +--type/--mock-adapter passthrough, NO lecturn code) + gateway_e2e.rs: binds state=gateway, user-msg FROM gateway HONORED, FROM live_agent RE-STAMPED — full gate through real spt binary.
- d42550f test(daemon): gateway-typed owner spawns+owns a shell (owner = opaque namespace string, no type check).

**doyle G2 ruling (2026-06-13, both confirmed):** local-only Gateway user-backed origin + fail-closed WAN = ACCEPTABLE for M9 (literal CONTEXT posture). Cross-node registry endpoint_type trust = correctly a LATER beat (logged: ROADMAP open-threads + CONTEXT §Gateway "_Implemented posture_" note). 2 conditions DONE: (1) WAN funnel fail-closed test ✓, (2) residual logged ✓.

**T6 NEXT (REQ-EP-7) — live-role.md:** lives tracked/agents/<id>/live-role.md beside live-context.md (contextstore seam). Start-transition injection renders role→live-context→project (assembly = resume::download_psyche_context). `spt endpoint role` print / `--overwrite <file>` = SOLE writer. Role-writers SURVEY DONE (zero automated writers reach it — echo-ingest/signoff/reconcile target specific live-context.md/project-context.md/.conflicts; sync-merge replicates = transport not authoring). TODO: write M9-WAVE2-ROLE-WRITERS.md (front-load #1) + injection-order test (front-load #4) + no-automated-writer test. Then G2 ping doyle.

**T4 user-msg COMPLETE — 3 green commits (all per-commit gates green; full workspace sweep 0 fail):**
- b1905da feat(proto): EVENT_TYPE_USER_MSG + compose_user_msg_event + the identity-gate CORE (MsgOrigin{LocalUserCli,Endpoint(EndpointType)}, GATEWAY_TAG="gateway" open-type, is_gateway_endpoint/is_user_backed_origin/gate_user_msg_type — co-located w/ is_psyche_authorable_type). Truth table (gateway/CLI pass · agent+node+broker+shell+foreign re-stamp · non-request=msg) + SPOOF row + **N-1 tolerance fixture** (front-load #3) + round-trip. REQ-MSG-5 minted [doc,impl,unit], doc=CONTEXT §Gateway user-msg L209 tagged.
- 7d60af9 feat(cli): `spt send --user-msg` LOCAL origination. Gate keys on session-proven `roster::detect_self_id` (NOT --from label); classify_local_origin reads perch info.json `state`→EndpointType; apply_user_msg_gate composes user-msg envelope (honored) / plain body + loud USER_MSG_RESTAMP (agent). Honored form = verbatim typed-envelope body (notify/echo pattern → NO schema change, renders <EVENT type="user-msg">). Threads into local+WAN legs. Empty never composes.
- 0207ba6 feat(daemon): WAN-ingress re-stamp (KH 7.5 cross-node). receive_wan restamp_wan_user_msg(body, origin_user_backed) parse→recompose user-msg→msg unless origin user-backed; **T4 fail-closes origin_user_backed=false** (re-stamps ALL WAN user-msg). T5 resolves Gateway origin from registry → threads true (the SEAM is ready).

**T4 design notes (for T5/review):** type travels as verbatim typed-envelope BODY (no WanMessage/spool/render schema change). Local LocalUserCli trusted (single-box posture; local agent-shells-CLI accepted per existing trust boundary). WAN: only registry-resolvable Gateway origin provably user-backed → T5 opens it. "delivery surfaces render type" = existing render_event_lines verbatim pass-through (is_typed_event_envelope), no extra code.

**T5 NEXT (REQ-EP-6):** Gateway-typed perch binds(api bind)/advertises/addressable/owns-shells (un-hardcode agent-family in `shellhost.rs` owner validation — must SPAWN today)/subscribes-digests/IS T4 gate's user-backed WAN origin. In-tree MOCK gateway fixture (manifest+fake binary, R-DOCS-2 pattern, NO lecturn code). **Thread origin_user_backed into receive_wan** = the T4→T5 join (resolve from-id@origin-node → Gateway in registry). G2 front-load #5 = type-acceptance table.
**T6 (REQ-EP-7):** live-role.md. Role-writers SURVEY DONE (agent, 2026-06-13): ZERO automated writers reach tracked/live-role.md — echo-ingest/signoff/reconcile all target specific live-context.md/project-context.md/.conflicts/<f>.<h>.md; sync-merge replicates (transport, not authoring). Still need to WRITE M9-WAVE2-ROLE-WRITERS.md (front-load #1). T6 note: live-role.md excluded from reconcile authoring; injection order role→live→project.

**Branch `m9-adapter-custom` (Wave 1) — landed so far (all per-commit gates green: build/test/clippy -D/--no-default-features/traceable EXIT=0/xtask check):**
- 4129b81 — leaf-replace merge pure fn + contract table (`spt-runtime/src/profile.rs`)
- 5441c2c — shipped-profile parsing (`[profiles.<name>]` raw toml::Value on Manifest) + `profile::resolve` + tighten-only floor (require_approval ladder none<remembered<always + over_cap reject>approve; max_instances NOT floored). Schema re-blessed (`SPT_BLESS=1 cargo test -p spt-runtime`).
- 3b701a7 — `M9-WAVE1-ADDRESSING-SURVEY.md` = G1 artifact, the adapter_name consumer map.
- 51f37bc — local profiles (`adapters/<a>/profiles/<p>.toml`, survive re-add) + `create_local_profile`/`delete_local_profile` (shadow-refusal, floor-at-create, name validation) + `resolve_option(<adapter>[:<profile>])` = THE composite-addressing entry point (bare=parent short-circuit, shipped-first then local) + `profile::split_option`/`resolve_overlay`. **spt-runtime profile ENGINE now complete + durable.**

**REQ:** REQ-MANIFEST-2 minted+activated [doc,impl,unit] (doc=CONTEXT.md §adapter profile, tagged). Later mints surveyed: REQ-MANIFEST-3 (strings), -4 (hints); REQ-MSG-5 (user-msg); REQ-EP-6 (Gateway), -7 (live-role); REQ-TERM-4 @ toml line 386 = Wave 3 re-point.

**T1 COMPLETE (2026-06-12) — 6 more green commits on `m9-adapter-custom`:**
- 3abd4df — `resolve_option_in(registered, adapters_dir, option)` slice-helper for daemon seams + extracted `apply_option_overlay` core (shared w/ `resolve_option`).
- ba54595 — daemon seams (#1/#2/#3/#10): shellhost+shellwake `shell_section_of` now return OWNED Shell via `resolve_option_in` (profile = fresh merged manifest, not a borrow); `cascade_owner_edge`/`reconcile_once` thread `adapters_dir`; linkhost `resolve_link_target` via `resolve_option`. `{adapter_name}` template subst carries composite (#10).
- 4d208a4 — store identity (#7/#8/#9): `mint_shell_id` splits to PARENT segment → id stays colon-free `<parent>-<n>` (REQ-HAZARD-ID-CHARSET: `:` is reserved delim), composite rides ONLY in `adapter_name`. Profiles of one parent share ordinal space. Split inlined (spt-store can't dep spt-runtime — graph runs other way).
- e26cd8e — CLI (#6/#11): spawn/resolve_driveable/teardown via `resolve_option`; cap filter compares PARENT segments (`split_option`); **+ `resolve_option` now honors `active` flag** (soft-deregistered refuses new resolution — CLI got this free via `registered()` before).
- 8b9c52e — api (#4/#5/#12): `load_manifest` matches PARENT segment + overlays via new `resolve_option_onto(parent, adapters_dir, option)` (runtime-manifest seam: parent from `--manifest` file, shipped from its `[profiles]`, local from adapters_dir); cmd_emit sensory via `resolve_option`.
- cdaf8e4 — CLI verbs: `adapter create-profile <a> <name> [--from f|stdin]`/`delete-profile` (local-only) + `list` renders shipped+local profiles adjacent. Docs regenerated (drift gate).

Full workspace test sweep GREEN (8 crates libs + 111 spt bin unit tests, 0 fail). All per-commit gates green.

**WAVE 1 COMPLETE (2026-06-12/13) — T2 + T3 landed:**
- de5841a — T2 strings (REQ-MANIFEST-3 [doc,impl,unit]): `[strings]` toml::Table on Manifest (rides resolve_overlay free) + `profile::string_at`/`set_string_at` + `registry::get_string`/`set_local_string` + CLI `get-string`/`set-string` (local-only, bare=usage err). Data-only. Schema re-blessed.
- 0f34daa — T3 hints (REQ-MANIFEST-4 [doc,impl,unit]): ordered `[[hints]]` {keywords,text,regex?} + `Hint::matched_keyword` (literal case-insens substring / regex opt-in, invalid-regex silent) + `perch::session_dir` + `api hint --session` (`reporting::select_and_mark_hint`: first matching UNSEEN hint declaration-order, durable seen-set under sessions/<id>/hints-seen, skip-seen→next match). regex dep added to spt-runtime. Schema re-blessed.

**WAVE 1 G1 = PASS + FULLY CLOSED (2026-06-13).** PR #7 (m9-adapter-custom→main), 11 commits, CI GREEN both runners (test Win/Linux + n1-gate ×2 + traceability ✓; twohost skipped, no [twohost] tag). doyle independently re-ran (spt-runtime 44/0, spt-store 152/0, xtask OK) + reviewed whole survey/engine/seams. Doc-pin condition folded (b189513: CONTEXT §keyword hints skip-seen tiebreak ratified). **MERGE POLICY (operator, 2026-06-13): merge to main ONLY for a public release.** So PR #7 does NOT merge per-wave; it's the CI/gate vehicle. M9 waves STACK on the branch line (Wave 2 branches off m9-adapter-custom HEAD, not main) and ride to main together at a release. "keep going" = proceed to Wave 2 without merging.

**WAVE 2 (`m9-user-surfaces`, after #7 merges) — doyle's G2 FRONT-LOAD list (produce artifacts ALONGSIDE code, evidence-based gate like G1 survey):**
- T4 user-msg (REQ-MSG-5) + T5 Gateway acceptance (REQ-EP-6) + T6 live-role.md (REQ-EP-7). T4 before T5 (Gateway fixture exercises the user-msg gate).
- **#1 (EARLIEST) M9-WAVE2-ROLE-WRITERS.md** — survey EVERY code path that can write `tracked/live-role.md`, classified: `spt endpoint role --overwrite` = the ONE legit writer vs everything else = ZERO automated writers (Psyche reconcile/echo-commune/signoff structurally exclude). THE no-automated-writer proof; missed writer = silent contract break (same class as missed addressing consumer).
- **#3 (EARLIEST) envelope-decode-order + N-1 fixture** (T4) — pin decode order explicitly (HTML-unescape + `<br>` split, `&` LAST = REQ-HAZARD-ENVELOPE-DECODE-ORDER); ship a CONCRETE N-1-sender fixture proving an older-version envelope still decodes (doyle wants fixture, not prose).
- #2 user-msg IDENTITY-GATE TRUTH TABLE (sender identity class × accept-as-user-authority/refuse, incl. spoofing rows) — rides T4 code.
- #4 role INJECTION-ORDER test — assert role→live-context→project ORDER (not just all-three-present).
- #5 Gateway TYPE-ACCEPTANCE — mock-gateway fixture (R-DOCS-2, no lecturn code) + truth table of types Gateway accepts/refuses.
- **CONTEXT@HEAD normative**: any user-msg authority-model / role-writer detail NOT already in CONTEXT = STOP-and-ping doyle before building (re-read CONTEXT §user-msg/§live-role/envelope-decode hazard first). REQ mints surveyed: REQ-MSG-5, REQ-EP-6, REQ-EP-7.

doyle's G1 checklist: merge unit table vs CONTEXT contract · tighten-only floor · the survey list whole · shadow-refusal · CLI shape · resolution-path proof at the 3 manifest seams (composite→parent→overlay, bare short-circuit).


Acknowledging todlando's high-priority message (condition logged at 58f034f), then finishing the memory updates.


Bash

$OWL send todlando <<'EOF'
ACK — tracked condition logged + pushed (58f034f, docs-only on PR #9, impl I gated unchanged). REQUIRED-BEFORE-LECTURN on REQ-MSG-6 + ROADMAP, "activate int when adding it" — exactly right. Gateway-WAN gate stays PASS (docs-only re-trigger, traceable green).

M9 scorecard: G1 ✓ G2 ✓ Gateway-WAN ✓. Three clean waves. Only Wave 3 / G3 left in M9 (digest log-projection + REQ-TERM-4 re-point same-commit; history-grain fallback flag-if-taken; zero PTY-parse remnants; CC-fixture E2E; docs sweep). I'm armed for G3 whenever you or the operator pick it up.

Standing by. Good run.
EOF

ACK todlando condition logged


Write

---
name: m9-wave1-progress
description: M9 status — three of four gates closed (G1/G2/Gateway-WAN); only Wave 3/G3 remains
metadata:
  node_type: memory
  type: project
  originSessionId: cac905c4-0186-45cc-8462-8b34fe78904b
---

M9 "Adapter customization & session surfaces". Executor todlando, reviewer doyle. CONTEXT = NORMATIVE (deviation = STOP-and-ping, never code-around). Stacking branches; **merge-to-main ONLY for a public release** (operator policy 2026-06-13) — PRs are CI/gate vehicles. No tag rides M9.

**THREE GATES CLOSED (2026-06-12 → 06-13):**
- **G1 — Wave 1 `m9-adapter-custom` (PR #7):** adapter profiles + strings + hints. Doc-pin b189513 (skip-seen hint tiebreak ratified). CI green both runners. doyle re-ran + reviewed survey/engine/seams.
- **G2 — Wave 2 `m9-user-surfaces` (PR #8):** user-msg identity gate + Gateway acceptance + live-role.md. Blocker (reconcile must STRUCTURALLY exclude live-role.md, not by-rarity) fixed 77e0fb1 + verified (RoleExcluded before reconcile_file; artifact preserved; failer-runtime test). Win RED was hfenduleam disk-full not code — see [[hfenduleam-target-bloat-ci-red]]. CI green.
- **Gateway-WAN — `m9-gateway-wan` (PR #9, REQ-MSG-6):** cross-node Gateway user-msg. `Instance.endpoint_type` advert (additive/N-1) + `receive_wan` resolves via instances_of(from) filtered to QUIC-proven origin_node, is_gateway only. **Trust posture (operator-ratified 2026-06-13): subnet membership = trust boundary; no anti-forge defense; user-surface-set VETOED.** Matrix + wanmsg + xtask green. doyle authored the doc/spec (CONTEXT §Gateway + ROADMAP + JIT plan, commit 30fea7f) → directed todlando → gated PASS.

**Tracked condition — LOGGED (58f034f on PR #9):** T5 twohost HONORED-path int marked REQUIRED-BEFORE-LECTURN on REQ-MSG-6 + ROADMAP ("activate int when adding it"). Negative/security path already E2E (wanmsg funnel); positive honored path has no exerciser until a real cross-node Gateway. Don't re-flag — it's logged.

**LAST M9 PIECE = Wave 3 / G3:** digest log-projection engine + `api digest-entry` → RETIRE the PTY-parse half + REQ-TERM-4 retitle/re-point in the SAME commit (never momentarily un-evidenced). Watch: history-grain fallback pre-authorized but MUST be flagged at G3 if taken. Zero PTY-parse remnants; CC-fixture digest E2E; docs sweep. Don't re-plan G1/G2/Gateway-WAN — closed.

**owl send gotcha:** owl `send` HANGS if body is passed as a positional FROM arg — pipe body via stdin heredoc (`$OWL send <target> <<'EOF'…EOF`). todlando hit this; messages stalled silently.