    let mut target = connect_retry(&name);
    let sid = target
        .spawn_session(echo_spawn_req())
        .expect("spawn echo child");
    // The loopback peer's origin is this node's OWN broker-minted id — the same
    // value `net_dial_loopback` returns as `remote_id_hex`.
    let own_origin = target
        .net_dial_loopback()
        .expect("loopback dial (target side, for the origin)")
        .remote_id_hex;

    let mut operator = connect_retry(&name);
    let dialed = operator.net_dial_loopback().expect("operator loopback dial");
    assert_eq!(
        dialed.remote_id_hex, own_origin,
        "the loopback conn is the broker singleton — same own-node origin"
    );
    let (_viewport, mut target) = attach_drive_detach(
        &mut operator,
        target,
        dialed.conn_id,
        sid,
        "ling",
        &own_origin,
        b"LOCAL_DRIVE_OK",
    );

    let mut target = connect_retry(&name);
    let sid = target
        .spawn_session(echo_spawn_req())
        .expect("spawn echo child");

    // The operator acquires the broker's loopback conn; its `remote_id_hex` is
    // this node's OWN broker-minted id — the origin the peer row carries and the
    // serve gates same-node against.
    let mut operator = connect_retry(&name);
    let dialed = operator.net_dial_loopback().expect("operator loopback dial");
    let own_origin = dialed.remote_id_hex.clone();
    let (_viewport, mut target) = attach_drive_detach(
        &mut operator,
        target,
        dialed.conn_id,
        sid,
        "ling",
        &own_origin,
        b"LOCAL_DRIVE_OK",
    );

-    // The loopback peer's origin is this node's OWN broker-minted id — the same

-    // value `net_dial_loopback` returns as `remote_id_hex`.

-        .expect("loopback dial (target side, for the origin)")

+    // The operator acquires the broker's loopback conn; its `remote_id_hex` is

+    // this node's OWN broker-minted id — the origin the peer row carries and the

-        dialed.remote_id_hex, own_origin,

-        "the loopback conn is the broker singleton — same own-node origin"

+    let own_origin = dialed.remote_id_hex.clone();


proceed with W1.5 next steps. first verify the plan with doyle


Now compile the test:


Bash

cargo test -p spt-daemon --test attach --no-run 2>&1 | tail -40

Compile attach integration test


Command running in background with ID: bz8zpbp1h. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\bz8zpbp1h.output. You will be notified when it completes. To check interim output, use Read on that file path.


<task-notification>
<task-id>bz8zpbp1h</task-id>
<tool-use-id>toolu_013vCy8pRcvKfnQF8kYNUrfu</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\bz8zpbp1h.output</output-file>
<status>completed</status>
<summary>Background command "Compile attach integration test" completed (exit code 0)</summary>
</task-notification>


Test compiling. Add a focused nethost unit test for loopback singleton + cross-wire (`[unit->REQ-RC-1]`). Find insertion point:


Grep

fn host_binds_endpoint_to_node_identity|fn two_hosts_dial_over_loopback|fn hermetic


1043:    fn hermetic(identity: &Identity) -> NetConfig {
1057:    fn host_binds_endpoint_to_node_identity() {
1069:    fn two_hosts_dial_over_loopback() {


Prompt:
<task-notification>
<task-id>bz8zpbp1h</task-id>
<tool-use-id>toolu_013vCy8pRcvKfnQF8kYNUrfu</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\bz8zpbp1h.output</output-file>
<status>completed</status>
<summary>Background command "Compile attach integration test" completed (exit code 0)</summary>
</task-notification>


1	   Compiling spt-daemon v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
2	    Finished `test` profile [unoptimized + debuginfo] target(s) in 15.74s
3	  Executable tests\attach.rs (target\debug\deps\attach-b46a93ad5a7fc526.exe)
4

   Compiling spt-daemon v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 15.74s
  Executable tests\attach.rs (target\debug\deps\attach-b46a93ad5a7fc526.exe)


Test compiles. Add nethost unit test for loopback. Read the existing loopback test for style:


1057	    fn host_binds_endpoint_to_node_identity() {
1058	        let id = Identity::generate();
1059	        let host = NetHost::start(hermetic(&id)).expect("host start");
1060	        assert_eq!(host.node_id_hex(), id.public_key().to_hex());
1061	        assert_eq!(host.conn_count(), 0);
1062	        assert!(!host.addr_json().is_null(), "dialable address serializes");
1063	    }
1064
1065	    // [unit->REQ-DAEMON-2] a broker-owned host dials a second in-process host
1066	    // over hermetic loopback QUIC; both sides register the connection — the
1067	    // dialer in its table, the acceptor via its accept loop.
1068	    #[test]
1069	    fn two_hosts_dial_over_loopback() {
1070	        let a = NetHost::start(hermetic(&Identity::generate())).expect("host a");
1071	        let b_id = Identity::generate();
1072	        let b = NetHost::start(hermetic(&b_id)).expect("host b");
1073
1074	        let (conn_id, remote) = a.dial(b.addr()).expect("dial");
1075	        assert_eq!(conn_id, 1, "first broker-minted conn id");
1076	        assert_eq!(
1077	            remote,
1078	            b_id.public_key().to_hex(),
1079	            "handshake proves the peer"
1080	        );
1081	        assert_eq!(a.conn_count(), 1);
1082
1083	        // The acceptor's loop registers the inbound connection (async — poll).
1084	        for _ in 0..400 {
1085	            if b.conn_count() == 1 {
1086	                break;
1087	            }
1088	            std::thread::sleep(std::time::Duration::from_millis(5));
1089	        }
1090	        assert_eq!(b.conn_count(), 1, "accept loop registered the inbound conn");
1091	        assert_eq!(
1092	            b.conn_remote_hex(1).as_deref(),
1093	            Some(a.node_id_hex().as_str()),
1094	            "acceptor sees the dialer's identity"
1095	        );
1096	    }
1097	}
1098

    fn host_binds_endpoint_to_node_identity() {
        let id = Identity::generate();
        let host = NetHost::start(hermetic(&id)).expect("host start");
        assert_eq!(host.node_id_hex(), id.public_key().to_hex());
        assert_eq!(host.conn_count(), 0);
        assert!(!host.addr_json().is_null(), "dialable address serializes");
    }

    // [unit->REQ-DAEMON-2] a broker-owned host dials a second in-process host
    // over hermetic loopback QUIC; both sides register the connection — the
    // dialer in its table, the acceptor via its accept loop.
    #[test]
    fn two_hosts_dial_over_loopback() {
        let a = NetHost::start(hermetic(&Identity::generate())).expect("host a");
        let b_id = Identity::generate();
        let b = NetHost::start(hermetic(&b_id)).expect("host b");

        let (conn_id, remote) = a.dial(b.addr()).expect("dial");
        assert_eq!(conn_id, 1, "first broker-minted conn id");
        assert_eq!(
            remote,
            b_id.public_key().to_hex(),
            "handshake proves the peer"
        );
        assert_eq!(a.conn_count(), 1);

        // The acceptor's loop registers the inbound connection (async — poll).
        for _ in 0..400 {
            if b.conn_count() == 1 {
                break;
            }
            std::thread::sleep(std::time::Duration::from_millis(5));
        }
        assert_eq!(b.conn_count(), 1, "accept loop registered the inbound conn");
        assert_eq!(
            b.conn_remote_hex(1).as_deref(),
            Some(a.node_id_hex().as_str()),
            "acceptor sees the dialer's identity"
        );
    }
}


Edit

        assert_eq!(
            b.conn_remote_hex(1).as_deref(),
            Some(a.node_id_hex().as_str()),
            "acceptor sees the dialer's identity"
        );
    }
}

        assert_eq!(
            b.conn_remote_hex(1).as_deref(),
            Some(a.node_id_hex().as_str()),
            "acceptor sees the dialer's identity"
        );
    }

    // [unit->REQ-RC-1] the broker loopback conn (M12 W1.5, B1): `dial_loopback`
    // mints a REUSED singleton whose origin is this node's OWN id, and
    // `open_stream` on it cross-wires a PAIR in-process — an operator row and a
    // peer row (the one the dispatcher serves) — with the operator's writes
    // crossing to the peer row's read pump. No iroh handshake, no QUIC.
    #[test]
    fn loopback_conn_is_a_reused_singleton_that_cross_wires_a_stream_pair() {
        let id = Identity::generate();
        let host = NetHost::start(hermetic(&id)).expect("host start");

        // Singleton: reused, own-node origin (never a foreign/handshake id).
        let (c1, hex1) = host.dial_loopback();
        let (c2, hex2) = host.dial_loopback();
        assert_eq!(c1, c2, "the loopback conn is a reused singleton");
        assert_eq!(
            hex1,
            id.public_key().to_hex(),
            "loopback origin is THIS node's own id (broker-minted)"
        );
        assert_eq!(hex2, hex1);
        assert_eq!(host.conn_count(), 1, "one loopback conn held");

        // open_stream mints the cross-wired pair: operator row + peer row.
        let op_stream = host.open_stream(c1).expect("open loopback stream");
        let infos = host.stream_infos();
        assert_eq!(infos.len(), 2, "operator + peer rows");
        let op = infos
            .iter()
            .find(|s| s.stream_id == op_stream)
            .expect("operator row present");
        assert!(op.initiated_locally, "the returned id is the operator row");
        let peer = infos
            .iter()
            .find(|s| !s.initiated_locally)
            .expect("peer row present");
        assert_eq!(peer.conn_id, c1);
        assert_eq!(
            peer.remote_id_hex, hex1,
            "the peer row carries the own-node origin (gates same-node)"
        );

        // Cross-wire: a write on the operator side reaches the peer row's read
        // pump (its log advances) — entirely in-process, no QUIC.
        host.send_stream(op_stream, b"ping", false)
            .expect("write operator side");
        let peer_id = peer.stream_id;
        let mut crossed = false;
        for _ in 0..400 {
            if host
                .stream_infos()
                .iter()
                .any(|s| s.stream_id == peer_id && s.next_seq > 0)
            {
                crossed = true;
                break;
            }
            std::thread::sleep(std::time::Duration::from_millis(5));
        }
        assert!(
            crossed,
            "the operator-side write crossed in-process to the peer row's read pump"
        );
    }
}


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

        assert_eq!(
            b.conn_remote_hex(1).as_deref(),
            Some(a.node_id_hex().as_str()),
            "acceptor sees the dialer's identity"
        );
    }
}

        assert_eq!(
            b.conn_remote_hex(1).as_deref(),
            Some(a.node_id_hex().as_str()),
            "acceptor sees the dialer's identity"
        );
    }

    // [unit->REQ-RC-1] the broker loopback conn (M12 W1.5, B1): `dial_loopback`
    // mints a REUSED singleton whose origin is this node's OWN id, and
    // `open_stream` on it cross-wires a PAIR in-process — an operator row and a
    // peer row (the one the dispatcher serves) — with the operator's writes
    // crossing to the peer row's read pump. No iroh handshake, no QUIC.
    #[test]
    fn loopback_conn_is_a_reused_singleton_that_cross_wires_a_stream_pair() {
        let id = Identity::generate();
        let host = NetHost::start(hermetic(&id)).expect("host start");

        // Singleton: reused, own-node origin (never a foreign/handshake id).
        let (c1, hex1) = host.dial_loopback();
        let (c2, hex2) = host.dial_loopback();
        assert_eq!(c1, c2, "the loopback conn is a reused singleton");
        assert_eq!(
            hex1,
            id.public_key().to_hex(),
            "loopback origin is THIS node's own id (broker-minted)"
        );
        assert_eq!(hex2, hex1);
        assert_eq!(host.conn_count(), 1, "one loopback conn held");

        // open_stream mints the cross-wired pair: operator row + peer row.
        let op_stream = host.open_stream(c1).expect("open loopback stream");
        let infos = host.stream_infos();
        assert_eq!(infos.len(), 2, "operator + peer rows");
        let op = infos
            .iter()
            .find(|s| s.stream_id == op_stream)
            .expect("operator row present");
        assert!(op.initiated_locally, "the returned id is the operator row");
        let peer = infos
            .iter()
            .find(|s| !s.initiated_locally)
            .expect("peer row present");
        assert_eq!(peer.conn_id, c1);
        assert_eq!(
            peer.remote_id_hex, hex1,
            "the peer row carries the own-node origin (gates same-node)"
        );

        // Cross-wire: a write on the operator side reaches the peer row's read
        // pump (its log advances) — entirely in-process, no QUIC.
        host.send_stream(op_stream, b"ping", false)
            .expect("write operator side");
        let peer_id = peer.stream_id;
        let mut crossed = false;
        for _ in 0..400 {
            if host
                .stream_infos()
                .iter()
                .any(|s| s.stream_id == peer_id && s.next_seq > 0)
            {
                crossed = true;
                break;
            }
            std::thread::sleep(std::time::Duration::from_millis(5));
        }
        assert!(
            crossed,
            "the operator-side write crossed in-process to the peer row's read pump"
        );
    }
}

+    // [unit->REQ-RC-1] the broker loopback conn (M12 W1.5, B1): `dial_loopback`

+    // mints a REUSED singleton whose origin is this node's OWN id, and

+    // `open_stream` on it cross-wires a PAIR in-process — an operator row and a

+    // peer row (the one the dispatcher serves) — with the operator's writes

+    // crossing to the peer row's read pump. No iroh handshake, no QUIC.

+    fn loopback_conn_is_a_reused_singleton_that_cross_wires_a_stream_pair() {

+        let host = NetHost::start(hermetic(&id)).expect("host start");

+        // Singleton: reused, own-node origin (never a foreign/handshake id).

+        let (c1, hex1) = host.dial_loopback();

+        let (c2, hex2) = host.dial_loopback();

+        assert_eq!(c1, c2, "the loopback conn is a reused singleton");

+            "loopback origin is THIS node's own id (broker-minted)"

+        assert_eq!(host.conn_count(), 1, "one loopback conn held");

+        // open_stream mints the cross-wired pair: operator row + peer row.

+        let op_stream = host.open_stream(c1).expect("open loopback stream");

+        let infos = host.stream_infos();

+        assert_eq!(infos.len(), 2, "operator + peer rows");

+            .find(|s| s.stream_id == op_stream)

+            .expect("operator row present");

+        assert!(op.initiated_locally, "the returned id is the operator row");

+            .find(|s| !s.initiated_locally)

+            .expect("peer row present");

+            "the peer row carries the own-node origin (gates same-node)"

+        // Cross-wire: a write on the operator side reaches the peer row's read

+        // pump (its log advances) — entirely in-process, no QUIC.

+        host.send_stream(op_stream, b"ping", false)

+            .expect("write operator side");

+                .any(|s| s.stream_id == peer_id && s.next_seq > 0)

+            std::thread::sleep(std::time::Duration::from_millis(5));

+            "the operator-side write crossed in-process to the peer row's read pump"


Now run the full attach test + nethost unit tests:


Bash

cargo test -p spt-daemon --test attach 2>&1 | tail -30; echo "=== NETHOST UNIT ==="; cargo test -p spt-daemon --lib nethost 2>&1 | tail -25

Run attach int + nethost unit tests


Command running in background with ID: bb9m8mlz1. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\bb9m8mlz1.output. You will be notified when it completes. To check interim output, use Read on that file path.


1	   Compiling spt-daemon v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
2	    Finished `test` profile [unoptimized + debuginfo] target(s) in 6.63s
3	     Running tests\attach.rs (target\debug\deps\attach-b46a93ad5a7fc526.exe)
4
5	running 6 tests
6	test loopback_self_dial_is_refused_local_uses_fallback_transport ... ok
7	test local_attach_via_loopback_conn_rides_the_same_pump ... ok
8	test remote_attach_drives_a_real_pty_cross_daemon ... ok
9	test spt_hosted_bringup_then_cross_node_attach_drives_the_pty ... ok
10	test attach_registers_remote_drive_detection ... ok
11	test attach_survives_target_brain_restart_exactly_once ... ok
12
13	test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.17s
14
15	=== NETHOST UNIT ===
16

   Compiling spt-daemon v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 6.63s
     Running tests\attach.rs (target\debug\deps\attach-b46a93ad5a7fc526.exe)

running 6 tests
test loopback_self_dial_is_refused_local_uses_fallback_transport ... ok
test local_attach_via_loopback_conn_rides_the_same_pump ... ok
test remote_attach_drives_a_real_pty_cross_daemon ... ok
test spt_hosted_bringup_then_cross_node_attach_drives_the_pty ... ok
test attach_registers_remote_drive_detection ... ok
test attach_survives_target_brain_restart_exactly_once ... ok

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

=== NETHOST UNIT ===


<task-notification>
<task-id>bb9m8mlz1</task-id>
<tool-use-id>toolu_01M6Ym27UwEJUDxeM7SxEDdd</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\bb9m8mlz1.output</output-file>
<status>completed</status>
<summary>Background command "Run attach int + nethost unit tests" completed (exit code 0)</summary>
</task-notification>


Attach tests 6/6 pass (new local + refactored cross-node both green). Nethost lib portion still running — re-read:


15	=== NETHOST UNIT ===
16	   Compiling spt-daemon v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
17	    Finished `test` profile [unoptimized + debuginfo] target(s) in 9.54s
18	     Running unittests src\lib.rs (target\debug\deps\spt_daemon-69d76b8e9c6e806f.exe)
19
20	running 3 tests
21	test nethost::tests::loopback_conn_is_a_reused_singleton_that_cross_wires_a_stream_pair ... ok
22	test nethost::tests::two_hosts_dial_over_loopback ... ok
23	test nethost::tests::host_binds_endpoint_to_node_identity ... ok
24
25	test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 282 filtered out; finished in 0.14s
26
27

=== NETHOST UNIT ===
   Compiling spt-daemon v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 9.54s
     Running unittests src\lib.rs (target\debug\deps\spt_daemon-69d76b8e9c6e806f.exe)

running 3 tests
test nethost::tests::loopback_conn_is_a_reused_singleton_that_cross_wires_a_stream_pair ... ok
test nethost::tests::two_hosts_dial_over_loopback ... ok
test nethost::tests::host_binds_endpoint_to_node_identity ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 282 filtered out; finished in 0.14s


Prompt:
<task-notification>
<task-id>bb9m8mlz1</task-id>
<tool-use-id>toolu_01M6Ym27UwEJUDxeM7SxEDdd</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\bb9m8mlz1.output</output-file>
<status>completed</status>
<summary>Background command "Run attach int + nethost unit tests" completed (exit code 0)</summary>
</task-notification>


All green: 6 attach int + 3 nethost unit. Now the gate sweep: full spt+spt-daemon suite, clippy -D warnings, traceable check. Chain in background.


echo "=== FULL SUITE (spt + spt-daemon) ==="; cargo test -p spt-daemon -p spt 2>&1 | tail -25; echo "=== CLIPPY -D warnings ==="; cargo clippy -p spt-daemon -p spt --all-targets -- -D warnings 2>&1 | tail -15; echo "CLIPPY_EXIT=$?"

Full suite + clippy gate sweep


Command running in background with ID: b162vyt7l. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\b162vyt7l.output. You will be notified when it completes. To check interim output, use Read on that file path.


While suite+clippy run, run traceable check (scans tags, independent):


traceable-reqs check 2>&1 | tail -30; echo "TRACEABLE_EXIT=$?"

traceable-reqs check


  [OK] REQ-TERM-1  required: [impl, unit]  stages: -doc +impl +unit -int
       Process-supervisor terminal wrapper hosting broker PTYs
  [OK] REQ-TERM-2  required: [impl, unit]  stages: -doc +impl +unit -int
       session-surface abstraction; send-keys + send-line injection
  [OK] REQ-TERM-3  required: [impl, unit]  stages: -doc +impl +unit -int
       Byte-stream remote terminal streaming for v1
  [OK] REQ-TERM-4  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Live activity buffer (session digest): projection of normalized session logs, snapshot-pull (spt endpoint digest) + structured-delta-stream contract + api digest-entry push
  [OK] REQ-TERM-5  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       Adapter-declared digest extractor seam: a `[digest]` manifest section declaring an imperative extractor (native harness log -> the {role,text,tool,ts} contract; defaults to the [history] source files with an own-source escape hatch), `api digest-entry` push fallback, register-time validation of the section, adapter-declared presentation defaults (window depth, arg-truncation, sprint-collapse) that any consumer may override, and a `spt adapter digest-proof` author tool plus runtime skip-diagnostics (no silent drop). Reverses M9's no-manifest-seam stance; no declarative DSL.
  [OK] REQ-TERM-6  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Thread-spanning digest across session boundaries: a per-endpoint session ledger (`<perch>/sessions.log`) appended at first bind and by `api boundary` on `/clear`|`/compact` session rotation, the digest enumerating the last K sessions so its rolling window bridges a boundary, and a distinctive in-timeline boundary marker (DigestEntry::Boundary). The digest follows the live-agent thread, not a single session.
  [OK] REQ-TERM-7  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Two-origin digest merge: spt-owned context-injection entries (psyche_download | echo_mirror | owl_message) appended by spt to the endpoint `digest.log`, timestamp-interleaved with the adapter's extracted activity records into one ordered timeline, via a distinct context-injection record category. Data model only this milestone; GUI collapse/expand and the echo-reads-digest delta loop are deferred to the surfaces that consume them.
  [OK] REQ-UPD-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Peer-propagated update over P2P
  [OK] REQ-UPD-2  required: [impl, unit]  stages: -doc +impl +unit -int
       All binaries signature-verified before handoff
  [OK] REQ-UPD-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
       No endpoint process terminates/suspends during self-update
  [OK] REQ-UPD-4  required: [impl, unit]  stages: -doc +impl +unit -int
       Update gated on user confirmation by default; opt-in full-auto
  [OK] REQ-UPD-5  required: [impl, unit]  stages: -doc +impl +unit -int
       spt-core ripple-updates registered adapters
  [OK] REQ-UPD-6  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       Platform-targeted update sets and debug rollout: signed multi-platform update metadata, recipient platform selection, channel-scoped monotonic counters, debug-channel opt-in via release-key overlay, local staging plus pull-based peer propagation, and maintainer-only convergence tooling (ADR-0016)
  [OK] REQ-UPD-7  required: [impl, unit]  stages: -doc +impl +unit -int
       Origin-source update bootstrap (`spt update fetch`): pull the latest signed release directly from the GitHub release origin (`SaberMage/spt-releases`) — the per-platform artifact + its `<asset>.release.json` SignedRelease metadata — and stage it through the EXISTING verify→stage pipeline (the same `plan_verified` gate: two-key signature + channel + monotonic rollback floor + SHA-256), after which the normal consent-notif / `spt update apply` flow is unchanged. Closes the peer-only-discovery gap (REQ-UPD-1): a first-in-fleet / isolated node can update with no peer to pull from. The signed-release anchor keeps the GitHub transport untrusted-but-verified.
  [OK] REQ-UPD-8  required: [impl, unit]  stages: -doc +impl +unit -int
       Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent.
TRACEABLE_EXIT=0


traceable EXIT=0 (overall pass). Confirm REQ-RC-1 row + no failures:


traceable-reqs check 2>&1 | grep -iE "RC-1|HOST-RUN|FAIL|MISSING|ERROR|\[X\]"

Filter traceable for RC-1/HOST-RUN/failures


       Peer address seeding, both cold starts: durable peer-addrs.json (identity dir) maps peer pubkey → last-known dialable address; the pump's resolver consults it FIRST with id-only discovery fallback on miss or dial failure (a stale addr never strands a peer); written by the pairing ceremony (both sides, from the live connection) and by the pump on successful connect; post-join first sync and post-restart resync converge in seconds, not ~1 min (M8 decisions 14, 20)
       Net-bind boot-race resilience: a daemon that comes up net-less (NetHost::start failed — e.g. the systemd unit autostarted before the network/DNS stack was ready, `Failed to create an address lookup service`) must SELF-HEAL — retry the net bring-up in the background with capped backoff and, on success, attach net to the broker + spawn the dispatcher/peer-pump (which today are gated on `net_up` at boot and so never start, leaving the node silently unreachable until a manual restart — kitsubito 2026-06-08). Status surfaces the net-less state honestly (a net-less broker renders as 'no connection', not only a pump-STALLED line with a bogus pre-boot heartbeat age). The installer's autostart unit waits for the network (`Wants=/After=network-online.target`) as belt-and-suspenders.
       A surfaced concurrent context pair is durably preserved (both versions, tracked artifacts) until a strictly dominating write clears it; no reconcile failure path discards an unmerged version (6.6, ADR-0013)
       Concurrent SQLite openers (registry/spool) must not fail with 'database is locked' (4.7)
       Stale registry entries degrade to fallback, never hard-fail (4.3)
  [OK] REQ-HOST-RUN-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
  [OK] REQ-HOST-RUN-2  required: []  stages: -doc -impl -unit -int
       cross-node Gateway user-msg honored via advertised endpoint_type: a user-msg from a Gateway-typed origin survives the receive_wan funnel as user-msg (vs the fail-closed re-stamp), keyed on the QUIC-handshake-proven origin node (never wire `from`). Trust boundary = subnet membership (operator-ratified 2026-06-13); no defense against an in-subnet member forging the type. Instance.endpoint_type is an additive serde-default field extending REQ-INST-7's data model. Absent/unknown type → re-stamp (N-1 rollout grace)
       NTP TOTP offset: the pairing ceremony queries NTP at ceremony time (both sides) and applies the derived offset to the TOTP calculation in-process only; system-clock fallback when NTP is unreachable (offline LAN pairing unaffected — NTP failure never blocks a pairing that succeeds today); never sets the OS clock; no background sync loop (M8 decision 18; field trigger: enlyzeam clock >1 min off exceeds the ±1 window)
  [OK] REQ-RC-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent.


REQ-RC-1 **[OK]** impl+unit+int all evidenced; REQ-HOST-RUN-1 [OK]. traceable EXIT=0. Now check the suite+clippy sweep:


1	=== FULL SUITE (spt + spt-daemon) ===
2

=== FULL SUITE (spt + spt-daemon) ===


Suite still running. Awaiting completion notification.


noop


waiting


Implementation complete, evidence green so far:
- **6 attach int tests pass** (new `local_attach_via_loopback_conn_rides_the_same_pump` + refactored cross-node both call the shared `attach_drive_detach` helper → one-pump by construction)
- **3 nethost unit tests pass** (incl new loopback singleton + cross-wire)
- **traceable EXIT=0** — REQ-RC-1 [OK] impl+unit+int, REQ-HOST-RUN-1 [OK]

Full suite + clippy -D warnings sweep still running. Awaiting it before pinging doyle at the W1.5 gate.


Wasted call — file unchanged since your last Read. Refer to that earlier tool_result instead.


Holding for the suite+clippy completion notification.


<task-notification>
<task-id>b162vyt7l</task-id>
<tool-use-id>toolu_016vCip6Hv5X9JVUj2qKukEb</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\b162vyt7l.output</output-file>
<status>completed</status>
<summary>Background command "Full suite + clippy gate sweep" completed (exit code 0)</summary>
</task-notification>


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


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


1	=== FULL SUITE (spt + spt-daemon) ===
2	test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
3
4	     Running tests\wanmsg.rs (target\debug\deps\wanmsg-7e7963949ca23b08.exe)
5
6	running 3 tests
7	test wan_user_msg_from_unproven_origin_is_restamped_at_the_funnel ... ok
8	test receiver_restart_replays_feed_without_double_delivery ... ok
9	test wan_message_lands_exactly_once_under_transport_origin ... ok
10
11	test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.28s
12
13	     Running tests\xfer.rs (target\debug\deps\xfer-9cb7b4cf405a2212.exe)
14
15	running 2 tests
16	test push_survives_target_brain_restart_exactly_once ... ok
17	test fetch_lands_byte_identical_with_progress_both_ends ... ok
18
19	test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.18s
20
21	   Doc-tests spt_daemon
22
23	running 0 tests
24
25	test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
26
27	=== CLIPPY -D warnings ===
28	    Checking spt-daemon v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
29	    Checking spt v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
30	    Finished `dev` profile [unoptimized + debuginfo] target(s) in 5.31s
31	CLIPPY_EXIT=0
32

=== FULL SUITE (spt + spt-daemon) ===
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests\wanmsg.rs (target\debug\deps\wanmsg-7e7963949ca23b08.exe)

running 3 tests
test wan_user_msg_from_unproven_origin_is_restamped_at_the_funnel ... ok
test receiver_restart_replays_feed_without_double_delivery ... ok
test wan_message_lands_exactly_once_under_transport_origin ... ok

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

     Running tests\xfer.rs (target\debug\deps\xfer-9cb7b4cf405a2212.exe)

running 2 tests
test push_survives_target_brain_restart_exactly_once ... ok
test fetch_lands_byte_identical_with_progress_both_ends ... ok

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

   Doc-tests spt_daemon

running 0 tests

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

=== CLIPPY -D warnings ===
    Checking spt-daemon v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Checking spt v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 5.31s
CLIPPY_EXIT=0


Clippy EXIT=0. Verify NO test failures across full suite (tail scrolled; confirm via grep):


grep -nE "test result:|FAILED|error\[|panicked" "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\b162vyt7l.output" | grep -viE "0 failed"

Find any non-zero failures in suite output


(Bash completed with no output)


No failures anywhere. Confirm suite breadth (binary count):


grep -cE "Running (unittests|tests)" "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\b162vyt7l.output"; echo "--- per-binary pass counts ---"; grep -oE "[0-9]+ passed" "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\b162vyt7l.output" | awk '{s+=$1} END{print s" tests passed total"}'

Count test binaries + total passed


2
--- per-binary pass counts ---
7 tests passed total


`tail -25` truncated the suite — only saw the tail, can't trust full-suite verdict. Re-run capturing real exit code:


cargo test -p spt-daemon -p spt 2>&1 | tee "C:\Users\decid\AppData\Local\Temp\spt-suite-full.txt" >/dev/null; echo "SUITE_EXIT=${PIPESTATUS[0]}"; echo "--- totals ---"; grep -oE "[0-9]+ passed" "C:\Users\decid\AppData\Local\Temp\spt-suite-full.txt" | awk '{s+=$1} END{print s" passed"}'; grep -E "test result: FAILED|[1-9][0-9]* failed" "C:\Users\decid\AppData\Local\Temp\spt-suite-full.txt" || echo "NO FAILURES"

Full suite, real exit code + failure scan


Command running in background with ID: bj853ln9q. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\bj853ln9q.output. You will be notified when it completes. To check interim output, use Read on that file path.


Re-running full suite with proper exit capture (prior tail truncated the verdict). Awaiting completion before pinging doyle.


<task-notification>
<task-id>bj853ln9q</task-id>
<tool-use-id>toolu_01SifXqVLNGf1mzwnZJiNo89</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\bj853ln9q.output</output-file>
<status>completed</status>
<summary>Background command "Full suite, real exit code + failure scan" completed (exit code 0)</summary>
</task-notification>


1	SUITE_EXIT=0
2	--- totals ---
3	498 passed
4	NO FAILURES
5

SUITE_EXIT=0
--- totals ---
498 passed
NO FAILURES


Full suite: **498 passed, 0 failures, EXIT=0.** All gate criteria met. Ping doyle at the W1.5 gate (ring for verdict):


$OWL ring doyle todlando <<'EOF'
W1.5 GATE — ready for your verdict. Built per your 3 rulings.

DELIVERED (B1 broker loopback-conn):
- nethost.rs: `ConnKind{Quic,Loopback}` + `SendHalf`/`RecvHalf` enums. Transport seam = `tokio::io::duplex` — ONE full-duplex pair wires an entire operator↔peer stream; both feed the SAME StreamLog/subscribe/send_stream/read-pump. `dial_loopback()` = reused singleton, `remote_id_hex` = THIS node's own id. `open_stream` on a loopback conn mints the cross-wired PAIR (operator row + peer row); peer row (initiated_locally=false, own-node origin) → dispatch loop → serve_attach.
- Explicit `net-dial-loopback` frame (NOT sentinel addr) — brain.net_dial_loopback + broker dispatch_net_dial_loopback. net_dial stays pristine.
- Origin = own-node, broker-minted. access_check same-node short-circuit Allows (access.rs:89) — NO access change needed. Invariant documented (own-node origin can only arise from a broker-minted loopback conn, never an inbound handshake).
- rc.rs: swapped the ONE self-dial line → net_dial_loopback. Pump unchanged.

ONE-PUMP PROOF (your Q3 — by construction): extracted shared `attach_drive_detach` helper. BOTH `remote_attach_drives_a_real_pty_cross_daemon` (cross-node QUIC) AND new `local_attach_via_loopback_conn_rides_the_same_pump` (loopback) call it identically — diverge ONLY in conn acquisition (net_dial vs net_dial_loopback) + expected_origin. No second path exists to drift.

EVIDENCE (all green):
- attach int 6/6 (incl new local). nethost unit +1 (`loopback_conn_is_a_reused_singleton_that_cross_wires_a_stream_pair`).
- full spt+spt-daemon suite: 498 passed / 0 failed.
- clippy -D warnings EXIT=0. traceable EXIT=0 — REQ-RC-1 [OK] impl+unit+int.
- UNCOMMITTED (operator-gated, on m12-w1-bringup-rc).

GATE PASS? Next per plan = REQ-HOST-RUN-2 (project cwd, additive SpawnReq.cwd) before M12 gate.
EOF

Ring doyle for W1.5 gate verdict


Command running in background with ID: bqp8ojamo. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\bqp8ojamo.output. You will be notified when it completes. To check interim output, use Read on that file path.


<task-notification>
<task-id>bqp8ojamo</task-id>
<tool-use-id>toolu_01PY9iNsQEGyJQ8jt88zNq2D</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\bqp8ojamo.output</output-file>
<status>completed</status>
<summary>Background command "Ring doyle for W1.5 gate verdict" completed (exit code 0)</summary>
</task-notification>


Gate ping sent (verdict arrives via poll). Update memory to reflect W1.5 completion while waiting:


1	---
2	name: m12-w1-progress
3	description: "M12 Wave 1 (spt endpoint run + spt rc) build state, decisions, and the self-dial-dead blocker"
4	metadata:
5	  node_type: memory
6	  type: project
7	  originSessionId: 1ae6a063-4622-4f5b-86b8-b4506d12ccfb
8	---
9
10	M12 W1 (spt-hosted harness bringup + `spt rc` PTY attach) — doyle handed execution to todlando 2026-06-14. Full spec: `M12-PLAN.md` + `M12-W1-PLAN.md` (durable build plan, resumable).
11
12	**Built + green:**
13	- Daemon `spt-daemon/src/harnesshost.rs`: `prepare_harness_spawn` (validate kind=harness + fill `[session.self]` template) + `launch_harness_brokered_in` (spawn into broker PTY via `spawn_session_pid`, mirrors `shellhost::launch_shell_brokered_in`) + `mint_session_id`. 3 unit tests.
14	- `spt/src/rc.rs`: operator attach pump (single-Brain main thread + stdin reader thread→mpsc, `cold_start_pump` Split carrier for bounded reads, seq-dedup, ctrl-b detach state machine). 3 unit tests.
15	- cli.rs: `EndpointCmd::Run` + `cmd_endpoint_run`; top-level `Cmd::Rc`; crossterm dep added.
16	- REQs registered: REQ-HOST-RUN-1 (spawn), REQ-RC-1 (attach), REQ-HOST-RUN-2 (project cwd — doyle-mandated, must land before M12 gate; W1 ships broker-inherited cwd as proof-shortcut only).
17
18	**KEY DECISIONS (doyle-validated):** spawn-first (harness self-registers its perch on bind via `api/startup.rs establish_perch`, no daemon pre-register); id rides argv via `{id}` template (NO SpawnReq wire change); one-pump invariant = transport swappable under one `serve_attach` pump.
19
20	**BLOCKER RESOLVED (doyle ruling 2026-06-14):** loopback self-dial IMPOSSIBLE (iroh refuses `net_dial(own_addr)`, pinned green test). doyle picked **B1 (broker in-process loopback-conn — local rides byte-identical pump+serve path, covered by cross-node tests; B2's pump-branch rejected as the lesser-tested local path warned against), scoped as a tight W1.5** (NOT in W1). Rationale: cross-node attach + endpoint-run bringup + pump are built+green → gate W1 NOW (bringup verifiable via cross-node attach to the spawned PTY); B1 is a sensitive broker-layer addition (6.7 zone) deserving its own focused task+gate; local lands W1.5 before M12 close. One-pump assertion (both transports → one pump) kept in W1.5.
21
22	**REQ-RC-1 split:** cross-node int evidence at W1 gate; local int evidence at W1.5 (per-wave activation).
23
24	**W1 GATE: PASS (doyle 2026-06-14).** Independently verified: traceable-reqs check reproduced (196 complete / 0 findings; REQ-HOST-RUN-1 + REQ-RC-1 impl+unit+int, REQ-HOST-RUN-2 []); int test `attach.rs:217` inspected — real `launch_harness_brokered_in` spawns [session.self] into broker-A PTY, B attaches cross-node, HOSTED_OK round-trips, detach→Detached+survives, origin=handshake-identity (KH 7.5); self-dial-refused pin genuine. Trusted reported full-suite (46 bins/0) + clippy -D. **UNCOMMITTED** (awaiting operator).
25
26	**NEXT:** **W1.5** (resumable JIT plan = `M12-W1.5-PLAN.md`, has B1 design + broker.rs/nethost.rs/dispatch.rs investigation TODOs) = build B1 broker loopback-conn (local attach rides byte-identical pump) + swap rc.rs's ONE self-dial line to loopback-dial (rc.rs currently codes the now-refused self-dial → LOCAL `spt rc` errors at runtime until then; cross-node works now) + local attach E2E int + "both transports → one pump" assertion → activate REQ-RC-1 local int → traceable check → W1.5 gate. Then **REQ-HOST-RUN-2** (project cwd via additive SpawnReq.cwd) before M12 close. Then W2 picker / W3 manifest / W4 subnet QR / W5 whoami. Commit only when user asks (not on default branch unprompted). See [[traceable-per-wave-activation]], [[pump-ipc-deadline-fix]], [[spt-claude-code-next]] (perri waits on M12 gate).
27

---
name: m12-w1-progress
description: "M12 Wave 1 (spt endpoint run + spt rc) build state, decisions, and the self-dial-dead blocker"
metadata:
  node_type: memory
  type: project
  originSessionId: 1ae6a063-4622-4f5b-86b8-b4506d12ccfb
---

M12 W1 (spt-hosted harness bringup + `spt rc` PTY attach) — doyle handed execution to todlando 2026-06-14. Full spec: `M12-PLAN.md` + `M12-W1-PLAN.md` (durable build plan, resumable).

**Built + green:**
- Daemon `spt-daemon/src/harnesshost.rs`: `prepare_harness_spawn` (validate kind=harness + fill `[session.self]` template) + `launch_harness_brokered_in` (spawn into broker PTY via `spawn_session_pid`, mirrors `shellhost::launch_shell_brokered_in`) + `mint_session_id`. 3 unit tests.
- `spt/src/rc.rs`: operator attach pump (single-Brain main thread + stdin reader thread→mpsc, `cold_start_pump` Split carrier for bounded reads, seq-dedup, ctrl-b detach state machine). 3 unit tests.
- cli.rs: `EndpointCmd::Run` + `cmd_endpoint_run`; top-level `Cmd::Rc`; crossterm dep added.
- REQs registered: REQ-HOST-RUN-1 (spawn), REQ-RC-1 (attach), REQ-HOST-RUN-2 (project cwd — doyle-mandated, must land before M12 gate; W1 ships broker-inherited cwd as proof-shortcut only).

**KEY DECISIONS (doyle-validated):** spawn-first (harness self-registers its perch on bind via `api/startup.rs establish_perch`, no daemon pre-register); id rides argv via `{id}` template (NO SpawnReq wire change); one-pump invariant = transport swappable under one `serve_attach` pump.

**BLOCKER RESOLVED (doyle ruling 2026-06-14):** loopback self-dial IMPOSSIBLE (iroh refuses `net_dial(own_addr)`, pinned green test). doyle picked **B1 (broker in-process loopback-conn — local rides byte-identical pump+serve path, covered by cross-node tests; B2's pump-branch rejected as the lesser-tested local path warned against), scoped as a tight W1.5** (NOT in W1). Rationale: cross-node attach + endpoint-run bringup + pump are built+green → gate W1 NOW (bringup verifiable via cross-node attach to the spawned PTY); B1 is a sensitive broker-layer addition (6.7 zone) deserving its own focused task+gate; local lands W1.5 before M12 close. One-pump assertion (both transports → one pump) kept in W1.5.

**REQ-RC-1 split:** cross-node int evidence at W1 gate; local int evidence at W1.5 (per-wave activation).

**W1 GATE: PASS (doyle 2026-06-14).** Independently verified: traceable-reqs check reproduced (196 complete / 0 findings; REQ-HOST-RUN-1 + REQ-RC-1 impl+unit+int, REQ-HOST-RUN-2 []); int test `attach.rs:217` inspected — real `launch_harness_brokered_in` spawns [session.self] into broker-A PTY, B attaches cross-node, HOSTED_OK round-trips, detach→Detached+survives, origin=handshake-identity (KH 7.5); self-dial-refused pin genuine. Trusted reported full-suite (46 bins/0) + clippy -D. **UNCOMMITTED** (awaiting operator).

**NEXT:** **W1.5** (resumable JIT plan = `M12-W1.5-PLAN.md`, has B1 design + broker.rs/nethost.rs/dispatch.rs investigation TODOs) = build B1 broker loopback-conn (local attach rides byte-identical pump) + swap rc.rs's ONE self-dial line to loopback-dial (rc.rs currently codes the now-refused self-dial → LOCAL `spt rc` errors at runtime until then; cross-node works now) + local attach E2E int + "both transports → one pump" assertion → activate REQ-RC-1 local int → traceable check → W1.5 gate. Then **REQ-HOST-RUN-2** (project cwd via additive SpawnReq.cwd) before M12 close. Then W2 picker / W3 manifest / W4 subnet QR / W5 whoami. Commit only when user asks (not on default branch unprompted). See [[traceable-per-wave-activation]], [[pump-ipc-deadline-fix]], [[spt-claude-code-next]] (perri waits on M12 gate).


Prompt:
<task-notification>
<task-id>bqp8ojamo</task-id>
<tool-use-id>toolu_01PY9iNsQEGyJQ8jt88zNq2D</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\bqp8ojamo.output</output-file>
<status>completed</status>
<summary>Background command "Ring doyle for W1.5 gate verdict" completed (exit code 0)</summary>
</task-notification>


Update memory — W1.5 built, gate pending doyle verdict:


Edit

**NEXT:** **W1.5** (resumable JIT plan = `M12-W1.5-PLAN.md`, has B1 design + broker.rs/nethost.rs/dispatch.rs investigation TODOs) = build B1 broker loopback-conn (local attach rides byte-identical pump) + swap rc.rs's ONE self-dial line to loopback-dial (rc.rs currently codes the now-refused self-dial → LOCAL `spt rc` errors at runtime until then; cross-node works now) + local attach E2E int + "both transports → one pump" assertion → activate REQ-RC-1 local int → traceable check → W1.5 gate. Then **REQ-HOST-RUN-2** (project cwd via additive SpawnReq.cwd) before M12 close. Then W2 picker / W3 manifest / W4 subnet QR / W5 whoami. Commit only when user asks (not on default branch unprompted). See [[traceable-per-wave-activation]], [[pump-ipc-deadline-fix]], [[spt-claude-code-next]] (perri waits on M12 gate).

**W1.5 BUILT (todlando 2026-06-14) — gate pinged to doyle, verdict pending.** B1 broker loopback-conn done (design in `M12-W1.5-PLAN.md` "Crystallized design"). Transport seam = `tokio::io::duplex` (ONE full-duplex pair wires a whole operator↔peer stream); only the leaf send/recv become enums (`SendHalf`/`RecvHalf`), so StreamLog/subscribe/send_stream/read-pump are byte-identical across QUIC + loopback. `nethost::dial_loopback` = reused SINGLETON, `remote_id_hex` = own-node id; `open_stream` on a `ConnKind::Loopback` mints the cross-wired PAIR (operator row + peer row); peer row (initiated_locally=false, own origin) → dispatch loop → serve_attach. Explicit `net-dial-loopback` frame (brain.net_dial_loopback + broker.dispatch_net_dial_loopback) — net_dial untouched. access same-node short-circuit (access.rs:89) Allows own-node → NO access change. rc.rs ONE self-dial line swapped → net_dial_loopback. ONE-PUMP PROOF by construction: shared `attach_drive_detach` helper called by BOTH cross-node `remote_attach_drives_a_real_pty_cross_daemon` AND new `local_attach_via_loopback_conn_rides_the_same_pump` — diverge only in conn acquisition + expected_origin. EVIDENCE: attach int 6/6 + new nethost unit `loopback_conn_is_a_reused_singleton_that_cross_wires_a_stream_pair`; full spt+spt-daemon suite **498 passed/0 failed**; clippy -D EXIT=0; traceable EXIT=0 (REQ-RC-1 [OK] impl+unit+int). **UNCOMMITTED** (operator-gated, m12-w1-bringup-rc).

**NEXT (after W1.5 gate verdict):** **REQ-HOST-RUN-2** (project cwd via additive `SpawnReq.cwd`) before M12 close. Then W2 picker / W3 manifest / W4 subnet QR / W5 whoami. Commit only when user asks (not on default branch unprompted). See [[traceable-per-wave-activation]], [[pump-ipc-deadline-fix]], [[spt-claude-code-next]] (perri waits on M12 gate).


The file C:\Users\decid\.ccs\instances\bigscreen\projects\C--Users-decid-Documents-projects-spt-core\memory\m12-w1-progress.md has been updated successfully. (file state is current in your context — no need to Read it back)

**W1.5 BUILT (todlando 2026-06-14) — gate pinged to doyle, verdict pending.** B1 broker loopback-conn done (design in `M12-W1.5-PLAN.md` "Crystallized design"). Transport seam = `tokio::io::duplex` (ONE full-duplex pair wires a whole operator↔peer stream); only the leaf send/recv become enums (`SendHalf`/`RecvHalf`), so StreamLog/subscribe/send_stream/read-pump are byte-identical across QUIC + loopback. `nethost::dial_loopback` = reused SINGLETON, `remote_id_hex` = own-node id; `open_stream` on a `ConnKind::Loopback` mints the cross-wired PAIR (operator row + peer row); peer row (initiated_locally=false, own origin) → dispatch loop → serve_attach. Explicit `net-dial-loopback` frame (brain.net_dial_loopback + broker.dispatch_net_dial_loopback) — net_dial untouched. access same-node short-circuit (access.rs:89) Allows own-node → NO access change. rc.rs ONE self-dial line swapped → net_dial_loopback. ONE-PUMP PROOF by construction: shared `attach_drive_detach` helper called by BOTH cross-node `remote_attach_drives_a_real_pty_cross_daemon` AND new `local_attach_via_loopback_conn_rides_the_same_pump` — diverge only in conn acquisition + expected_origin. EVIDENCE: attach int 6/6 + new nethost unit `loopback_conn_is_a_reused_singleton_that_cross_wires_a_stream_pair`; full spt+spt-daemon suite **498 passed/0 failed**; clippy -D EXIT=0; traceable EXIT=0 (REQ-RC-1 [OK] impl+unit+int). **UNCOMMITTED** (operator-gated, m12-w1-bringup-rc).

**NEXT (after W1.5 gate verdict):** **REQ-HOST-RUN-2** (project cwd via additive `SpawnReq.cwd`) before M12 close. Then W2 picker / W3 manifest / W4 subnet QR / W5 whoami. Commit only when user asks (not on default branch unprompted). See [[traceable-per-wave-activation]], [[pump-ipc-deadline-fix]], [[spt-claude-code-next]] (perri waits on M12 gate).

---
name: m12-w1-progress
description: "M12 Wave 1 (spt endpoint run + spt rc) build state, decisions, and the self-dial-dead blocker"
metadata:
  node_type: memory
  type: project
  originSessionId: 1ae6a063-4622-4f5b-86b8-b4506d12ccfb
---

M12 W1 (spt-hosted harness bringup + `spt rc` PTY attach) — doyle handed execution to todlando 2026-06-14. Full spec: `M12-PLAN.md` + `M12-W1-PLAN.md` (durable build plan, resumable).

**Built + green:**
- Daemon `spt-daemon/src/harnesshost.rs`: `prepare_harness_spawn` (validate kind=harness + fill `[session.self]` template) + `launch_harness_brokered_in` (spawn into broker PTY via `spawn_session_pid`, mirrors `shellhost::launch_shell_brokered_in`) + `mint_session_id`. 3 unit tests.
- `spt/src/rc.rs`: operator attach pump (single-Brain main thread + stdin reader thread→mpsc, `cold_start_pump` Split carrier for bounded reads, seq-dedup, ctrl-b detach state machine). 3 unit tests.
- cli.rs: `EndpointCmd::Run` + `cmd_endpoint_run`; top-level `Cmd::Rc`; crossterm dep added.
- REQs registered: REQ-HOST-RUN-1 (spawn), REQ-RC-1 (attach), REQ-HOST-RUN-2 (project cwd — doyle-mandated, must land before M12 gate; W1 ships broker-inherited cwd as proof-shortcut only).

**KEY DECISIONS (doyle-validated):** spawn-first (harness self-registers its perch on bind via `api/startup.rs establish_perch`, no daemon pre-register); id rides argv via `{id}` template (NO SpawnReq wire change); one-pump invariant = transport swappable under one `serve_attach` pump.

**BLOCKER RESOLVED (doyle ruling 2026-06-14):** loopback self-dial IMPOSSIBLE (iroh refuses `net_dial(own_addr)`, pinned green test). doyle picked **B1 (broker in-process loopback-conn — local rides byte-identical pump+serve path, covered by cross-node tests; B2's pump-branch rejected as the lesser-tested local path warned against), scoped as a tight W1.5** (NOT in W1). Rationale: cross-node attach + endpoint-run bringup + pump are built+green → gate W1 NOW (bringup verifiable via cross-node attach to the spawned PTY); B1 is a sensitive broker-layer addition (6.7 zone) deserving its own focused task+gate; local lands W1.5 before M12 close. One-pump assertion (both transports → one pump) kept in W1.5.

**REQ-RC-1 split:** cross-node int evidence at W1 gate; local int evidence at W1.5 (per-wave activation).

**W1 GATE: PASS (doyle 2026-06-14).** Independently verified: traceable-reqs check reproduced (196 complete / 0 findings; REQ-HOST-RUN-1 + REQ-RC-1 impl+unit+int, REQ-HOST-RUN-2 []); int test `attach.rs:217` inspected — real `launch_harness_brokered_in` spawns [session.self] into broker-A PTY, B attaches cross-node, HOSTED_OK round-trips, detach→Detached+survives, origin=handshake-identity (KH 7.5); self-dial-refused pin genuine. Trusted reported full-suite (46 bins/0) + clippy -D. **UNCOMMITTED** (awaiting operator).

**NEXT:** **W1.5** (resumable JIT plan = `M12-W1.5-PLAN.md`, has B1 design + broker.rs/nethost.rs/dispatch.rs investigation TODOs) = build B1 broker loopback-conn (local attach rides byte-identical pump) + swap rc.rs's ONE self-dial line to loopback-dial (rc.rs currently codes the now-refused self-dial → LOCAL `spt rc` errors at runtime until then; cross-node works now) + local attach E2E int + "both transports → one pump" assertion → activate REQ-RC-1 local int → traceable check → W1.5 gate. Then **REQ-HOST-RUN-2** (project cwd via additive SpawnReq.cwd) before M12 close. Then W2 picker / W3 manifest / W4 subnet QR / W5 whoami. Commit only when user asks (not on default branch unprompted). See [[traceable-per-wave-activation]], [[pump-ipc-deadline-fix]], [[spt-claude-code-next]] (perri waits on M12 gate).

-**NEXT:** **W1.5** (resumable JIT plan = `M12-W1.5-PLAN.md`, has B1 design + broker.rs/nethost.rs/dispatch.rs investigation TODOs) = build B1 broker loopback-conn (local attach rides byte-identical pump) + swap rc.rs's ONE self-dial line to loopback-dial (rc.rs currently codes the now-refused self-dial → LOCAL `spt rc` errors at runtime until then; cross-node works now) + local attach E2E int + "both transports → one pump" assertion → activate REQ-RC-1 local int → traceable check → W1.5 gate. Then **REQ-HOST-RUN-2** (project cwd via additive SpawnReq.cwd) before M12 close. Then W2 picker / W3 manifest / W4 subnet QR / W5 whoami. Commit only when user asks (not on default branch unprompted). See [[traceable-per-wave-activation]], [[pump-ipc-deadline-fix]], [[spt-claude-code-next]] (perri waits on M12 gate).

+**W1.5 BUILT (todlando 2026-06-14) — gate pinged to doyle, verdict pending.** B1 broker loopback-conn done (design in `M12-W1.5-PLAN.md` "Crystallized design"). Transport seam = `tokio::io::duplex` (ONE full-duplex pair wires a whole operator↔peer stream); only the leaf send/recv become enums (`SendHalf`/`RecvHalf`), so StreamLog/subscribe/send_stream/read-pump are byte-identical across QUIC + loopback. `nethost::dial_loopback` = reused SINGLETON, `remote_id_hex` = own-node id; `open_stream` on a `ConnKind::Loopback` mints the cross-wired PAIR (operator row + peer row); peer row (initiated_locally=false, own origin) → dispatch loop → serve_attach. Explicit `net-dial-loopback` frame (brain.net_dial_loopback + broker.dispatch_net_dial_loopback) — net_dial untouched. access same-node short-circuit (access.rs:89) Allows own-node → NO access change. rc.rs ONE self-dial line swapped → net_dial_loopback. ONE-PUMP PROOF by construction: shared `attach_drive_detach` helper called by BOTH cross-node `remote_attach_drives_a_real_pty_cross_daemon` AND new `local_attach_via_loopback_conn_rides_the_same_pump` — diverge only in conn acquisition + expected_origin. EVIDENCE: attach int 6/6 + new nethost unit `loopback_conn_is_a_reused_singleton_that_cross_wires_a_stream_pair`; full spt+spt-daemon suite **498 passed/0 failed**; clippy -D EXIT=0; traceable EXIT=0 (REQ-RC-1 [OK] impl+unit+int). **UNCOMMITTED** (operator-gated, m12-w1-bringup-rc).

+**NEXT (after W1.5 gate verdict):** **REQ-HOST-RUN-2** (project cwd via additive `SpawnReq.cwd`) before M12 close. Then W2 picker / W3 manifest / W4 subnet QR / W5 whoami. Commit only when user asks (not on default branch unprompted). See [[traceable-per-wave-activation]], [[pump-ipc-deadline-fix]], [[spt-claude-code-next]] (perri waits on M12 gate).


Update MEMORY.md index pointer:


- [M12 W1 progress](m12-w1-progress.md) — M12 W1 (spt endpoint run + spt rc) GATE PASS by doyle 2026-06-14 (traceable reproduced 196/0; int test independently inspected — real launch_harness_brokered_in bringup + cross-node attach + survives-detach). UNCOMMITTED pending operator. NEXT = W1.5 (B1 broker loopback-conn = local attach) → REQ-HOST-RUN-2 (cwd) before M12 close

- [M12 W1 progress](m12-w1-progress.md) — M12 W1 GATE PASS (doyle 2026-06-14). **W1.5 BUILT** (B1 broker loopback-conn via tokio::io::duplex; local `spt rc` rides byte-identical pump; one-pump proven by shared attach_drive_detach helper; suite 498/0, clippy+traceable EXIT=0, REQ-RC-1 [OK] impl+unit+int) — gate pinged doyle, verdict pending. UNCOMMITTED. NEXT = REQ-HOST-RUN-2 (cwd) before M12 close


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


1	- [M12 W1 progress](m12-w1-progress.md) — M12 W1 (spt endpoint run + spt rc) GATE PASS by doyle 2026-06-14 (traceable reproduced 196/0; int test independently inspected — real launch_harness_brokered_in bringup + cross-node attach + survives-detach). UNCOMMITTED pending operator. NEXT = W1.5 (B1 broker loopback-conn = local attach) → REQ-HOST-RUN-2 (cwd) before M12 close
2	- [M9 status](m9-wave1-progress.md) — M9 COMPLETE + SHIPPED: all 4 gates passed (G1/G2/Gateway-WAN/G3), all PRs merged to main (operator GO 2026-06-13), **v0.5.0 tagged** (counter 10, deployah signing). Don't re-plan. See [[v050-published]] for release state
3	- [spt-claude-code NEXT](spt-claude-code-next.md) — SCOPING DONE 2026-06-14 (SCOPE.md/CONTEXT.md committed @09d6020). M12 handed to todlando as GATING prereq (now executing W1). RESUME = author experimplate → instantiate spt-claude-code → write perri HANDOFF. perri builds from spt-releases+GH-Pages ONLY

- [M12 W1 progress](m12-w1-progress.md) — M12 W1 (spt endpoint run + spt rc) GATE PASS by doyle 2026-06-14 (traceable reproduced 196/0; int test independently inspected — real launch_harness_brokered_in bringup + cross-node attach + survives-detach). UNCOMMITTED pending operator. NEXT = W1.5 (B1 broker loopback-conn = local attach) → REQ-HOST-RUN-2 (cwd) before M12 close
- [M9 status](m9-wave1-progress.md) — M9 COMPLETE + SHIPPED: all 4 gates passed (G1/G2/Gateway-WAN/G3), all PRs merged to main (operator GO 2026-06-13), **v0.5.0 tagged** (counter 10, deployah signing). Don't re-plan. See [[v050-published]] for release state
- [spt-claude-code NEXT](spt-claude-code-next.md) — SCOPING DONE 2026-06-14 (SCOPE.md/CONTEXT.md committed @09d6020). M12 handed to todlando as GATING prereq (now executing W1). RESUME = author experimplate → instantiate spt-claude-code → write perri HANDOFF. perri builds from spt-releases+GH-Pages ONLY


- [M12 W1 progress](m12-w1-progress.md) — M12 W1 (spt endpoint run + spt rc) GATE PASS by doyle 2026-06-14 (traceable reproduced 196/0; int test independently inspected — real launch_harness_brokered_in bringup + cross-node attach + survives-detach). UNCOMMITTED pending operator. NEXT = W1.5 (B1 broker loopback-conn = local attach) → REQ-HOST-RUN-2 (cwd) before M12 close

- [M12 W1 progress](m12-w1-progress.md) — M12 W1 GATE PASS (doyle 2026-06-14). **W1.5 BUILT** (B1 broker loopback-conn via tokio::io::duplex; local `spt rc` rides byte-identical pump; one-pump proven by shared attach_drive_detach helper; suite 498/0, clippy+traceable EXIT=0, REQ-RC-1 [OK] impl+unit+int) — gate pinged doyle, verdict pending. UNCOMMITTED. NEXT = REQ-HOST-RUN-2 (cwd) before M12 close


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

- [M12 W1 progress](m12-w1-progress.md) — M12 W1 (spt endpoint run + spt rc) GATE PASS by doyle 2026-06-14 (traceable reproduced 196/0; int test independently inspected — real launch_harness_brokered_in bringup + cross-node attach + survives-detach). UNCOMMITTED pending operator. NEXT = W1.5 (B1 broker loopback-conn = local attach) → REQ-HOST-RUN-2 (cwd) before M12 close
- [M9 status](m9-wave1-progress.md) — M9 COMPLETE + SHIPPED: all 4 gates passed (G1/G2/Gateway-WAN/G3), all PRs merged to main (operator GO 2026-06-13), **v0.5.0 tagged** (counter 10, deployah signing). Don't re-plan. See [[v050-published]] for release state
- [spt-claude-code NEXT](spt-claude-code-next.md) — SCOPING DONE 2026-06-14 (SCOPE.md/CONTEXT.md committed @09d6020). M12 handed to todlando as GATING prereq (now executing W1). RESUME = author experimplate → instantiate spt-claude-code → write perri HANDOFF. perri builds from spt-releases+GH-Pages ONLY
- [M10 digest extractor](m10-digest-extractor.md) — doyle GATE PASS 2026-06-13: all 4 waves (39463a2..afa0561), REQ-TERM-5/6/7 evidenced, full suite + capstone E2E green, independently verified. `[digest]` imperative extractor seam (ADR-0019, reverses M9) + thread-spanning (session ledger) + two-origin merge + digest-proof + skip-diagnostics. SHIPPED as v0.6.0 (counter 11) — see [[v060-published]]. Carried: real-CC digest-proof → perri/spt-claude-code acceptance criterion
- [v0.6.0 published](v060-published.md) — M10 PUBLIC 2026-06-13 (counter 11); todlando merge+tag, deployah sign+publish; minor bump (0.5.0→0.6.0); hashes linux 002ed710… win 4f5c61e8…. ROUTINE seamless roll now (respawn-path arc closed); publisher leg proven 5× (counter 7-11). enlyzeam still pending natural-propagation confirm
- [traceable per-wave activation](traceable-per-wave-activation.md) — traceable-reqs check requires ALL declared required_stages evidenced per commit; activate stages incrementally as evidence lands (int at final wave), never the full end-state list upfront
- [v0.5.0 published](v050-published.md) — M9 PUBLIC 2026-06-13 (counter 10); todlando drove merge+tag, deployah signed+published (handoff split vs v0.4.x full-solo); minor bump (breaking [pty_digest] removal pre-1.0); hashes linux 4c079100… win 984298ba…. FLEET ROLL = next phase, operator-gated (their ask was merge+publish, DONE). v0.4.2→v0.5.0 kitsubito apply = FIRST genuine Linux seamless no-bounce proof on fixed broker; bytes-assert→auto-rollback armed. deployah on-call for hash check
- [v0.5.0 release](v050-release.md) — M9 SHIPPED: merged to main (22a9b6f, clean stack collapse; #8 auto-closed by mid-stack --delete-branch, content rode via #9), v0.5.0 tagged (0.4.2→0.5.0 minor: feature milestone + breaking [pty_digest] removal), counter 10, deployah signing. LESSON: don't --delete-branch mid-stack
- [M9 Wave3/G3 digest](m9-wave3-digest-progress.md) — session-digest re-founded on session logs (REQ-TERM-4, ADR-0008 amendment); G3 STAMPED PASS by doyle 2026-06-13, PR#10 held. On-demand projection (topology-independent, no PTY); PTY-parse retired; published digest-record contract; autonomous-freshness+Option-C deferred to consuming-frontend milestone (DEFERRED.md:44)
- [hfenduleam target bloat → CI RED](hfenduleam-target-bloat-ci-red.md) — Win-runner LNK1318/os-error-112 can be disk-full from cargo target bloat (396GB seen), NOT a code regression; cargo clean + re-run; check disk before treating a Windows RED as a code defect
- [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
- [CI self-host migration](ci-selfhost-migration.md) — DONE: runners live on kitsubito + hfenduleam (gravity retired 2026-06-07); twohost jobs gated by [twohost] commit tag; don't re-plan
- [which process is the listener](spt-daemon-is-live-infra.md) — owl.exe (plugin) = live listener, hands off; target\debug\spt.exe daemon = dev/test artifact, killable
- [self-clear skill API-blocked](self-clear-skill-api-blocked.md) — never invoke /self-clear; usage-policy classifier refuses the turn; continue in-context / user-manual /clear instead
- [Adapter glue-model boundary](adapter-glue-model-boundary.md) — adapters = manifest + binary only; real adapters in own repos; no adapter-aware source in spt-core
- [M6 shipped v0.1.0](m6-shipped-v010.md) — v0.1.0 public 2026-06-05; release procedure = RELEASE-RUNBOOK.md; releases-repo/ is repo-face truth; don't re-plan M6
- [Gravity deploy procedure](gravity-deploy-procedure.md) — ssh david@100.100.62.43; build in ~/spt-core-deploy NOT runner workspace; setsid daemon; ~1 min first-sync after restart
- [Linux SPT_HOME no-bug](linux-spt-home-no-bug.md) — unix spawn_detached inherits env (no env_clear), forwards SPT_HOME; only Win CreateEnvironmentBlock rebuilds/drops it (f99dc25). Default-home daemon = launched w/o the var, not a propagation bug; don't reopen
- [kitsubito Linux rig](kitsubito-linux-rig.md) — ssh reavus@kitsubito (key auth); cargo at ~/.cargo/bin; deploy checkout origin=local runner (NOT GitHub) so unpushed commits transfer via git bundle; live spt-daemon user unit, --user ops need no sudo; build-in-scratch leaves ~/.local/bin/spt untouched
- [Use $OWL send for messaging](owl-send-not-legacy-spt-send.md) — `$OWL send` works (legacy = where live perches run); `spt send` is the NEW spt-core binary but no perches on it yet → bounces NO_PERCH; don't read `$?` after piping send through tail (use PIPESTATUS)
- [daemon service-detection gotcha](daemon-service-detection-gotcha.md) — service detection reading GLOBAL OS state is blind on a single-home non-elevated dev box; gate Linux service routing on SPT_HOME-unset, Windows run-refuse on would-vanish (not is_elevated); push→CI BEFORE tagging a release (v0.3.0 tagged early, CI caught 2 regressions, retagged)
- [v0.3.2 platform-safe update](v032-platform-safe-update.md) — REQ-UPD-8 PUBLISHED + rig-verified 2026-06-09 (counter 6): fetch stages multi-platform update-set, apply refuses unstamped/wrong-OS single. Fleet kitsubito+hfenduleam=0.3.2 (ENLYZEAM TODO). GOTCHA: fetch/pump floor=staged_version → can't re-stage same version single→set; clear `$SPT_HOME/releases` first (Linux ~/.spt-core, Win %LOCALAPPDATA%\spt-core). CORRECTION: release-publish never built a SignedUpdateSet pre-v0.3.2 (was debug_rollout)
- [boot-race self-heal](boot-race-self-heal.md) — v0.3.1: daemon self-heals a net-less boot start (NetHost retry+attach to OnceLock broker); systemd-USER network-online ordering does NOT reliably prevent the race under linger (field-proven kitsubito) — the self-heal is the real guarantee, unit ordering is belt
- [v0.4.0 published + fleet rolled](v040-published-fleet-rolled.md) — restoration release PUBLIC 2026-06-10 (counter 7); D7-4 bytes-match done both nodes (brain.ready exe_hash == artifact); v0.3.2→v0.4.0 apply needed last manual bounce (in-process can't self-relaunch); seamless no-bounce proof + enlyzeam catch-up + milestone close all deferred to v0.4.1
- [v0.4.1 published](v041-published.md) — PUBLIC 2026-06-11 (counter 8); deployah drove full merge→tag→sign→publish solo; D7 SEAMLESS no-bounce fleet-verify payload (apply must roll bytes live w/ pid change, no manual bounce); acceptance hashes linux 35c9544c… win 728a1bfc…; hfenduleam(Win) GREEN no-bounce, kitsubito(Linux) RED → CLOSE BLOCKED: v0.4.0 broker respawns via current_exe() which follows apply-rename to .old-8=OLD bytes (Linux only); artifacts SOUND don't yank; v0.4.2 fix = respawn by canonical path + bytes-assert, needs manual Linux broker bounce; deployah takes counter 9 publish
- [hfenduleam disk-full CI](hfenduleam-disk-full-ci.md) — Win CI LNK1318/os-error-112 = disk full not code; cargo clean dev target (was 396GB), rerun job
- [M9 gateway-wan progress](m9-gateway-wan-progress.md) — REQ-MSG-6 cross-node Gateway WAN user-msg; PR #9 open (base m9-user-surfaces), impl complete + local sweep green, HOLDING on CI; PR #8 G2 now green post disk-fix

-- [M12 W1 progress](m12-w1-progress.md) — M12 W1 (spt endpoint run + spt rc) GATE PASS by doyle 2026-06-14 (traceable reproduced 196/0; int test independently inspected — real launch_harness_brokered_in bringup + cross-node attach + survives-detach). UNCOMMITTED pending operator. NEXT = W1.5 (B1 broker loopback-conn = local attach) → REQ-HOST-RUN-2 (cwd) before M12 close

+- [M12 W1 progress](m12-w1-progress.md) — M12 W1 GATE PASS (doyle 2026-06-14). **W1.5 BUILT** (B1 broker loopback-conn via tokio::io::duplex; local `spt rc` rides byte-identical pump; one-pump proven by shared attach_drive_detach helper; suite 498/0, clippy+traceable EXIT=0, REQ-RC-1 [OK] impl+unit+int) — gate pinged doyle, verdict pending. UNCOMMITTED. NEXT = REQ-HOST-RUN-2 (cwd) before M12 close


W1.5 build complete. Memory + plan updated.

**W1.5 DONE — gate pinged doyle, verdict pending:**

| Piece | State |
|---|---|
| B1 broker loopback-conn (`tokio::io::duplex` seam, only leaf send/recv enums) | ✅ |
| `dial_loopback` singleton, own-node origin | ✅ |
| `open_stream` cross-wired pair (operator + peer rows) | ✅ |
| Explicit `net-dial-loopback` frame (net_dial untouched) | ✅ |
| access same-node short-circuit → no access change | ✅ confirmed |
| rc.rs self-dial → loopback-dial swap | ✅ |
| One-pump by construction (shared `attach_drive_detach` helper, both transports) | ✅ |

**Evidence:** attach int 6/6 + new nethost unit; full suite **498/0**; clippy -D EXIT=0; traceable EXIT=0 (REQ-RC-1 [OK] impl+unit+int). UNCOMMITTED (operator-gated).

Next after verdict = REQ-HOST-RUN-2 (project cwd). Awaiting doyle gate reply via poll.


<task-notification>
<task-id>bmby1p3hm</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/4" id="215fbc78" type="msg" from="doyle">W1.5 GATE: PASS. ✅ Independently verified — all 3 rulings honored:

1. Origin (Q1) — local test's origin = dialed.remote_id_hex = own broker-minted id; helper asserts origin==expected_origin (KH 7.5 stream-table, not payload). Invariant documented. access same-node short-circuit Allow = no access change. ✓</EVENT-PART>
<EVENT-PART seq="2/4" id="215fbc78">
2. Frame (Q2) — explicit net_dial_loopback (brain + dispatch_net_dial_loopback); net_dial stays pristine. ✓
3. One-pump by-construction (Q3) — VERIFIED: read attach_drive_detach (attach.rs:131) — the whole request→serve→drive→detach flow lives there; both the cross-node test (:386) and the new local test (:436) call the ID</EVENT-PART>
<EVENT-PART seq="3/4" id="215fbc78">ENTICAL helper, diverging ONLY in conn acquisition + expected_origin. No second path to drift. This is exactly the proof I asked for. ✓

traceable reproduced (REQ-RC-1 impl+unit+int [OK]; REQ-HOST-RUN-2 []). Trusting your 498/0 suite + clippy -D — I reproduced the trace + read both int tests + the shared helper; the load-bearing clai</EVENT-PART>
<EVENT-PART seq="4/4" id="215fbc78">m is real.

LOCAL spt rc now rides the byte-identical pump — the cc launcher's local-attach foundation exists.

PROCEED: REQ-HOST-RUN-2 (project cwd, additive SpawnReq.cwd) — the last must-land before the M12 gate. Stays uncommitted on m12-w1-bringup-rc until the operator calls it. Strong work. — doyle</EVENT-PART></event>
</task-notification>


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


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


<task-notification>
<task-id>bp43y3i9s</task-id>
<tool-use-id>toolu_01D3WWPz3e3d3XBNECiuM3Lw</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\bp43y3i9s.output</output-file>
<status>completed</status>
<summary>Background command "Ack doyle W1.5 gate pass" completed (exit code 0)</summary>
</task-notification>


W1.5 **GATE PASS** (doyle, all 3 rulings independently verified). Proceeding to REQ-HOST-RUN-2 (project cwd, additive `SpawnReq.cwd`) — doyle-mandated last must-land before M12 gate, stays uncommitted.

Ack doyle + update memory, then investigate the spawn path:


Bash

$OWL send doyle todlando <<'EOF'
W1.5 GATE PASS received — thanks for the independent verify (helper + both int tests + trace). Proceeding to REQ-HOST-RUN-2: additive `SpawnReq.cwd`, broker spawn honors it, harnesshost passes the project dir, activate stages, tests. Uncommitted on m12-w1-bringup-rc until operator calls it. Ping at the REQ-HOST-RUN-2 / M12 gate.
EOF

Ack doyle W1.5 gate pass


Command running in background with ID: bp43y3i9s. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\bp43y3i9s.output. You will be notified when it completes. To check interim output, use Read on that file path.


Prompt:
<task-notification>
<task-id>bp43y3i9s</task-id>
<tool-use-id>toolu_01D3WWPz3e3d3XBNECiuM3Lw</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\bp43y3i9s.output</output-file>
<status>completed</status>
<summary>Background command "Ack doyle W1.5 gate pass" completed (exit code 0)</summary>
</task-notification>


1	- [M12 W1 progress](m12-w1-progress.md) — M12 W1 + **W1.5 GATE PASS** (doyle 2026-06-14, independently verified: traceable reproduced, shared attach_drive_detach helper read — both int tests call it, diverge only in conn+origin; local `spt rc` rides byte-identical pump via B1 broker loopback-conn). suite 498/0. UNCOMMITTED on m12-w1-bringup-rc. NEXT = REQ-HOST-RUN-2 (cwd via additive SpawnReq.cwd) — last must-land before M12 gate
2	- [M9 status](m9-wave1-progress.md) — M9 COMPLETE + SHIPPED: all 4 gates passed (G1/G2/Gateway-WAN/G3), all PRs merged to main (operator GO 2026-06-13), **v0.5.0 tagged** (counter 10, deployah signing). Don't re-plan. See [[v050-published]] for release state
3	- [spt-claude-code NEXT](spt-claude-code-next.md) — SCOPING DONE 2026-06-14 (SCOPE.md/CONTEXT.md committed @09d6020). M12 handed to todlando as GATING prereq (now executing W1). RESUME = author experimplate → instantiate spt-claude-code → write perri HANDOFF. perri builds from spt-releases+GH-Pages ONLY
4	- [M10 digest extractor](m10-digest-extractor.md) — doyle GATE PASS 2026-06-13: all 4 waves (39463a2..afa0561), REQ-TERM-5/6/7 evidenced, full suite + capstone E2E green, independently verified. `[digest]` imperative extractor seam (ADR-0019, reverses M9) + thread-spanning (session ledger) + two-origin merge + digest-proof + skip-diagnostics. SHIPPED as v0.6.0 (counter 11) — see [[v060-published]]. Carried: real-CC digest-proof → perri/spt-claude-code acceptance criterion
5	- [v0.6.0 published](v060-published.md) — M10 PUBLIC 2026-06-13 (counter 11); todlando merge+tag, deployah sign+publish; minor bump (0.5.0→0.6.0); hashes linux 002ed710… win 4f5c61e8…. ROUTINE seamless roll now (respawn-path arc closed); publisher leg proven 5× (counter 7-11). enlyzeam still pending natural-propagation confirm
6	- [traceable per-wave activation](traceable-per-wave-activation.md) — traceable-reqs check requires ALL declared required_stages evidenced per commit; activate stages incrementally as evidence lands (int at final wave), never the full end-state list upfront
7	- [v0.5.0 published](v050-published.md) — M9 PUBLIC 2026-06-13 (counter 10); todlando drove merge+tag, deployah signed+published (handoff split vs v0.4.x full-solo); minor bump (breaking [pty_digest] removal pre-1.0); hashes linux 4c079100… win 984298ba…. FLEET ROLL = next phase, operator-gated (their ask was merge+publish, DONE). v0.4.2→v0.5.0 kitsubito apply = FIRST genuine Linux seamless no-bounce proof on fixed broker; bytes-assert→auto-rollback armed. deployah on-call for hash check
8	- [v0.5.0 release](v050-release.md) — M9 SHIPPED: merged to main (22a9b6f, clean stack collapse; #8 auto-closed by mid-stack --delete-branch, content rode via #9), v0.5.0 tagged (0.4.2→0.5.0 minor: feature milestone + breaking [pty_digest] removal), counter 10, deployah signing. LESSON: don't --delete-branch mid-stack
9	- [M9 Wave3/G3 digest](m9-wave3-digest-progress.md) — session-digest re-founded on session logs (REQ-TERM-4, ADR-0008 amendment); G3 STAMPED PASS by doyle 2026-06-13, PR#10 held. On-demand projection (topology-independent, no PTY); PTY-parse retired; published digest-record contract; autonomous-freshness+Option-C deferred to consuming-frontend milestone (DEFERRED.md:44)
10	- [hfenduleam target bloat → CI RED](hfenduleam-target-bloat-ci-red.md) — Win-runner LNK1318/os-error-112 can be disk-full from cargo target bloat (396GB seen), NOT a code regression; cargo clean + re-run; check disk before treating a Windows RED as a code defect
11	- [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]]
12	- [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
13	- [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
14	- [CI self-host migration](ci-selfhost-migration.md) — DONE: runners live on kitsubito + hfenduleam (gravity retired 2026-06-07); twohost jobs gated by [twohost] commit tag; don't re-plan
15	- [which process is the listener](spt-daemon-is-live-infra.md) — owl.exe (plugin) = live listener, hands off; target\debug\spt.exe daemon = dev/test artifact, killable
16	- [self-clear skill API-blocked](self-clear-skill-api-blocked.md) — never invoke /self-clear; usage-policy classifier refuses the turn; continue in-context / user-manual /clear instead
17	- [Adapter glue-model boundary](adapter-glue-model-boundary.md) — adapters = manifest + binary only; real adapters in own repos; no adapter-aware source in spt-core
18	- [M6 shipped v0.1.0](m6-shipped-v010.md) — v0.1.0 public 2026-06-05; release procedure = RELEASE-RUNBOOK.md; releases-repo/ is repo-face truth; don't re-plan M6
19	- [Gravity deploy procedure](gravity-deploy-procedure.md) — ssh david@100.100.62.43; build in ~/spt-core-deploy NOT runner workspace; setsid daemon; ~1 min first-sync after restart
20	- [Linux SPT_HOME no-bug](linux-spt-home-no-bug.md) — unix spawn_detached inherits env (no env_clear), forwards SPT_HOME; only Win CreateEnvironmentBlock rebuilds/drops it (f99dc25). Default-home daemon = launched w/o the var, not a propagation bug; don't reopen
21	- [kitsubito Linux rig](kitsubito-linux-rig.md) — ssh reavus@kitsubito (key auth); cargo at ~/.cargo/bin; deploy checkout origin=local runner (NOT GitHub) so unpushed commits transfer via git bundle; live spt-daemon user unit, --user ops need no sudo; build-in-scratch leaves ~/.local/bin/spt untouched
22	- [Use $OWL send for messaging](owl-send-not-legacy-spt-send.md) — `$OWL send` works (legacy = where live perches run); `spt send` is the NEW spt-core binary but no perches on it yet → bounces NO_PERCH; don't read `$?` after piping send through tail (use PIPESTATUS)
23	- [daemon service-detection gotcha](daemon-service-detection-gotcha.md) — service detection reading GLOBAL OS state is blind on a single-home non-elevated dev box; gate Linux service routing on SPT_HOME-unset, Windows run-refuse on would-vanish (not is_elevated); push→CI BEFORE tagging a release (v0.3.0 tagged early, CI caught 2 regressions, retagged)
24	- [v0.3.2 platform-safe update](v032-platform-safe-update.md) — REQ-UPD-8 PUBLISHED + rig-verified 2026-06-09 (counter 6): fetch stages multi-platform update-set, apply refuses unstamped/wrong-OS single. Fleet kitsubito+hfenduleam=0.3.2 (ENLYZEAM TODO). GOTCHA: fetch/pump floor=staged_version → can't re-stage same version single→set; clear `$SPT_HOME/releases` first (Linux ~/.spt-core, Win %LOCALAPPDATA%\spt-core). CORRECTION: release-publish never built a SignedUpdateSet pre-v0.3.2 (was debug_rollout)
25	- [boot-race self-heal](boot-race-self-heal.md) — v0.3.1: daemon self-heals a net-less boot start (NetHost retry+attach to OnceLock broker); systemd-USER network-online ordering does NOT reliably prevent the race under linger (field-proven kitsubito) — the self-heal is the real guarantee, unit ordering is belt
26	- [v0.4.0 published + fleet rolled](v040-published-fleet-rolled.md) — restoration release PUBLIC 2026-06-10 (counter 7); D7-4 bytes-match done both nodes (brain.ready exe_hash == artifact); v0.3.2→v0.4.0 apply needed last manual bounce (in-process can't self-relaunch); seamless no-bounce proof + enlyzeam catch-up + milestone close all deferred to v0.4.1

... [3 lines truncated] ...


Memory already reflects GATE PASS (psyche sync). Update detail file gate-pending → PASS, then investigate spawn path for REQ-HOST-RUN-2:


Edit

**W1.5 BUILT (todlando 2026-06-14) — gate pinged to doyle, verdict pending.** B1 broker loopback-conn done

**W1.5 GATE PASS (doyle 2026-06-14, independently verified — read shared helper + both int tests + reproduced trace).** B1 broker loopback-conn done


---
name: m12-w1-progress
description: "M12 Wave 1 (spt endpoint run + spt rc) build state, decisions, and the self-dial-dead blocker"
metadata:
  node_type: memory
  type: project
  originSessionId: 1ae6a063-4622-4f5b-86b8-b4506d12ccfb
---

M12 W1 (spt-hosted harness bringup + `spt rc` PTY attach) — doyle handed execution to todlando 2026-06-14. Full spec: `M12-PLAN.md` + `M12-W1-PLAN.md` (durable build plan, resumable).

**Built + green:**
- Daemon `spt-daemon/src/harnesshost.rs`: `prepare_harness_spawn` (validate kind=harness + fill `[session.self]` template) + `launch_harness_brokered_in` (spawn into broker PTY via `spawn_session_pid`, mirrors `shellhost::launch_shell_brokered_in`) + `mint_session_id`. 3 unit tests.
- `spt/src/rc.rs`: operator attach pump (single-Brain main thread + stdin reader thread→mpsc, `cold_start_pump` Split carrier for bounded reads, seq-dedup, ctrl-b detach state machine). 3 unit tests.
- cli.rs: `EndpointCmd::Run` + `cmd_endpoint_run`; top-level `Cmd::Rc`; crossterm dep added.
- REQs registered: REQ-HOST-RUN-1 (spawn), REQ-RC-1 (attach), REQ-HOST-RUN-2 (project cwd — doyle-mandated, must land before M12 gate; W1 ships broker-inherited cwd as proof-shortcut only).

**KEY DECISIONS (doyle-validated):** spawn-first (harness self-registers its perch on bind via `api/startup.rs establish_perch`, no daemon pre-register); id rides argv via `{id}` template (NO SpawnReq wire change); one-pump invariant = transport swappable under one `serve_attach` pump.

**BLOCKER RESOLVED (doyle ruling 2026-06-14):** loopback self-dial IMPOSSIBLE (iroh refuses `net_dial(own_addr)`, pinned green test). doyle picked **B1 (broker in-process loopback-conn — local rides byte-identical pump+serve path, covered by cross-node tests; B2's pump-branch rejected as the lesser-tested local path warned against), scoped as a tight W1.5** (NOT in W1). Rationale: cross-node attach + endpoint-run bringup + pump are built+green → gate W1 NOW (bringup verifiable via cross-node attach to the spawned PTY); B1 is a sensitive broker-layer addition (6.7 zone) deserving its own focused task+gate; local lands W1.5 before M12 close. One-pump assertion (both transports → one pump) kept in W1.5.

**REQ-RC-1 split:** cross-node int evidence at W1 gate; local int evidence at W1.5 (per-wave activation).

**W1 GATE: PASS (doyle 2026-06-14).** Independently verified: traceable-reqs check reproduced (196 complete / 0 findings; REQ-HOST-RUN-1 + REQ-RC-1 impl+unit+int, REQ-HOST-RUN-2 []); int test `attach.rs:217` inspected — real `launch_harness_brokered_in` spawns [session.self] into broker-A PTY, B attaches cross-node, HOSTED_OK round-trips, detach→Detached+survives, origin=handshake-identity (KH 7.5); self-dial-refused pin genuine. Trusted reported full-suite (46 bins/0) + clippy -D. **UNCOMMITTED** (awaiting operator).

**W1.5 BUILT (todlando 2026-06-14) — gate pinged to doyle, verdict pending.** B1 broker loopback-conn done (design in `M12-W1.5-PLAN.md` "Crystallized design"). Transport seam = `tokio::io::duplex` (ONE full-duplex pair wires a whole operator↔peer stream); only the leaf send/recv become enums (`SendHalf`/`RecvHalf`), so StreamLog/subscribe/send_stream/read-pump are byte-identical across QUIC + loopback. `nethost::dial_loopback` = reused SINGLETON, `remote_id_hex` = own-node id; `open_stream` on a `ConnKind::Loopback` mints the cross-wired PAIR (operator row + peer row); peer row (initiated_locally=false, own origin) → dispatch loop → serve_attach. Explicit `net-dial-loopback` frame (brain.net_dial_loopback + broker.dispatch_net_dial_loopback) — net_dial untouched. access same-node short-circuit (access.rs:89) Allows own-node → NO access change. rc.rs ONE self-dial line swapped → net_dial_loopback. ONE-PUMP PROOF by construction: shared `attach_drive_detach` helper called by BOTH cross-node `remote_attach_drives_a_real_pty_cross_daemon` AND new `local_attach_via_loopback_conn_rides_the_same_pump` — diverge only in conn acquisition + expected_origin. EVIDENCE: attach int 6/6 + new nethost unit `loopback_conn_is_a_reused_singleton_that_cross_wires_a_stream_pair`; full spt+spt-daemon suite **498 passed/0 failed**; clippy -D EXIT=0; traceable EXIT=0 (REQ-RC-1 [OK] impl+unit+int). **UNCOMMITTED** (operator-gated, m12-w1-bringup-rc).

**NEXT (after W1.5 gate verdict):** **REQ-HOST-RUN-2** (project cwd via additive `SpawnReq.cwd`) before M12 close. Then W2 picker / W3 manifest / W4 subnet QR / W5 whoami. Commit only when user asks (not on default branch unprompted). See [[traceable-per-wave-activation]], [[pump-ipc-deadline-fix]], [[spt-claude-code-next]] (perri waits on M12 gate).

-**W1.5 BUILT (todlando 2026-06-14) — gate pinged to doyle, verdict pending.** B1 broker loopback-conn done (design in `M12-W1.5-PLAN.md` "Crystallized design"). Transport seam = `tokio::io::duplex` (ONE full-duplex pair wires a whole operator↔peer stream); only the leaf send/recv become enums (`SendHalf`/`RecvHalf`), so StreamLog/subscribe/send_stream/read-pump are byte-identical across QUIC + loopback. `nethost::dial_loopback` = reused SINGLETON, `remote_id_hex` = own-node id; `open_stream` on a `ConnKind::Loopback` mints the cross-wired PAIR (operator row + peer row); peer row (initiated_locally=false, own origin) → dispatch loop → serve_attach. Explicit `net-dial-loopback` frame (brain.net_dial_loopback + broker.dispatch_net_dial_loopback) — net_dial untouched. access same-node short-circuit (access.rs:89) Allows own-node → NO access change. rc.rs ONE self-dial line swapped → net_dial_loopback. ONE-PUMP PROOF by construction: shared `attach_drive_detach` helper called by BOTH cross-node `remote_attach_drives_a_real_pty_cross_daemon` AND new `local_attach_via_loopback_conn_rides_the_same_pump` — diverge only in conn acquisition + expected_origin. EVIDENCE: attach int 6/6 + new nethost unit `loopback_conn_is_a_reused_singleton_that_cross_wires_a_stream_pair`; full spt+spt-daemon suite **498 passed/0 failed**; clippy -D EXIT=0; traceable EXIT=0 (REQ-RC-1 [OK] impl+unit+int). **UNCOMMITTED** (operator-gated, m12-w1-bringup-rc).

+**W1.5 GATE PASS (doyle 2026-06-14, independently verified — read shared helper + both int tests + reproduced trace).** B1 broker loopback-conn done (design in `M12-W1.5-PLAN.md` "Crystallized design"). Transport seam = `tokio::io::duplex` (ONE full-duplex pair wires a whole operator↔peer stream); only the leaf send/recv become enums (`SendHalf`/`RecvHalf`), so StreamLog/subscribe/send_stream/read-pump are byte-identical across QUIC + loopback. `nethost::dial_loopback` = reused SINGLETON, `remote_id_hex` = own-node id; `open_stream` on a `ConnKind::Loopback` mints the cross-wired PAIR (operator row + peer row); peer row (initiated_locally=false, own origin) → dispatch loop → serve_attach. Explicit `net-dial-loopback` frame (brain.net_dial_loopback + broker.dispatch_net_dial_loopback) — net_dial untouched. access same-node short-circuit (access.rs:89) Allows own-node → NO access change. rc.rs ONE self-dial line swapped → net_dial_loopback. ONE-PUMP PROOF by construction: shared `attach_drive_detach` helper called by BOTH cross-node `remote_attach_drives_a_real_pty_cross_daemon` AND new `local_attach_via_loopback_conn_rides_the_same_pump` — diverge only in conn acquisition + expected_origin. EVIDENCE: attach int 6/6 + new nethost unit `loopback_conn_is_a_reused_singleton_that_cross_wires_a_stream_pair`; full spt+spt-daemon suite **498 passed/0 failed**; clippy -D EXIT=0; traceable EXIT=0 (REQ-RC-1 [OK] impl+unit+int). **UNCOMMITTED** (operator-gated, m12-w1-bringup-rc).


Now REQ-HOST-RUN-2. Read its spec + SpawnReq struct + spawn path + harnesshost + cli run:


453	[[requirements]]
454	id = "REQ-HOST-RUN-2"
455	title = "Project-scoped working directory for spt-hosted bringup: `spt endpoint run` lands the broker-spawned harness PTY in the user's PROJECT cwd, not the daemon's, via an additive `SpawnReq.cwd` field carried through the broker PTY spawn (portable-pty CommandBuilder cwd). N-1-safe wire change (additive, defaulted). Required because the consumer (Claude Code) is project-scoped: broker-inherited cwd = the daemon's cwd = the wrong `.claude`, wrong session history, wrong digest source; `cc <id>` at a project root MUST land the harness in that project. W1 ships broker-inherited cwd as a bringup-proof shortcut only; this REQ must land before the M12 gate (doyle, 2026-06-14)."
456	required_stages = []
457

[[requirements]]
id = "REQ-HOST-RUN-2"
title = "Project-scoped working directory for spt-hosted bringup: `spt endpoint run` lands the broker-spawned harness PTY in the user's PROJECT cwd, not the daemon's, via an additive `SpawnReq.cwd` field carried through the broker PTY spawn (portable-pty CommandBuilder cwd). N-1-safe wire change (additive, defaulted). Required because the consumer (Claude Code) is project-scoped: broker-inherited cwd = the daemon's cwd = the wrong `.claude`, wrong session history, wrong digest source; `cc <id>` at a project root MUST land the harness in that project. W1 ships broker-inherited cwd as a bringup-proof shortcut only; this REQ must land before the M12 gate (doyle, 2026-06-14)."
required_stages = []


Found 8 files
crates\spt-daemon\tests\attach.rs
crates\spt-daemon\src\brain.rs
crates\spt-daemon\src\msg.rs
crates\spt\src\cli.rs
crates\spt-daemon\src\harnesshost.rs
crates\spt-term\src\lib.rs
crates\spt-term\src\pty.rs
crates\spt-runtime\src\runtime.rs


Grep

struct SpawnReq|pub program|pub args|pub rows|pub cols|pub endpoint|CommandBuilder|\.cwd\(|fn spawn|cmd\.cwd|build_command


crates\spt-term\src\lib.rs:51:pub use portable_pty::CommandBuilder;
crates\spt-term\src\surface.rs:25:    pub rows: u16,
crates\spt-term\src\surface.rs:27:    pub cols: u16,
crates\spt-term\src\pty.rs:21:use portable_pty::{native_pty_system, Child, ChildKiller, CommandBuilder, ExitStatus, MasterPty};
crates\spt-term\src\pty.rs:60:    pub fn spawn(cmd: CommandBuilder, size: SurfaceSize) -> Result<Self, SurfaceError> {
crates\spt-term\src\pty.rs:95:    pub fn spawn_program<P, A, S>(
crates\spt-term\src\pty.rs:105:        let mut cmd = CommandBuilder::new(program);
crates\spt-term\src\reader.rs:107:    fn spawn<R, S>(reader: R, writer: SharedWriter, mut sink: S) -> Self


90	            pid,
91	        })
92	    }
93
94	    /// Convenience: spawn `program` with `args` under a PTY sized to `size`.
95	    pub fn spawn_program<P, A, S>(
96	        program: P,
97	        args: A,
98	        size: SurfaceSize,
99	    ) -> Result<Self, SurfaceError>
100	    where
101	        P: AsRef<std::ffi::OsStr>,
102	        A: IntoIterator<Item = S>,
103	        S: AsRef<std::ffi::OsStr>,
104	    {
105	        let mut cmd = CommandBuilder::new(program);
106	        for a in args {
107	            cmd.arg(a);
108	        }
109	        Self::spawn(cmd, size)
110	    }
111
112	    /// Take the master reader. Returns [`SurfaceError::ReaderTaken`] on the
113	    /// second call — output is drained by exactly one consumer (the A2 drain
114	    /// thread in the hosted path; a test consumer otherwise).
115	    pub fn take_reader(&self) -> Result<Box<dyn Read + Send>, SurfaceError> {
116	        self.reader
117	            .lock()
118	            .unwrap()
119	            .take()
120	            .ok_or(SurfaceError::ReaderTaken)
121	    }
122
123	    /// The shared writer handle — cloned to the A2 DSR drain thread so its
124	    /// auto-answer goes through the same serialized writer the caller injects
125	    /// input through (no torn writes).
126	    pub(crate) fn shared_writer(&self) -> SharedWriter {
127	        Arc::clone(&self.writer)
128	    }
129
130	    /// Poll for child exit without blocking.
131	    pub fn try_wait(&self) -> Result<Option<ExitStatus>, SurfaceError> {
132	        self.child
133	            .lock()
134	            .unwrap()

            pid,
        })
    }

    /// Convenience: spawn `program` with `args` under a PTY sized to `size`.
    pub fn spawn_program<P, A, S>(
        program: P,
        args: A,
        size: SurfaceSize,
    ) -> Result<Self, SurfaceError>
    where
        P: AsRef<std::ffi::OsStr>,
        A: IntoIterator<Item = S>,
        S: AsRef<std::ffi::OsStr>,
    {
        let mut cmd = CommandBuilder::new(program);
        for a in args {
            cmd.arg(a);
        }
        Self::spawn(cmd, size)
    }

    /// Take the master reader. Returns [`SurfaceError::ReaderTaken`] on the
    /// second call — output is drained by exactly one consumer (the A2 drain
    /// thread in the hosted path; a test consumer otherwise).
    pub fn take_reader(&self) -> Result<Box<dyn Read + Send>, SurfaceError> {
        self.reader
            .lock()
            .unwrap()
            .take()
            .ok_or(SurfaceError::ReaderTaken)
    }

    /// The shared writer handle — cloned to the A2 DSR drain thread so its
    /// auto-answer goes through the same serialized writer the caller injects
    /// input through (no torn writes).
    pub(crate) fn shared_writer(&self) -> SharedWriter {
        Arc::clone(&self.writer)
    }

    /// Poll for child exit without blocking.
    pub fn try_wait(&self) -> Result<Option<ExitStatus>, SurfaceError> {
        self.child
            .lock()
            .unwrap()